2025-02-04 08:00:00
之前用了一段时间飞书日历,想要把日历里的事件导出来备份,但是发现飞书自己的导出功能太弱,因此参考 从飞书导出日历到 Fastmail - Xuanwo's Blog 进行了导出的尝试。
上面提到的文章,是通过 CalDAV 的方式进行的日历同步。因此我第一步也是配置飞书的 CalDAV 服务:
按照界面所示,配置 CalDAV 同步,就可以得到用于 CalDAV 的域名、用户名和密码了。如果只是要订阅,那么到这一步,就可以直接用 CalDAV 客户端来同步了。但我想进一步得到 iCalendar 格式的日历文件。
于是我参考了上述文章的评论区的做法:
@jason5ng32jason5ng32Oct 28, 2024分享一下我的方法:1. 在服务器上安装 vdirsyncer ,这个工具可以同步 CalDAV 的内容,在同步设置里,不需要先找到 UUID,可以直接用飞书提供的 URL。2. 写一个 Python 脚本,将 vdirsyncer 同步的内容合并成单一的 ics 文件。3. 将 ics 文件放到一个地址稍微复杂一点的 http 目录里,可以外部访问。4. 写一个 run.sh 脚本,通过 crontab 每 10 分钟执行一次 vdirsyncer 同步和日历文件合成。
也就是说,用 vdirsyncer 把日历同步到本地,再转换为 iCalendar 格式的日历文件。参考 vdirsyncer 文档,这件事情并不复杂:
pip3 install vdirsyncer
~/.vdirsyncer/config
,填入在飞书处得到的用户密码: [general]status_path = "~/.vdirsyncer/status/"[pair my_contacts]a = "my_contacts_local"b = "my_contacts_remote"collections = ["from a", "from b"][storage my_contacts_local]type = "filesystem"path = "~/.contacts/"fileext = ".ics"[storage my_contacts_remote]type = "caldav"url = "https://caldav.feishu.cn"username = "REDACTED"password = "REDACTED"
vdirsyncer discover && vdirsyncer sync
此时在 ~/.contacts
目录下,已经能看到很多个 ics 文件了,每个 ics 文件对应了日历中的一个事件。实际上,这些文件就已经是 iCalendar 格式了,只不过每个文件只有一个事件。
为了让一个 .ics
文件包括日历的所有事件,写了一个脚本,实际上就是处理每个 ics 文件,去掉每个文件开头结尾的 BEGIN:VCALENDAR
和 END:VCALENDAR
,把中间的部分拼起来,最后再加上开头结尾:
import sysall_lines = []all_lines += ["BEGIN:VCALENDAR"]for f in sys.argv[1:]: content = open(f).read().strip() lines = content.splitlines() all_lines += lines[1:-1]all_lines += ["END:VCALENDAR"]print("\n".join(all_lines))
运行上述脚本:python3 dump.py ~/.contacts/*/*.ics > dump.ics
,这样得到的 .ics
文件就可以直接导入到日历软件了。
注:也可以用类似的方法导出 iCloud 国区的日历:把 url 改成 "https://caldav.icloud.com.cn"
,在 Apple ID 上生成 App 密码,填入上面的 password,再把邮箱写到 username 即可。
2025-01-10 08:00:00
前段时间测试了 AMD/Apple/Qualcomm/ARM 的处理器的微架构,自然不能漏了 Intel。虽然 Intel 已经出了 Redwood Cove 和 Lion Cove,但手上没有设备,而且 Golden Cove 也是“相对比较成功”(“缩缸的是 Raptor Cove,和我 Golden Cove 有什么关系,虽然其实 Raptor Cove 是 Golden Cove Refresh”)的一代微架构,用在了 Alder Lake 和 Sapphire Rapids 上,因此就来分析它,后续有机会也会分析一下对应的 E 核架构 Gracemont。
Intel 关于 Golden Cove 微架构有这些官方的信息:
网上已经有较多针对 Golden Cove 微架构的评测和分析,建议阅读:
下面分各个模块分别记录官方提供的信息,以及实测的结果。读者可以对照已有的第三方评测理解。官方信息与实测结果一致的数据会加粗。
Intel Golden Cove 的性能测试结果见 SPEC。
官方信息:
官方信息:
官方信息:
Intel 的 uOP(Micro-OP) Cache 称为 Decode Stream Buffer (DSB): Decode Stream Buffer (DSB) is a Uop-cache that holds translations of previously fetched instructions that were decoded by the legacy x86 decode pipeline (MITE).
。
uOP Cache 的组织方式通常是组相连,每个 entry 保存了几条 uOP,这些 uOP 对应了原来指令流中连续的几条指令。
为了测试 uOP Cache 的大小,构造不同大小的循环,循环体是复制若干份的 add %%rsi, %%rdx
指令,最后是 dec + jnz
作为循环结尾,通过 IDQ.DSB_UOPS 性能计数器统计每次循环有多少个 uOP 来自于 DSB 也就是 uOP Cache,发现其最大值为 2800 左右,距离 4K 还有一定的距离。目前还没有找到一个可以稳定跑出 4K uOP 的指令模式,不知道遇到了什么瓶颈。
考虑到 taken branch 在典型的 uOP Cache 设计中会结束一个 entry,把循环体改成若干条 jmp
指令,并且每个 64B 缓存行只有一条 jmp
指令,此时每个 uOP entry 只记录一条 jmp
指令。观察到每次循环最多 512 个 uOP 来自 uOP Cache,那么 Golden Cove 的 uOP Cache 大概就是 512 个 entry。如果改成每 128B 缓存行只有一条 jmp
指令,uOP Cache 容量减少到 256 个 entry;继续增加间距,256B 间距对应 128 个 entry,512B 间距对应 64 个 entry,1024B 间距对应 32 个 entry,2048B 间距对应 16 个 entry,4096B 间距对应 8 个 entry,继续增大间距后,entry 数维持中 8 不再减少,意味着 Golden Cove 的 uOP Cache 是 8 Way 64 Set 一共 512 Entry,Index 是 PC[11:6]。
那么按照官方信息所说的 4K 容量,一共 512 个 Entry,那么每个 Entry 应该能够记录最多 8 个 uOP,这正好也对应上了 8 uOP 的吞吐。
根据前人在 Intel 比较老的微架构上的测试结果(见 The microarchitecture of Intel, AMD, and VIA CPUs)以及 Intel 的官方文档 Software Optimization Manual(这个文档把 uOP Cache 叫做 Decoded ICache),Intel 之前很多代微架构的 uOP Cache Entry 的构造条件是:
3*6=18
个 uOP(The Decoded ICache can hold only up to 18 micro-ops per each 32 byte aligned memory chunk
);如果指令跨了 32B 边界,它被算在后面那个 32B 里面each unconditional branch is the last micro-op occupying a Decoded ICache Way
)下面来分析 Golden Cove 上这些构造条件是否有变化。参考 I See Dead µops: Leaking Secrets via Intel/AMD Micro-Op Caches 的方法,构造了一个循环,循环体由 4x 15-byte-nop + 1x 4-byte-nop
组成,这样的 5 条指令填满了对齐的 64B。在 Golden Cove 上测试,发现依然可以用满 512 个 Entry,假如 Entry 不能跨越 32B 边界,那么这 5 条指令至少就要 2 个 Entry,但实际上只用了 1 个 Entry。这说明 Golden Cove 上 uOP Cache Entry 的第一条限制中,Entry 不能跨越的边界,从 32B 扩大到了 64B,毕竟每个 Entry 能存的 uOP 数量也增多了,如果继续限制 32B,每个 Entry 就很难存满 8 个 uOP 了。接下来测试对齐的 64B 内可以最多有多少个 entry。
把循环体改成每对齐的 64B 就有四条 jmp 指令,前一条 jmp 指令跳转到后一条 jmp 指令,模拟每 64B 有四个 Entry 的情况:
测试发现这个情况下能达到 512 个 Entry。说明对齐的 64B 内至少可以存 4 个 Entry。
进一步测试,如果每对齐的 64B 有五条 jmp 指令,模拟每 64B 有五个 Entry 的情况:
发现最高的 Entry 数只有 480 左右,不确定是遇到了什么限制,如果对齐的 64B 内不能存 5 个 Entry,也不应该得到 480 这个结果。
如果单独去测试每个对齐的 64B 能缓存多少个 uOP,比如每个对齐的 64B 里由若干条 nop 加最后一条跳到下一个 64B 开头的 jmp 指令组成,会发现当对齐的 64B 内的 uOP 个数从 36 个变成 37 个时,uOP Cache 命中率急剧下降。这意味着,每对齐的 64B 内依然不能存超过 36 个 uOP。这类似于原来的每对齐的 32B 内不能存超过 18 个 uOP,但粒度更粗,实际上更加宽松,比如对齐的 64B 内的前 32B 可以全是 NOP 指令,只要 64B 内总数不超过 36 就可以。但比较奇怪的是,36 uOP per 64B 不能整除 8 uOP/Entry,不像原来的 18 per 32B 可以整除 6 uOP/Entry。
官方信息:
构造一系列的 jmp 指令,使得 jmp 指令分布在不同的 page 上,使得 ITLB 成为瓶颈:
可以看到 256 个 Page 出现了明显的拐点,对应的就是 256 的 L1 ITLB 容量。注意要避免 ICache 和 BTB 的容量成为瓶颈,把 jmp 指令分布在不同的 Cache Line 和 BTB entry 上。
超过 256 个 Page 以后,如图有周期数突然下降后缓慢上升的情况(例如横坐标 288->289、320->321、352->353、384->385 等,以 32 为周期),背后的原理需要进一步分析。
扩大 jmp 指令的距离再测试:
从这个结果来看,L1 ITLB 对于 4K 页应该是 32 Set 8 Way。
官方信息:
为了测试 L1 ICache 容量,构造一个具有巨大指令 footprint 的循环,由大量的 4 字节 nop 和最后的分支指令组成。观察在不同 footprint 大小下的 IPC:
可以看到 footprint 在 32 KB 之前时可以达到 6 IPC,之后则降到 4 IPC,这里的 32 KB 就对应了 L1 ICache 的容量。
构造不同深度的调用链,测试每次调用花费的平均时间,得到下面的图:
可以看到调用链深度为 20 时性能突然变差,因此 Return Stack 深度为 20。
官方信息:
Golden Cove 架构针对循环做了优化,Loop Stream Detector(简称 LSD)会检测当前指令流是否在一个循环当中,并且循环的 uop 不超出 Instruction Decode Queue(IDQ) 的容量,那么 LSD 会把 Legacy decode pipeline(MITE) 和 Decode stream buffer(DSB) 关掉,不再让 IDQ 的指令出队,而是直接在 IDQ 的内部循环提供指令,这个时候就节省了很多处理器前端的功耗。
为了测试 Instruction Decode Queue 的大小,构造不同大小的循环,循环体是复制若干份的 inc %rsi
指令,最后是 dec + jnz
作为循环结尾,通过 LSD.UOPS 性能计数器统计每次循环有多少个 UOP 来自于 Loop Stream Detector 机制,发现其最大值为 144,说明 Golden Cove 的 Loop Stream Detector 可以识别最多 144 个 uop 的循环。此时每个循环要执行 145 条指令,最后的 dec + jnz
被融合成了一个 uop。
循环体中,如果用 nop
指令来填充,会得到 40 左右的小得多的容量,猜测是进入了低功耗模式。
官方信息:
官方信息:
官方信息:
针对 Load Store 带宽,实测每个周期可以完成:
因为测试环境是 Client 而非 Server,所以 AVX512 被屏蔽了,无法测试 AVX512 的读写带宽。此时最大的读带宽是 96B/cyc,最大的写带宽是 64B/cyc,二者不能同时达到。
官方信息:
经过实际测试,Golden Cove 上如下的情况可以成功转发,对地址 x 的 Store 转发到对地址 y 的 Load 成功时 y-x 的取值范围:
Store\Load | 8b Load | 16b Load | 32b Load | 64b Load |
---|---|---|---|---|
8b Store | {0} | {} | {} | {} |
16b Store | [0,1] | {0} | {} | {} |
32b Store | [0,3] | [0,2] | {0} | {} |
64b Store | [0,7] | [0,6] | [0,4] | {0} |
可以看到,Golden Cove 在 Store 完全包含 Load 的情况下都可以转发,没有额外的对齐要求。但当 Load 和 Store 只有部分重合时,就无法转发,这和官方信息有所冲突。两个连续的 32 位的 Store 和一个 64 位的 Load 重合也不能转发。
比较有意思的是,在 y=x 且不跨越缓存行边界且满足下列要求的情况下,Store Forwarding 不会带来性能损失,就好像 Load Store 访问的是不同的没有 Overlap 的地址一样:
考虑到 y 必须等于 x,也就是地址要一样,并且没有带来性能损失,猜测 Golden Cove 使用了类似 Memory Renaming 的技术来实现这个效果。如果是连续两个对同一个地址的 Store 对一个 Load 的转发,效果和只有一个 Store 是一样的。
除了上述情况以外,Store Forwarding 成功时的延迟在 5 个周期,失败则要 19 个周期。
小结:Golden Cove 的 Store to Load Forwarding:
官方信息:
构造不同大小 footprint 的 pointer chasing 链,测试不同 footprint 下每条 load 指令耗费的时间:
可以看到 48KB 出现了明显的拐点,对应的就是 48KB 的 L1 DCache 容量。第二个拐点在 384KB,对应的是 L1 DTLB 的容量。
官方信息:
用类似测 L1 DCache 的方法测试 L1 DTLB 容量,只不过这次 pointer chasing 链的指针分布在不同的 page 上,使得 DTLB 成为瓶颈:
可以看到 96 Page 出现了明显的拐点,对应的就是 96 的 L1 DTLB 容量。没有超出 L1 DTLB 容量前,Load to use latency 是 5 cycle;超出 L1 DTLB 容量后,Load to use latency 是 12 cycle,说明 L1 DTLB miss 带来了 7 cycle 的损失。
官方信息:
沿用之前测试 L1 DTLB 的方法,把规模扩大到 L2 Unified TLB 的范围,就可以测出来 L2 Unified TLB 的容量,下面是 Golden Cove 上的测试结果:
第一个拐点是 96 个 Page,对应 L1 DTLB,此时 CPI 从 5 提升到 12;第二个拐点是 768,对应 L1 DCache,此时 CPI 从 12 提升到 23;第三个拐点是 1600 左右,而没有到 2048,猜测有 QoS 限制了数据对 L2 TLB 的占用。
官方信息:
构造不同大小 footprint 的 pointer chasing 链,测试不同 footprint 下每条 load 指令耗费的时间:
Intel Golden Cove 的处理器通过 MSR 1A4H 可以配置各个预取器(来源:Software Developers Manual,MSRs Supported by 12th and 13th Generation Intel® Core™ Processor P-core):
在 Golden Cove 上按 64B 的跳步进行访存,测量每次访存的延迟,得到如下结果:
可以观察到在 48KB 之内是 5 cycle latency,在 L2 Cache 范围内是 5-8 cycle latency。
如果通过 wrmsr -p 0 0x1a4 0x8
把 DCU_IP_PREFETCHER_DISABLE
设为 1,即关闭 L1 data cache IP prefetcher,再在 0 号核心上重新跑上面的测试,得到如下结果:
就可以看到 L2 Cache 的范围内的性能退化到了 16 Cycle,和随机 pointer chasing 一样。关闭其他的 prefetcher 则没有这个现象,说明正是 L1 data cache IP prefetcher 实现了针对 L1 的 Stride Prefetcher。
更进一步,参考 Battling the Prefetcher: Exploring Coffee Lake (Part 1) 的方式,研究 Stride 预取器的行为:分配一片内存,把数据从缓存中 flush 掉,再按照特定的访存模式访问,触发预取器,最后测量访问每个缓存行的时间,从而得到预取器预取了哪些缓存行的信息。
首先是只访问一个 cache line 的时候,可以看到,除了已经访问过的 cache line,其他 cache line 都出现了缓存缺失,说明此时预取器没有在工作:
接下来,按照固定的 stride 访问各个缓存行,发现当访问了五个 cache line 时,预取器会比较稳定地预取第六个 cache line:
继续增加访问次数,可以看到预取器总是会预取将要访问的下一个 cache line:
如果通过 wrmsr -p 0 0x1a4 0x8
把 DCU_IP_PREFETCHER_DISABLE
设为 1,即关闭 L1 data cache IP prefetcher,就会观察到上述 Stride 预取的行为消失,不会预取将要访问的下一个 cache line。
把相同的代码放到 Gracemont 上运行,会看到它的预取器会预取将要访问的未来两个 cache line:
可见不同微架构的预取器的策略是不同的。
官方信息:
为了测试 ROB 的大小,设计了一个循环,循环开始和结束是长延迟的 long latency load。中间是若干条 NOP 指令,当 NOP 指令比较少时,循环的时候取决于 load 指令的时间;当 NOP 指令数量过多,填满了 ROB 以后,就会导致 ROB 无法保存循环末尾的 load 指令,性能出现下降。测试结果如下:
当 NOP 数量达到 512 时,性能开始急剧下滑,说明 Golden Cove 的 ROB 大小是 512。
2024-12-27 08:00:00
最近做了不少微架构的评测,其中涉及到了很多的 CPU 微架构的逆向:
因此总结一下 CPU 微架构逆向方法学。
首先定义一下:什么是 CPU 微架构逆向,我认为 CPU 微架构逆向包括两部分含义:
举一个例子,已经知道某 CPU 微架构有一个组相连的 L1 DCache,但不知道它的容量,几路组相连,此时通过微架构逆向的方法,可以得到它的容量,具体是几路组相连,进一步可能把它的 Index 函数也逆向出来。这是第一部分含义。
再举一个例子,已经知道某 CPU 微架构有一个分支预测器,但不知道它使用了什么信息来做预测,可能用了分支的地址,可能用了分支要跳转的目的地址,可能用了分支的方向,这时候通过微架构逆向的方法,对不同的可能性做排除,找到真正的那一个。如果不能排除到只剩一个可能,或者全部可能都被排除掉,说明实际的微架构设计和预期不相符。
第一部分含义,目前已经有大量的成熟的 Microbenchmark(针对微架构 Microarchitecture 设计的 Benchmark,叫做 Microbenchmark)来解决,它们针对常见的微架构设计,实现了对相应设计参数的逆向的 Microbenchmark,可以在很多平台上直接使用。第二部分含义,目前还只能逐个分析,去猜测背后的设计,再根据设计去构造对应该设计的 Microbenchmark。
下面主要来介绍,设计和实现 Microbenchmark 的方法学。
首先要了解 Microbenchmark 的原理,它的核心思路就是,通过构造程序,让某个微架构部件成为瓶颈,接着在想要逆向的设计参数的维度上进行扫描,通过某种指标来反映是否出现了瓶颈,通过瓶颈对应的设计参数,就可以逆向出来设计参数的取值。这一段有点难理解,下面给一个例子:
比如要测试的是 L1 DCache 的容量,那就希望 L1 DCache 的容量变成瓶颈。为了让它成为瓶颈,那就需要不断地访问一片内存,它的大小比 L1 DCache 要更大,让 L1 DCache 无法完整保存下来,出现缓存缺失。为了判断缓存缺失是否出现,可以通过时间或周期,因为缓存缺失肯定会带来性能损失,也可以直接通过缓存缺失的性能计数器。既然要逆向的设计参数是 L1 DCache 的容量,那就在容量上进行一个扫描:在内存中开辟不同大小的数组,比如一个是 32KB,另一个是 64KB,每次测试的时候只访问其中一个数组。每个数组扫描访问若干次,然后统计总时间或周期数或缓存缺失次数。假如实际 L1 DCache 容量介于 32KB 和 64KB 之间,那么应该可以观察到 64KB 数组大小测得的性能相比 32KB 有明显下降。如果把测试粒度变细,每 1KB 设置一个数组大小,最终就可以确定实际的 L1 DCache 容量。
在上面这个例子里,成为瓶颈的微架构部件是 L1 DCache,想要逆向的设计参数是它的容量,反映是否出现瓶颈的指标是性能或缓存缺失次数,构造的程序做的事情是不断地访问一个可变大小的数组,其中数组大小和想要逆向的设计参数是挂钩的。
因此可以总结出 Microbenchmark 设计的几个要素:
比如上面的 L1 DCache 容量的测试上,这几个要素的回答是:
假如要设计一个针对 ROB(ReOrder Buffer) 容量的测试,思考同样的要素:
思考明白这些要素,就可以知道怎么设计出一个 Microbenchmark 了。
原理介绍完了,下面介绍一些常用的方法。
上面提到,为了反映出瓶颈,需要有一个指标,它最好能够精确地反映出瓶颈的发生与否,同时也尽量要减少噪声。能用的指标不多,只有两类:
虽然测时间最简单也最通用,但它会受到频率波动的限制,如果在运行测试的时候,频率剧烈变化(特别是手机平台),引入了大量噪声,就会导致有效信息被淹没在噪声当中。
其中性能计数器是最为精确的,虽然使用起来较为麻烦,但也确实支撑了很多更深入的 CPU 微架构的逆向。希望硬件厂商看到这篇文章,不要为了避免逆向把性能计数器藏起来:因为它对于应用的性能分析真的很有用。具体怎么用性能计数器,可以参考一些现成的 Microbenchmark 框架。
在有异构核的处理器上,为了保证测试的是预期微架构的核心,一般还会配合绑核,绑核在除了 macOS 和 iOS 以外的系统都可以直接指定绑哪个核心,而 macOS 和 iOS 只能通过指定 QoS 来建议调度器调度到 P 核还是 E 核,首先是不能确定是哪个 P 核或哪个 E 核,其次这只是个建议,并非强制。
接下来介绍一些构造瓶颈的一些常见套路:
再介绍一些常见的坑:
实际上,现在已经有很多现成的 Microbenchmark,以及一些记录了 Microbenchmark 的文档:
以及一些用 Microbenchmark 做逆向并公开的网站:
如果你想要去逆向某个微架构的某个部件,但不知道怎么做,不妨在上面这些网站上寻找一下,是不是已经有现成的实现了。
如果你对如何编写这些 Microbenchmark 不感兴趣,也可以试试在自己电脑上运行这些程序,或者直接阅读已有的分析。
2024-12-26 08:00:00
虽然 Apple M1 已经是 2020 年的处理器,但它对苹果自研芯片来说是一个里程碑,考虑到 X Elite 处理器的 Oryon 微架构和 Apple M1 性能核 Firestorm 微架构的相似性,还是测试一下这个 Firestorm + Icestorm 微架构在各个方面的表现。Apple A14 采用了和 Apple M1 一样的微架构。
Apple M1 的官方信息乏善可陈,关于微架构的信息几乎为零,但能从操作系统汇报的硬件信息中找到一些内容。
网上已经有较多针对 Apple M1 微架构的评测和分析,建议阅读:
下面分各个模块分别记录官方提供的信息,以及实测的结果。读者可以对照已有的第三方评测理解。官方信息与实测结果一致的数据会加粗。
Apple Firestorm 的性能测试结果见 SPEC。Apple Icestorm 尚未进行性能测试。
Apple M1 预装的是 macOS,macOS 的绑核只能绑到 P 或者 E,不能具体到某一个核上;在 macOS 上可以读取 PMU,需要使用 kpep 的私有框架,代码可以在这里找到。
如果想更方便地进行测试,建议安装 Asahi Linux 的各种发行版,此时可以在 Linux 下自由地绑核,也可以用标准的方式使用 PMU。
为了测试实际的 Fetch 宽度,参考 如何测量真正的取指带宽(I-fetch width) - JamesAslan 构造了测试。其原理是当 Fetch 要跨页的时候,由于两个相邻页可能映射到不同的物理地址,如果要支持单周期跨页取指,需要查询两次 ITLB,或者 ITLB 需要把相邻两个页的映射存在一起。这个场景一般比较少,处理器很少会针对这种特殊情况做优化,但也不是没有。经过测试,把循环放在两个页的边界上,发现 Firestorm 微架构遇到跨页的取指时确实会拆成两个周期来进行。在此基础上,构造一个循环,循环的第一条指令放在第一个页的最后四个字节,其余指令放第二个页上,那么每次循环的取指时间,就是一个周期(读取第一个页内的指令)加上第二个页内指令需要 Fetch 的周期数,多的这一个周期就足以把 Fetch 宽度从后端限制中区分开,实验结果如下:
图中蓝线(cross-page)表示的就是上面所述的第一条指令放一个页,其余指令放第二个页的情况,横坐标是第二个页内的指令数,那么一次循环的指令数等于横坐标 +1。纵坐标是运行很多次循环的总 cycle 数除以循环次数,也就是平均每次循环耗费的周期数。可以看到每 16 条指令会多一个周期,因此 Firestorm 的前端取指宽度确实是 16 条指令。为了确认这个瓶颈是由取指造成的,又构造了一组实验,把循环的所有指令都放到一个页中,这个时候 Fetch 不再成为瓶颈(图中 aligned),两个曲线的对比可以明确地得出上述结论。
用相同的方式测试 Icestorm,结果如下:
可以看到每 8 条指令会多一个周期,意味着 Icestorm 的前端取指宽度为 8 条指令。
官方信息:通过 sysctl 可以看到,Firestorm 具有 192KB L1 ICache,Icestorm 具有 128KB L1 ICache:
为了测试 L1 ICache 容量,构造一个具有巨大指令 footprint 的循环,由大量的 nop 和最后的分支指令组成。观察在不同 footprint 大小下 Firestorm 的 IPC:
可以看到 footprint 在 192 KB 之前时可以达到 8 IPC,之后则快速降到 2.22 IPC,这里的 192 KB 就对应了 Firestorm 的 L1 ICache 的容量。虽然 Fetch 可以每周期 16 条指令,也就是一条 64B 的缓存行,由于后端的限制,只能观察到 8 的 IPC。
用相同的方式测试 Icestorm,结果如下:
可以看到 footprint 在 128 KB 之前时可以达到 4 IPC,之后则快速降到 2.10 IPC,这里的 128 KB 就对应了 Icestorm 的 L1 ICache 的容量。虽然 Fetch 可以每周期 8 条指令,由于后端的限制,只能观察到 4 的 IPC。
构造大量的无条件分支指令(B 指令),BTB 需要记录这些指令的目的地址,那么如果分支数量超过了 BTB 的容量,性能会出现明显下降。当把大量 B 指令紧密放置,也就是每 4 字节一条 B 指令时:
可见在 1024 个分支之内可以达到 1 的 CPI,超过 1024 个分支,出现了 3 CPI 的平台,一直延续到 49152 个分支。超出 BTB 容量以后,分支预测时,无法从 BTB 中得到哪些指令是分支指令的信息,只能等到取指甚至译码后才能后知后觉地发现这是一条分支指令,这样就出现了性能损失,出现了 3 CPI 的情况。第二个拐点 49152,对应的是指令 footprint 超出 L1 ICache 的情况:L1 ICache 是 192KB,按照每 4 字节一个 B 指令计算,最多可以存放 49152 条 B 指令。
降低分支指令的密度,在 B 指令之间插入 NOP 指令,使得每 8 个字节有一条 B 指令,得到如下结果:
可以看到 CPI=1 的拐点前移到 1024 个分支,同时 CPI=3 的平台的拐点也前移到了 24576。拐点的前移,意味着 BTB 采用了组相连的结构,当 B 指令的 PC 的部分低位总是为 0 时,组相连的 Index 可能无法取到所有的 Set,导致表现出来的 BTB 容量只有部分 Set,例如此处容量减半,说明只有一半的 Set 被用到了。
如果进一步降低 B 指令的密度,使得它的低若干位都等于 0,最终 CPI=1 的拐点定格在 2 条分支,此时分支的间距大于或等于 2048B;CPI=3 的拐点定格在 6 条分支,此时分支的间距大于或等于 32KB。根据这个信息,可以认为 Firestorm 的 BTB 是 512 Set 2 Way 的结构,Index 是 PC[10:2];同时也侧面佐证了 192KB L1 ICache 是 512 Set 6 Way,Index 是 PC[14:6]。
用相同的方式测试 Icestorm,首先用 4B 的间距:
可以看到 1024 的拐点,1024 之前是 1 IPC,之后增加到 3 IPC。比较奇怪的是,没有看到第二个拐点,第二个拐点在 8B 的间距下显现:
第一个拐点前移到 512,第二个拐点出现在 16384,而 Icestorm 的 L1 ICache 容量是 128KB,8B 间距下正好可以保存 16384 个分支。
用 16B 间距测试:
第一个拐点前移到 256,然后出现了一个 2 CPI 的新平台,2 CPI 的平台的拐点出现在 2048,第三个拐点出现在 8192,对应 L1 ICache 容量。
用 32B 间距测试:
第一个拐点在 1024,第二个拐点出现在 4096,对应 L1 ICache 容量,没有观察到 2 CPI。
用 64B 间距测试:
第一个拐点在 512,第二个拐点出现在 2048,对应 L1 ICache 容量。
Icestorm 的 BTB 测试结果并不像 Firestorm 那样有规律,根据这个现象,给出一些猜测:
针对 4B 间距没有出现 CPI>3 的情况,给出一些猜测:
构造一系列的 B 指令,使得 B 指令分布在不同的 page 上,使得 ITLB 成为瓶颈,在 Firestorm 上进行测试:
从 1 Cycle 到 3 Cycle 的增加是由于 L1 BTB 的冲突缺失,之后在 192 个页时从 3 Cycle 快速增加到 13 Cycle,则对应了 192 项的 L1 ITLB 容量。
在 Icestorm 上重复实验:
只有一个拐点,在 128 个页时,性能从 1 Cycle 下降到 8 Cycle,意味 L1 ITLB 容量是 128 项。
从前面的测试来看,Firestorm 最大观察到 8 IPC,Icestorm 最大观察到 4 IPC,那么 Decode 宽度也至少是这么多,暂时也不能排除有更大的 Decode 宽度。
构造不同深度的调用链,测试每次调用花费的平均时间,在 Firestorm 上得到下面的图:
可以看到调用链深度为 50 时性能突然变差,因此 Firestorm 的 Return Stack 深度为 50。在 Icestorm 上测试:
可以看到调用链深度为 32 时性能突然变差,因此 Icestorm 的 Return Stack 深度为 32。
为了测试物理寄存器堆的大小,一般会用两个依赖链很长的操作放在开头和结尾,中间填入若干个无关的指令,并且用这些指令来耗费物理寄存器堆。Firestorm 测试结果见下图:
Icestorm 测试结果如下:
注意这里测试的都是能够用于预测执行的寄存器数量,实际的物理寄存器堆还需要保存架构寄存器。但具体保存多少个架构寄存器不确定,但至少 32 个整数通用寄存器和浮点寄存器是一定有的,但可能还有一些额外的需要重命名的状态也要算进来。
官方信息:通过 sysctl 可以看到,Firestorm 具有 128KB L1 DCache,Icestorm 具有 64KB L1 DCache:
构造不同大小 footprint 的 pointer chasing 链,测试不同 footprint 下每条 load 指令耗费的时间,Firestorm 上的结果:
可以看到 128KB 出现了明显的拐点,对应的就是 128KB 的 L1 DCache 容量。L1 DCache 范围内延迟是 3 cycle,之后提升到 16 cycle。
Icestorm 上的结果:
可以看到 64KB 出现了明显的拐点,对应的就是 64KB 的 L1 DCache 容量。L1 DCache 范围内延迟是 3 cycle,之后提升到 14 cycle。
用类似的方法测试 L1 DTLB 容量,只不过这次 pointer chasing 链的指针分布在不同的 page 上,使得 DTLB 成为瓶颈,在 Firestorm 上:
从 160 个页开始性能下降,到 250 个页时性能稳定在 9 CPI,认为 Firestorm 的 L1 DTLB 有 160 项。9 CPI 包括了 L1 DTLB miss L2 TLB hit 带来的额外延迟。
如果每两个页放一个指针,则拐点前移到 80;每四个页放一个指针,拐点变成 40;每八个页放一个指针,拐点变成 20;每 16 个页一个指针,拐点是 10;每 32 个页一个指针,拐点变成 5;每 64 个页一个指针,拐点依然是 5。说明 Firestorm 的 L1 DTLB 是 5 路组相连,32 个 Set,Index 是 VA[18:14],注意页表大小是 16KB。
Icestorm:
从 128 个页开始性能下降,到 160 个页时性能稳定在 10 CPI,认为 Icestorm 的 L1 DTLB 有 128 项。10 CPI 包括了 L1 DTLB miss L2 TLB hit 带来的额外延迟。
如果每两个页放一个指针,则拐点前移到 64;每四个页放一个指针,拐点变成 32;每八个页放一个指针,拐点变成 16;每 16 个页一个指针,拐点是 8;每 32 个页一个指针,拐点变成 4;每 64 个页一个指针,拐点依然是 4。说明 Icestorm 的 L1 DTLB 是 4 路组相连,32 个 Set,Index 是 VA[18:14]。
针对 Load Store 带宽,实测 Firestorm 每个周期可以完成:
如果把每条指令的访存位宽从 128b 改成 256b,读写带宽不变,指令吞吐减半。也就是说最大的读带宽是 48B/cyc,最大的写带宽是 32B/cyc,二者不能同时达到。
实测 Icestorm 每个周期可以完成:
如果把每条指令的访存位宽从 128b 改成 256b,读写带宽不变,指令吞吐减半。也就是说最大的读带宽是 32B/cyc,最大的写带宽是 16B/cyc,二者不能同时达到。
为了预测执行 Load,需要保证 Load 和之前的 Store 访问的内存没有 Overlap,那么就需要有一个预测器来预测 Load 和 Store 之前在内存上的依赖。参考 Store-to-Load Forwarding and Memory Disambiguation in x86 Processors 的方法,构造两个指令模式,分别在地址和数据上有依赖:
str x3, [x1]
和 ldr x3, [x2]
str x2, [x1]
和 ldr x1, [x2]
初始化时,x1
和 x2
指向同一个地址,重复如上的指令模式,观察到多少条 ldr
指令时会出现性能下降,在 Firestorm 上测试:
数据依赖没有明显的阈值,但从 84 开始出现了一个小的增长,且斜率不为零;地址依赖的阈值是 70。
Icestorm:
数据依赖和地址依赖的阈值都是 13。
经过实际测试,Firestorm 上如下的情况可以成功转发,对地址 x 的 Store 转发到对地址 y 的 Load 成功时 y-x 的取值范围:
Store\Load | 8b Load | 16b Load | 32b Load | 64b Load |
---|---|---|---|---|
8b Store | {0} | [-1,0] | [-3,0] | [-7,0] |
16b Store | [0,1] | [-1,1] | [-3,1] | [-7,1] |
32b Store | [0,3] | [-1,3] | [-3,3] | [-7,3] |
64b Store | [0,7] | [-1,7] | [-3,7] | [-7,7] |
从上表可以看到,所有 Store 和 Load Overlap 的情况,无论地址偏移,都能成功转发。甚至在 Load 或 Store 跨越 64B 缓存行边界时,也可以成功转发,代价是多一个周期。
一个 Load 需要转发两个、四个甚至八个 Store 的数据时,如果数据跨越缓存行,则不能转发,但其他情况下,无论地址偏移,都可以转发,只是比从单个 Store 转发需要多耗费 1-4 个周期。
成功转发时 7.5 cycle,跨缓存行且转发失败时 28+ cycle。
小结:Apple Firestorm 的 Store to Load Forwarding:
在 Icestorm 上,如果 Load 和 Store 访问范围出现重叠,则需要 10 Cycle,无论是和几个 Store 重叠,也无论是否跨缓存行。
实测 Firestorm 的 Load to use latency 针对 pointer chasing 场景做了优化,在下列的场景下可以达到 3 cycle:
ldr x0, [x0]
: load 结果转发到基地址,无偏移ldr x0, [x0, 8]
:load 结果转发到基地址,有立即数偏移ldr x0, [x0, x1]
:load 结果转发到基地址,有寄存器偏移ldp x0, x1, [x0]
:load pair 的第一个目的寄存器转发到基地址,无偏移如果访存跨越了 8B 边界,则退化到 4 cycle。
在下列场景下 Load to use latency 则是 4 cycle:
ldr x0, [sp, x0, lsl #3]
:load 结果转发到 indexldp x1, x0, [x0]
:load pair 的第二个目的寄存器转发到基地址,无偏移实测 Icestorm 的 Load to use latency 针对 pointer chasing 场景做了优化,在下列的场景下可以达到 3 cycle:
ldr x0, [x0]
: load 结果转发到基地址,无偏移ldr x0, [x0, 8]
:load 结果转发到基地址,有立即数偏移ldr x0, [x0, x1]
:load 结果转发到基地址,有寄存器偏移ldp x0, x1, [x0]
:load pair 的第一个目的寄存器转发到基地址,无偏移如果访存跨越了 8B/16B/32B 边界,依然是 3 cycle;跨越了 64B 边界则退化到 7 cycle。
在下列场景下 Load to use latency 则是 4 cycle:
ldr x0, [sp, x0, lsl #3]
:load 结果转发到 indexldp x1, x0, [x0]
:load pair 的第二个目的寄存器转发到基地址,无偏移Linear Address UTag/Way-Predictor 是 AMD 的叫法,但使用相同的测试方法,也可以在 Apple M1 上观察到类似的现象,猜想它也用了类似的基于虚拟地址的 UTag/Way Predictor 方案,并测出来它的 UTag 也有 8 bit,Firestorm 和 Icestorm 都是相同的:
一共有 8 bit,由 VA[47:14] 折叠而来。
想要测试有多少个执行单元,每个执行单元可以运行哪些指令,首先要测试各类指令在无依赖情况下的的 IPC,通过 IPC 来推断有多少个能够执行这类指令的执行单元;但由于一个执行单元可能可以执行多类指令,于是进一步需要观察在混合不同类的指令时的 IPC,从而推断出完整的结果。
在 Firestorm 上测试如下各类指令的延迟和每周期吞吐:
指令 | 延迟 | 吞吐 |
---|---|---|
asimd int add | 2 | 4 |
asimd aesd/aese | 3 | 4 |
asimd aesimc/aesmc | 2 | 4 |
asimd fabs | 2 | 4 |
asimd fadd | 3 | 4 |
asimd fdiv 64b | 10 | 1 |
asimd fdiv 32b | 8 | 1 |
asimd fmax | 2 | 4 |
asimd fmin | 2 | 4 |
asimd fmla | 4 | 4 |
asimd fmul | 4 | 4 |
asimd fneg | 2 | 4 |
asimd frecpe | 3 | 1 |
asimd frsqrte | 3 | 1 |
asimd fsqrt 64b | 13 | 0.5 |
asimd fsqrt 32b | 10 | 0.5 |
fp cvtf2i (fcvtzs) | - | 2 |
fp cvti2f (scvtf) | - | 3 |
fp fabs | 2 | 4 |
fp fadd | 3 | 4 |
fp fdiv 64b | 10 | 1 |
fp fdiv 32b | 8 | 1 |
fp fjcvtzs | - | 1 |
fp fmax | 2 | 4 |
fp fmin | 2 | 4 |
fp fmov f2i | - | 2 |
fp fmov i2f | - | 3 |
fp fmul | 4 | 4 |
fp fneg | 2 | 4 |
fp frecpe | 3 | 1 |
fp frecpx | 3 | 1 |
fp frsqrte | 3 | 1 |
fp fsqrt 64b | 13 | 0.5 |
fp fsqrt 32b | 10 | 0.5 |
int add | 1 | 4.6 |
int addi | 1 | 6 |
int bfm | 1 | 1 |
int crc | 3 | 1 |
int csel | 1 | 3 |
int madd (addend) | 1 | 1 |
int madd (others) | 3 | 1 |
int mrs nzcv | - | 2 |
int mul | 3 | 2 |
int nop | - | 8 |
int sbfm | 1 | 4.7 |
int sdiv | 7 | 0.5 |
int smull | 3 | 2 |
int ubfm | 1 | 4.7 |
int udiv | 7 | 0.5 |
not taken branch | - | 2 |
taken branch | - | 1 |
mem asimd load | - | 3 |
mem asimd store | - | 2 |
mem int load | - | 3 |
mem int store | - | 2 |
从上面的结果可以初步得到的信息:
首先来看浮点和 ASIMD 单元,根据上面的信息,认为至少有 4 个执行单元,每个执行单元都可以做这些操作:asimd int add/aes/fabs/fadd/fmax/fmin/fmla/fmul/fneg,下面把这些指令称为 basic fp/asimd ops + aes。接下来要判断,fmov f2i/fmov i2f/fdiv/frecpe/frecpx/frsqrte/fsqrt 由哪些执行单元负责执行,方法是把这些指令混合起来测试吞吐(此处的吞吐不代表 CPI,而是每周能够执行多少次指令组合,例如用 2 条指令的组合测试,那么吞吐等于 CPI 除以 2):
指令 | 吞吐 |
---|---|
fp fdiv + fp frecpe | 0.5 |
fp fdiv + fp frecpx | 0.5 |
fp fdiv + fp frsqrte | 0.5 |
fp fdiv + fp fsqrt | 0.32=3/3.12 |
fp fdiv + fmov f2i | 1 |
fp fdiv + 2x fmov f2i | 0.67=1/1.50 |
fp fdiv + 3x fmov i2f | 1 |
fp fdiv + 4x fmov i2f | 0.75=1/1.33 |
fmov i2f + 4x fp fadd | 1 |
fmov f2i + 4x fp fadd | 0.67=1/1.50 |
根据以上测试结果,可以得到如下的推论:
推断这四个执行单元支持的操作:
当然还有很多指令没有测,不过原理是一样的。
访存部分,前面已经在测 LSU 的时候测过了,每周期 Load + Store 不超过 4 个,其中 Load 不超过 3 个,Store 不超过 2 个。虽然从 IPC 的角度来看 LSU 的 Load/Store Pipe 未必准确,比如可能它发射和提交的带宽是不同的,但先暂时简化为如下的执行单元:
最后是整数部分。从 addi 的指令来看,有 6 个 ALU,能够执行基本的整数指令(add/ubfm/sbfm 的吞吐有时候测出来 4.6-4.7,有时候测出来 6,怀疑是进入了什么低功耗模式)。但其他很多指令可能只有一部分执行单元可以执行:bfm/crc/csel/madd/mrs nzcv/mul/div/branch/fmov i2f。为了测试这些指令使用的执行单元是否重合,进行一系列的混合指令测试,吞吐的定义和上面相同:
指令 | 吞吐 |
---|---|
3x int csel + 3x fmov i2f | 1 |
3x int csel + 2x fmov f2i | 0.75=1/1.33 |
3x int csel + int bfm | 1 |
3x int csel + int crc | 1 |
3x int csel + int madd | 1 |
3x int csel + int mul | 1 |
3x int csel + int sdiv | 0.5 |
3x int csel + mrs nzcv | 0.75=1/1.33 |
3x int csel + not taken branch | 0.75=1/1.33 |
mrs nzcv + not taken branch | 1 |
mrs nzcv + 2x not taken branch | 0.67=1/1.50 |
2x fmov f2i + 2x not taken branch | 1 |
2x fmov f2i + 2x int mul | 1 |
int madd + 2x int mul | 0.67=1/1.50 |
int madd + int sdiv | 0.5 |
int madd + int crc | 0.5 |
根据上述结果分析:
得到初步的结果:
还有很多其他的指令没有测试,不过方法是类似的。从上面的结果里,可以看到一些值得一提的点:
小结:Firestorm 的执行单元如下:
接下来用类似的方法测试 Icestorm:
指令 | 延迟 | 吞吐 |
---|---|---|
asimd int add | 2 | 2 |
asimd aesd/aese | 3 | 2 |
asimd aesimc/aesmc | 2 | 2 |
asimd fabs | 2 | 2 |
asimd fadd | 3 | 2 |
asimd fdiv 64b | 11 | 0.5 |
asimd fdiv 32b | 9 | 0.5 |
asimd fmax | 2 | 2 |
asimd fmin | 2 | 2 |
asimd fmla | 4 | 2 |
asimd fmul | 4 | 2 |
asimd fneg | 2 | 2 |
asimd frecpe | 4 | 0.5 |
asimd frsqrte | 4 | 0.5 |
asimd fsqrt 64b | 15 | 0.5 |
asimd fsqrt 32b | 12 | 0.5 |
fp cvtf2i (fcvtzs) | - | 1 |
fp cvti2f (scvtf) | - | 2 |
fp fabs | 2 | 2 |
fp fadd | 3 | 2 |
fp fdiv 64b | 10 | 1 |
fp fdiv 32b | 8 | 1 |
fp fjcvtzs | - | 0.5 |
fp fmax | 2 | 2 |
fp fmin | 2 | 2 |
fp fmov f2i | - | 1 |
fp fmov i2f | - | 2 |
fp fmul | 4 | 2 |
fp fneg | 2 | 2 |
fp frecpe | 3 | 1 |
fp frecpx | 3 | 1 |
fp frsqrte | 3 | 1 |
fp fsqrt 64b | 13 | 0.5 |
fp fsqrt 32b | 10 | 0.5 |
int add | 1 | 3 |
int addi | 1 | 3 |
int bfm | 1 | 1 |
int crc | 3 | 1 |
int csel | 1 | 3 |
int madd (addend) | 1 | 1 |
int madd (others) | 3 | 1 |
int mrs nzcv | - | 3 |
int mul | 3 | 1 |
int nop | - | 4 |
int sbfm | 1 | 3 |
int sdiv | 7 | 0.125=1/8 |
int smull | 3 | 1 |
int ubfm | 1 | 3 |
int udiv | 7 | 0.125=1/8 |
not taken branch | - | 2 |
taken branch | - | 1 |
mem asimd load | - | 2 |
mem asimd store | - | 1 |
mem int load | - | 2 |
mem int store | - | 1 |
从上面的结果可以初步得到的信息:
还是先看浮点,基本指令 add/aes/fabs/fadd/fmax/fmin/fmla/fmul/fneg 都能做到 2 的吞吐,也就是这两个指定单元都能执行这些基本指令。接下来测其余指令的混合吞吐(吞吐定义见上):
指令 | 吞吐 |
---|---|
fp fdiv + fp frecpe | 0.5 |
fp fdiv + fp frecpx | 0.5 |
fp fdiv + fp frsqrte | 0.5 |
fp fdiv + fp fsqrt | 0.31=3/3.25 |
fp fdiv + fmov f2i | 0.5 |
fp fdiv + 2x fmov i2f | 1 |
fp fdiv + 3x fmov i2f | 0.67/1/1.50 |
可见 fdiv/frecpe/frecpx/frsqrte/fsqrt/fmov f2i 都在同一个执行单元内:
还有很多指令没有测,不过原理是一样的。访存在前面测 LSU 的时候已经测过了:
最后是整数部分。从 addi 的指令来看,有 3 个 ALU,能够执行基本的整数指令。但其他很多指令可能只有一部分执行单元可以执行:bfm/crc/csel/madd/mul/div/branch。为了测试这些指令使用的执行单元是否重合,进行一系列的混合指令测试,吞吐的定义和上面相同:
指令 | 吞吐 |
---|---|
int madd + int mul | 0.5 |
int madd + int crc | 0.5 |
int madd + 2x not taken branch | 1 |
由此可见,madd/mul/crc 是一个执行单元,和 branch 的两个执行单元不重合,因此整数侧的执行单元有:
小结:Icestorm 的执行单元如下:
为了测试 Scheduler 的大小和组织方式(分布式还是集中式),测试方法是:首先用长延迟的操作堵住 ROB,接着用若干条依赖长延迟操作的指令堵住 Scheduler,当指令塞不进去的时候,就说明 Scheduler 满了。更进一步,由于现在很多处理器会引入 Non Scheduling Queue,里面的指令不会直接调度进执行单元,也不检查它依赖的操作数是否已经准备好,此时为了区分可调度部分和不可调度部分,在依赖长延迟操作的指令后面,添加若干条不依赖长延迟操作的指令,这样测出来的就是可调度部分的深度。
在 Firestorm 上测试,结果如下:
指令 | 可调度 + 不可调度 | 可调度 |
---|---|---|
ld | 58 | 48 |
st | 58 | 43 |
alu | 158 | 134 |
fp | 156 | 144 |
crc | 40 | 28 |
idiv | 40 | 28 |
bfm | 40 | 28 |
fjcvtzs | 42 | 36 |
fmov f2i | 84 | 72 |
csel | 76 | 64 |
mrs nzcv | 62 | 50 |
首先看浮点:
下面是访存部分,load 和 store 总数一样但 Scheduler 差了 5,不确定是测试误差还是什么问题,暂且考虑为一个统一的 Scheduler 和同一个 Non Scheduling Queue。
最后是整数部分,由于有 6 个整数执行单元,情况会比较复杂:
Firestorm 的 ROB 采用的是比较特别的方案,它的 entry 地位不是等同的,而是若干个 entry 组合在一起,成为一个 group,每个 group 有若干个 entry,一个 group 对 group 内指令的类型和数量有要求,这就导致用传统方法测 Firestorm 的 ROB,可能会测到特别巨大的数:2200+,也可能测到比较小的数:320+,这就和指令类型有关系。为什么对指令的类型和位置有要求呢,这是为了方便处理有副作用的指令。很多指令是没有副作用的,也不会抛异常,这些指令可以比较随意地放置;但是对于有副作用的指令,retire 时是需要特殊处理的,因此一个合理的设计就是,让这些指令只能放在 group 的开头。
经过测试,发现 Firestorm 上 pointer chasing 的延迟波动比较大,目测是 prefetcher 做了针对性的优化,因此用 fsqrt chain 来做延迟,Firestorm 上一条双精度 fsqrt 的延迟是 13 个周期。构造一个循环,循环包括 M 个串行的 sqrt 和 N 个 nop,如果没有触碰到 ROB 的瓶颈,那么当 N 比较小的时候,瓶颈在串行 fsqrt 上,每次循环的周期数应该为 M*13
;当 N 比较大的时候,瓶颈在每个周期执行 8 个 NOP 上(NOP 被 eliminate 了,不用进 ALU,可以打满 8 的发射宽度),每次循环的周期数应该为 N/8
再加上一个常数。
但当 N 很大的时候,可能会撞上 ROB 大小的限制。下面给出了不同的 M 取值情况下,可以保证循环周期数在 M*13
左右的最大的 N 取值:
可以看到 N 最大在 2277 附近就不能再增加了,说明遇到了 ROB 的瓶颈,预计 ROB 的所有 group 的 entry 个数加起来大概是 2277 左右。
而如果把填充的指令改成 load/store,inflight 的 load 和 store 最多都是 325 个,并且这和 load/store queue 大小无关,而 load/store 又是有副作用的,很可能是因为它们只能在 ROB 每个 group 里只能放一条,于是看起来 ROB 的容量比 2277 小了很多,只表现出 325。按照这个猜想,对二者进行除法,发现商和 7 十分接近,这大概率意味着 Firestorm 的 ROB 有 325 左右个 group,每个 group 内有 7 个 entry,每个 entry 可以放一条指令(uop)。测试里开头的 20 个 sqrt 也要占用 ROB,实际的 ROB group 数量可能比 325 略多。
结论:Firestorm 的 ROB 有大约 330 个 group,每个 group 最多保存 7 个 uop。
官方信息:通过 sysctl 可以看到,4 个 Firestorm 核心共享一个 12MB L2 Cache,4 个 Icestorm 核心共享一个 4MB L2 Cache:
hw.perflevel0.l2cachesize: 12582912hw.perflevel0.cpusperl2: 4hw.perflevel1.l2cachesize: 4194304hw.perflevel1.cpusperl2: 4
从苹果提供的性能计数器来看,L1 TLB 分为指令和数据,而 L2 TLB 是 Unified,不区分指令和数据。沿用之前测试 L1 DTLB 的方法,把规模扩大到 L2 Unified TLB 的范围,就可以测出来 L2 Unified TLB 的容量,下面是 Firestorm 上的测试结果:
可以看到拐点是 3072 个 Page,说明 Firestorm 的 L2 TLB 容量是 3072 项。
把指针的跨度增大:
认为 Firestorm 的 L2 TLB 是 12 Way,256 Set,Index 位是 VA[21:14]。
在 Icestorm 上测试:
可以看到拐点是 1024 个 Page,说明 Icestorm 的 L2 TLB 容量是 1024 项。
把指针的跨度增大:
由于 Icestorm 的 L1 DTLB 就是 4 Way,不确定 Icestorm 的 L2 TLB 组相连是 1/2/4 Way 的哪一种,假如是 4 Way,那么 Icestorm 的 L2 TLB 是 4 Way,256 Set,Index 位是 VA[21:14]。
2024-12-10 08:00:00
最近使用 Linux 的性能分析功能比较多,但是很少去探究背后的原理,例如硬件的 PMU 是怎么配置的,每个进程乃至每个线程级别的 PMU 是怎么采样的。这篇博客尝试探究这背后的原理。
支撑性能分析的背后是硬件提供的机制,最常用的就是性能计数器:硬件会提供一些可以配置的性能计数器,在对应的硬件事件触发是,更新这些计数器,然后再由程序读取计数器的值并统计。下面以 ARM 为例,分析一下硬件提供的性能计数的接口:
注:实际上,由于经常会对指令数进行采样,ARM v9.4/8.9 允许硬件实现一个额外的指令计数器,和 Cycle 计数器类似。
如果想要在用户态频繁地读取性能计数器(cap_user_rdpmc),避免频繁进入内核的开销,也可以在用户态中直接读取性能计数器 PMCCNTR_EL0/PMEVCNTR
LoongArch 也是类似的,其接口更简单:它只有通用性能计数器,有如下的 csr 来配置各个性能计数器:
在 Linux 内核中,负责控制 ARM 性能计数接口的代码在 arm_pmuv3.c 当中。根据这个硬件接口,可以预想到,如果要对一段程序观察它在某个计数器上的取值,需要:
除了由单独的架构相关的内核驱动负责配置硬件以外,还需要由 Perf 子系统来处理来自用户的 perf 使用。具体地,内核驱动会注册一个 struct pmu
给 Perf 子系统,实现这些函数:
// Fully disable/enable this PMUvoid (*pmu_enable) (struct pmu *pmu); /* optional */void (*pmu_disable) (struct pmu *pmu); /* optional */// Try and initialize the event for this PMU.int (*event_init) (struct perf_event *event);// Adds/Removes a counter to/from the PMUint (*add) (struct perf_event *event, int flags);void (*del) (struct perf_event *event, int flags);// Starts/Stops a counter present on the PMU.void (*start) (struct perf_event *event, int flags);void (*stop) (struct perf_event *event, int flags);// Updates the counter value of the event.void (*read) (struct perf_event *event);
可见在内核里,PMU 计数器的抽象是 struct perf_event
,这个是架构无关的,根据用户态程序通过 perf_event_open
构造出来的;内核驱动就会根据这个 struct perf_event
去进行实际的硬件计数器的配置。例如用户程序在 struct perf_event_attr
里设置 exclude_kernel = 1
,就会传到 struct perf_event
当中,最后在相应的内核驱动中,变成硬件性能计数器配置里,计数时忽略内核所在特权态的配置。
perf record
是基于采样实现的:当性能计数器溢出(一般是 Cycle 计数器)的时候,触发中断进入内核,此时由软件来收集被打断的程序的上下文信息,收集完成后再回到被打断的程序继续执行。
在虚拟化场景下,依然希望虚拟机内的 OS 可以有性能计数器可以用,同时宿主机上也可能希望获取虚拟机的性能计数器信息。
一种方法是以纯软件的方法去实现性能计数器,比如 QEMU 的 TCG 模式,可以模拟出一个以固定频率运行的处理器的 Cycle 计数器,但实际上就是拿时间除以频率,是假的性能计数器;此外还能模拟出 Instruction 计数器,因为 QEMU 在做指令翻译的时候,可以顺带记录下执行的指令数;而微架构相关的性能计数器就没法靠这个来实现了。
另一种方法则是在硬件虚拟化的基础上,让虚拟机享受到性能计数器。不过为了安全性,宿主机可以获取虚拟机的性能计数器,但反过来,虚拟机的性能计数器不应该得到宿主机的信息。
目前 LoongArch KVM 已经支持性能计数器的虚拟化,下面来看它是怎么做的:
Intel PT 是 Intel 平台上跟踪指令流的机制,它可以记录这些信息:
perf 工具也是支持用 Intel PT 进行跟踪的:perf-intel-pt(1) — Linux manual page,下面的命令用 Intel PT 跟踪一条命令的执行过程,并显示出它生成的跟踪信息:
perf record -e intel_pt//u ls# itrace: instruction trace# i: instructions events# y: cycles events# b: branches events# x: transactions events# w: ptwrite events# p: power events# e: error eventsperf script --itrace=iybxwpe
比如写一个循环 10 次的代码:
对应的汇编:
0000000000001129 <main>: 1129: 55 push %rbp 112a: 48 89 e5 mov %rsp,%rbp 112d: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 1134: eb 04 jmp 113a <main+0x11> 1136: 83 45 fc 01 addl $0x1,-0x4(%rbp) 113a: 83 7d fc 09 cmpl $0x9,-0x4(%rbp) 113e: 7e f6 jle 1136 <main+0xd> 1140: b8 00 00 00 00 mov $0x0,%eax 1145: 5d pop %rbp 1146: c3 ret
按照上述方法,可以看到它生成了各个分支跳转的信息:
test1 3772291 [006] 1693209.504363: 1 branches:u: 7311943d6248 __libc_start_call_main+0x78 (/usr/lib/x86_64-linux-gnu/libc.so.6) => 5b4f1df51129 main+0x0 (/home/jiegec/test1)test1 3772291 [006] 1693209.504363: 1 branches:u: 5b4f1df51134 main+0xb (/home/jiegec/test1) => 5b4f1df5113a main+0x11 (/home/jiegec/test1)test1 3772291 [006] 1693209.504363: 1 branches:u: 5b4f1df5113e main+0x15 (/home/jiegec/test1) => 5b4f1df51136 main+0xd (/home/jiegec/test1)test1 3772291 [006] 1693209.504363: 1 branches:u: 5b4f1df5113e main+0x15 (/home/jiegec/test1) => 5b4f1df51136 main+0xd (/home/jiegec/test1)test1 3772291 [006] 1693209.504363: 1 branches:u: 5b4f1df5113e main+0x15 (/home/jiegec/test1) => 5b4f1df51136 main+0xd (/home/jiegec/test1)test1 3772291 [006] 1693209.504363: 1 branches:u: 5b4f1df5113e main+0x15 (/home/jiegec/test1) => 5b4f1df51136 main+0xd (/home/jiegec/test1)test1 3772291 [006] 1693209.504363: 1 branches:u: 5b4f1df5113e main+0x15 (/home/jiegec/test1) => 5b4f1df51136 main+0xd (/home/jiegec/test1)test1 3772291 [006] 1693209.504363: 1 branches:u: 5b4f1df5113e main+0x15 (/home/jiegec/test1) => 5b4f1df51136 main+0xd (/home/jiegec/test1)test1 3772291 [006] 1693209.504363: 1 branches:u: 5b4f1df5113e main+0x15 (/home/jiegec/test1) => 5b4f1df51136 main+0xd (/home/jiegec/test1)test1 3772291 [006] 1693209.504363: 1 branches:u: 5b4f1df5113e main+0x15 (/home/jiegec/test1) => 5b4f1df51136 main+0xd (/home/jiegec/test1)test1 3772291 [006] 1693209.504363: 1 branches:u: 5b4f1df5113e main+0x15 (/home/jiegec/test1) => 5b4f1df51136 main+0xd (/home/jiegec/test1)test1 3772291 [006] 1693209.504363: 1 branches:u: 5b4f1df5113e main+0x15 (/home/jiegec/test1) => 5b4f1df51136 main+0xd (/home/jiegec/test1)test1 3772291 [006] 1693209.504363: 1 branches:u: 5b4f1df51146 main+0x1d (/home/jiegec/test1) => 7311943d624a __libc_start_call_main+0x7a (/usr/lib/x86_64-linux-gnu/libc.so.6)
通过这个信息,就可以还原出程序执行了哪些代码:
如果要看 Intel PT 到底生成了什么数据,可以用 perf script -D
显示。例如要记录分支跳转还是不跳转的历史,用的是如下的 TNT(Taken/Not Taken) packet:
TNT packet 的定义在 Intel® 64 and IA-32 Architectures Software Developer Manuals 中给出:
由于 Intel PT 的数据量很大,它和 SPE 类似,也是在内存中保存 trace 信息。
Intel LBR(Last Branch Record) 机制记录了处理器最近若干次控制流转移,比如 taken branch。它记录的数量很小,信息直接保存在 MSR 当中,而不是像前面的 SPE 和 Intel PT 那样,需要在内存中记录信息。
LBR 在 perf 中,主要用来跟踪 call stack:设置 LBR,只记录 call 指令,并且打开 call-stack 模式,那么 LBR 记录的就是当前的 call stack,perf 可以利用这个信息来找到当前函数的调用链,虽然有长度限制。除了 lbr 以外,perf 还支持利用 fp(frame pointer) 或 dwarf(调试信息) 来寻找调用链:
--call-graph Setup and enable call-graph (stack chain/backtrace) recording, implies -g. Default is "fp" (for user space). Valid options are "fp" (frame pointer), "dwarf" (DWARF's CFI - Call Frame Information) or "lbr" (Hardware Last Branch Record facility).
和 Intel PT 相比,它记录的信息较少,但实现上也更简单,开销更小。而且 LBR 只会记录跳转的分支,不会记录没跳转的分支,此外就是记录的分支数有上限。
较早的 Intel 处理器没有实现 Intel PT,但考虑到 LBR 记录的历史长度限制,基于 LBR 做了一个把 LBR 信息保存到内存里的技术,叫做 Branch Trace Store。perf 也支持基于 BTS 来记录分支历史:
# recordsudo perf record --per-thread -e intel_bts//u ls# display tracesudo perf script# dump raw datasudo perf script -D
BTS 每个 entry 占用 24 字节,包括 8 字节的 Last Branch From 和 8 字节的 Last Branch To,还有 8 字节记录了分支预测正确还是错误(图源 Intel® 64 and IA-32 Architectures Software Developer Manuals):
和 LBR 一样,BTS 只记录跳转的分支,不记录没跳转的分支。
Intel PEBS(Processor Event Based Sampling) 是一种硬件的采样方法,顾名思义,当处理器触发某些事件时,自动进行一次采样。这个事件,实际上就是某个性能计数器溢出。本来,性能计数器溢出的时候,应该触发中断,让内核维护性能计数器的真实值(例如硬件实现了 32 位计数器,但内核维护的是 64 位);但在 PEBS 中,这个性能计数器的溢出事件被用来触发硬件的采样:计数溢出的时候,会捕捉当前的处理器状态(PC、访存地址和延迟、通用寄存器和浮点寄存器的值、时钟周期计数和 LBR 信息),把状态写入到内存中的缓冲区,并自动把性能计数器设为指定的复位值。当内存中的缓冲区满的时候,才会触发中断,让内核来处理 PEBS 生成的数据,并分配新的空间。
PEBS 可以精细地根据性能计数器来决定采样的频率,例如每 1000 条指令采样一次,每 1000 个周期采样一次,甚至每 1000 次缓存缺失采样一次。具体做法是,把对应的性能计数器的复位值设置为最大值减 1000,那么每次溢出触发 PEBS 采样以后,性能计数器会被设置为最大值减 1000,等性能计数器增加 1000 以后,再次溢出,触发 PEBS 采样,如此循环。
AMD 也有类似的机制,叫做 IBS(Instruction Based Sampling)。IBS 没有和 PMU 绑定起来,而是数指令数或数周期。推荐阅读论文 Precise Event Sampling on AMD Versus Intel: Quantitative and Qualitative Comparison,它深入比较了 AMD IBS 和 Intel PEBS 的差异。
如果要启用 PEBS 或 IBS,在 perf record
指令事件时,追加 :p
:
The p modifier can be used for specifying how precise the instruction address should be. The p modifier can be specifiedmultiple times: 0 - SAMPLE_IP can have arbitrary skid 1 - SAMPLE_IP must have constant skid 2 - SAMPLE_IP requested to have 0 skid 3 - SAMPLE_IP must have 0 skid, or uses randomization to avoid sample shadowing effects.For Intel systems precise event sampling is implemented with PEBS which supports up to precise-level 2, and precise level 3 for some special cases.On AMD systems it is implemented using IBS OP (up to precise-level 2).
详细信息见 perf-list(1) — Linux manual page。
ARM 平台定义了 BRBE(Branch Record Buffer Extension),它和 Intel LBR 类似,也是在 System Register 中记录最近若干条跳转的分支的信息。它会记录 taken branch 的这些信息:
不跳转的条件分支指令就不记录。和 LBR 类似,它也支持根据分支类型过滤,因此也可以用于 call graph 的抓取。
ARM 平台定义了 SPE(Statistical Profiling Extension),它的做法是基于采样的:硬件上每过一段时间,采样一个操作,比如正在执行的指令;采样得到的操作的详细信息会写入到内存当中,由内核驱动准备好的一段空间。空间满的时候,会发中断通知内核并让内核重新分配空间。
perf record
在默认参数下的工作原理和 SPE 有点像,都是定时打断程序并采样:但 perf record
是在程序被打断后,由软件来收集信息;而 SPE 是由硬件采样,可以提供更多的微架构信息(延迟,访存地址,是否命中 TLB,分支跳转与否等等)。
SPE 的内核驱动实现在 arm_spe_pmu.c 当中;它做的事情是,在内存中分配好缓冲区,启动 SPE,并且在 SPE 触发中断时,进行缓冲区的维护;同时缓冲区中的数据会通过 perf ring buffer (aka perf aux) 传递给用户态的程序,具体数据的解析是由用户态的程序完成的。如果用 perf 工具,那么这个解析和展示的工作就是由 perf 完成的。
SPE 和 AMD IBS 类似,也是数指令数;和 Intel PEBS 不同,它没有和性能计数器耦合起来。
ARM 除了提供性能计数单元(PMU)以外,还提供了 AMU(Activity Monitor Unit)。它和 PMU 很类似,也是有一些性能计数器,但 AMU 在计数器溢出的时候不会触发中断,所以它并不是拿来观察某个程序的性能怎么样,而是观察系统整体的状态,比如时钟频率,IPC 等等。
目前 AMU 的主要用途是在内核的调度器上:调度器需要估计一段时间内做了多少单位的任务,需要知道 CPU 的频率,比如频率高,就认为它做了更多的任务。之前的做法是让调度器通过 cpufreq 读取当前的 CPU 频率,但由于 CPU 的频率会不断变化,这样读取到的频率是一个瞬时功率,不能代表一段时间的平均值,就会带来误差。
为了获取一段时间内的平均频率,amu_scale_freq_tick 函数就使用了 AMU 的两个计数器:
通过记录一段时间内两个计数器的变化量,将两个变化量做除法,就可以得到一个正比于这段时间内 CPU 的平均频率的值,交给调度器。
2024-11-11 08:00:00
Zen 5 是 AMD 最新的一代微架构,在很多地方和之前不同,因此测试一下这个微架构在各个方面的表现。
AMD 一向公开得比较大方,关于 Zen 5 的信息有:
网上已经有较多针对 Zen 5 微架构的评测和分析,建议阅读:
下面分各个模块分别记录官方提供的信息,以及实测的结果。读者可以对照已有的第三方评测理解。官方信息与实测结果一致的数据会加粗。
AMD Zen 5 的性能测试结果见 SPEC。
MOP = Macro operation, uOP = Micro operation
AMD 的文档里是这么说的:
The processor implements AMD64 instruction set by means of macro-ops (the primary units ofwork managed by the processor) and micro-ops (the primitive operations executed in theprocessor's execution units).Instructions are marked as fast path single (one macro-op), fast path double (two macro-ops), ormicrocode (greater than two macro-ops). Macro ops can normally contain up to two micro-ops.
一条指令可以分成若干个 MOP(比如 REP MOVS 会拆成很多个 MOP),一个 MOP 可以继续细分为 uOP(比如 store 拆分成 store data 和 store address;把内存的值加到寄存器上的 add 指令拆分成 load 和 add)。Dispatch 的单位是 MOP,ROB 保存的也是 MOP。与 Zen3/Zen4 不同,Op Cache 保存的不是 MOP,而是 Fused Instructions,这个 Fusion 来自于 Branch Fusion 或 MOV + ALU Fusion。Fusion 相当于把多条指令合成了一个,减少了 MOP 的数量。
MOP 到 uOP 的拆分需要等到 Scheduler 中才进行,Scheduler 输入 MOP,输出 uOP,也就是说最终给到执行单元的是 uOP。
和 ARM 公版核的 MOP/uOP 对比,其实是很类似的:uOP 是执行单元看到的指令粒度,MOP 是维护精确异常的指令粒度。
官方信息:64 set, 16 way, 1024 entry, 6 (fused) inst/entry, 供指 2 entry/cycle
AMD 在 UEFI 固件中提供了关闭 Op Cache 的设置,因此我们可以测试在 Op Cache 开启/关闭不同情况下的性能。通过进一步研究,发现固件的 Op Cache 关闭设置,实际上对应了 MSR[0xc0011021] 的 bit 5:初始情况下,MSR[0xc0011021] 的值为 0x20000000000040,如果进入固件关闭 Op Cache,可以观察到 MSR[0xc0011021] 变成了 0x20000000000060。实际上,Op Cache 可以在进入 Linux 后动态开启/关闭(感谢 David Huang 在博客中提供的信息):
sudo modprobe msr# Disable Op Cache for Core 0sudo wrmsr -p 0 0xc0011021 0x20000000000060# Enable Op Cache for Core 0sudo wrmsr -p 0 0xc0011021 0x20000000000040
因此开关 Op Cache 不需要重启进固件了。
Zen 5 的 Op Cache 每个 entry 是 6 (fused) inst,为了测出 Op Cache 的容量,以及确认保存的是 fused inst,利用 MOV + ALU Fusion 来构造指令序列:
这两条指令满足 Zen 5 的 MOV + ALU Fusion 要求,硬件上融合成一个 rsi = rdx + rdi
的操作。做这个融合也是因为 x86 指令集缺少 3 地址指令,当然未来 APX 会补上这个缺失。实测发现,这样的指令序列可以达到 12 的 IPC,正好 Zen 5 的 ALU 有 6 个,也就是每周期执行 6 条融合后的指令,和 12 IPC 是吻合的。12 的 IPC 可以一直维持到 36KB 的 footprint,这里的 mov 和 add 指令都是 3 字节,换算下来 36KB 对应 36*1024/6=6144
个 fused instruction,正好 64*16*6=6144
,对上了。关掉 Op Cache 后,性能下降到 4 IPC,对应了 Decode 宽度,同时也说明 Decode 的 4 Wide 对应的是指令,而不是融合后的指令。
接下来要测试 Op Cache 能否单周期给单个线程提供 2 个 entry 的吞吐。由于每个 entry 最多可以有 6 (fused) inst,加起来是 12,而 dispatch 只有 8 MOP/cycle,因此退而求其次,不要求用完 entry 的 6 条指令,而是用 jmp 指令来提前结束 entry:
重复上述指令,发现在 5KB 之前都可以达到 4 的 IPC,之后则下降到 2 IPC,说明 5KB 时用满了 Op Cache。这里的 mov 指令是 3 字节,jmp 指令是 2 字节,也就是说 5KB 对应上述指令模式重复了 1024 次,此时 Op Cache 用满了容量,正好 Op Cache 也是 64*16=1024
个 entry,印证了 Op Cache 的 entry 会被 jmp 提前结束,在上述的指令模式下,entry 不会跨越 jmp 指令记录后面的指令,每个 entry 只有两条指令。那么 4 IPC 证明了 Op Cache 可以每周期提供 2 entry,相比 Decode 只能每周期给单线程提供 4 条指令明显要快。
官方信息:每周期共 64B,可以取两个 32B 对齐的指令块
为了测试取指,需要关掉 Op Cache,但由于 Decode 瓶颈太明显,不容易测出取指的性能,例如是否一个周期可以给单线程取两个 32B 对齐的指令块。目前通过实测可以知道,在关闭 Op Cache 的情况下,测试循环体跨越 64B 缓存行边界的情况,指令模式见下:
循环一次需要 1.5 个周期。如果 Fetch 每周期只能取一个 32B/64B 对齐的指令块,那么一次循环需要 2 个周期来取指,但如果 Fetch 每周期可以取两个 32B 对齐的指令块,那么一次循环只需要 1 个周期取指,但实际测出来又是 1.5 个周期,目前还没有找到合理的解释,但大概率 Fetch 还是可以给单线程每周期提供两个 32B 指令块。
官方信息:2x 4-wide decode pipeline, one pipeline per thread
AMD Zen 5 的 Decode 虽然有两个 Pipe,但是每个逻辑线程只能用一个,意味着单线程情况下,无法做到 8-wide Decode,而 4-wide Decode 又太窄了点,因此 Op Cache 的命中率就显得很重要。
为了测试 Decode,需要首先按照上面的方法关闭 Op Cache,然后构造不同的指令序列以观察 IPC,得到的结果如下:
上述 nop 的编码取自 Software Optimization Guide 的 Encodings for NOP Instructions 1 to 15 表格。
首先可以看到 Zen5 4-wide Decode 的限制,其次可以发现重复 5-10 字节的 nop,每周期的 Decode 吞吐都是 16B。11 字节以上则是撞到了 Decode 的限制:Only the first decode slot (of four) can decode instructions greater than 10 bytes in length
。
比较有意思的是这个 16B 的限制,考虑移动窗口的译码设计,每周期可以对两个连续 16B 的窗口译码(IBQ entries hold 16 byte-aligned fetch windows of the instruction byte stream. The decode pipes each scan two IBQ entries.
),在 5 字节的 nop 模式下,每个周期的 Decode 应该是:
按这个理想的方法来看,应该可以做到 4 的 IPC,但实际上没有。一个猜测是,滑动窗口每次只能移动 1 个 16B,而不能从 48 跳到 80,那么从 Cycle 4 开始会出现性能损失:
这个规律延续下去,平均下来就是 3.2 IPC。
根据这个猜想,Decode 从两个连续的 IBQ entry 译码最多四条指令,是没有 16B 的限制的,但 IBQ 在一些情况下,每周期只能弹出一个 entry,而不能每周期弹出两个,这才导致了 16B 的吞吐。总之,4-wide 以及 16B 的限制,应该说是很小的。
官方信息:32KB, 8-way set associative
为了测试 L1 ICache 的容量,需要关闭 Op Cache,但由于 Decode 的限制,即使 footprint 大于 L1 ICache 容量,IPC 依然没有变化,针对这个现象,猜测 L1 ICache 的预取在起作用,并且 L2 Cache 到 L1 ICache 的 Refill 带宽不小于 Decode 带宽,导致瓶颈在 Decode。
因此,为了测试 L1 ICache 的容量,构造一个 jmp 序列,以 4B 位间距排布,观察到在关闭 Op Cache 的情况下,在 8192 条 jmp 指令之前可以做到 1 CPI,之后逐渐提升到 1.5 CPI,正好 8192 对应了 8192*4=32768
也就是 32KB L1 ICache 的容量限制。
官方信息:64-entry, fully associative
为了测试 L1 ITLB 的容量,构造 jmp 序列,每个 jmp 在一个单独的页中,在关闭 Op Cache 的情况下观察 jmp 的性能:
可以看到明显的 64 pages 的拐点,对应了 64 entry 的 L1 ITLB。
官方信息:2048-entry, 8-way set associative L2 ITLB
继续沿用测试 L1 ITLB 的方式,把页的数量提高到 2000+,在关闭 Op Cache 的情况下得到以下测试结果:
可以看到明显的 2048 pages 的拐点,对应了 2048 entry 的 L2 ITLB。
官方信息:16K-entry L1 BTB, 8K-entry L2 BTB
因为 L1 ICache 只有 32KB,而 L1 BTB 有 16K entry,每个 entry 最多能保存两条分支指令,因此多数情况下,首先遇到的是 L1 ICache 的瓶颈,而不是 L1 BTB 的瓶颈。
官方信息:52-entry per thread
构造不同深度的调用链,测试每次调用花费的时间,在关闭 Op Cache 的情况下得到如下测试结果:
可以看到 52 的拐点,对应的就是 Return Address Stack 的大小。
官方信息:3072-entry Indirect Target Array
官方信息:支持 xor/sub/cmp/sbb/vxorp/vandnp/vpcmpgt/vpandn/vpxor/vpsub 的 Zeroing Idiom,支持 pcmpeq/vpcmpeq 的 Ones Idiom,支持 mov/movsxd/xchg/(v)vmovap/(v)movdp/(v)movup 的 Zero Cycle Move。
实测下来,以下指令序列的 IPC 为:
其中 r 表示整数寄存器,vr 表示浮点/向量寄存器。总体来说还是做的比较完善的。
官方信息:8 MOP/cycle, up to 2 taken branches/cycle
官方信息:224-entry per thread, 1-2 MOP per entry
把两个独立的 long latency pointer chasing load 放在循环的头和尾,中间用 NOP 填充,当 NOP 填满了 ROB,第二个 pointer chasing load 无法提前执行,导致性能下降。测试结果如下:
当 NOP 指令达到 446 条时出现性能突变,此时应该是触发了 Zen 5 的每个 entry 保存两个 MOP 的条件,因此 446 条 NOP 指令对应 223 个 entry,加上循环开头的 load 指令,正好把循环尾部的 load 拦在了 ROB 外面,导致性能下降。
说明单线程可以访问到的 ROB 容量是 224 entry。
官方信息:240-entry(40 per thread for architectural) integer physical register file, 192-entry flag physical register file, 384-entry 512b vector register file
为了测试物理寄存器堆大小,构造一个循环,循环开头和结尾各是一个长延迟的操作,由于 Zen 5 没有实现 temporal prefetcher,使用的是 pointer chasing load。然后在两个长延迟的操作中间穿插不同的指令类型,从而测出对应的物理寄存器堆可供预测执行的寄存器数量:
整数方面使用 lea 指令来消耗整数物理寄存器而不消耗 flags 寄存器,此时无论是 32 位还是 64 位寄存器,供预测执行的寄存器数都有 200 个,和官方的信息吻合:200+40=240
,说明超线程在没有负载的时候,不会占用整数物理寄存器堆,这在 AMD 的文档中叫做 Watermarked:Resource entries are assigned on demand
。356 个 flags 寄存器超过了官方宣传的 192 的大小,猜测做了一些优化,测到的并非 flags 寄存器堆大小。
浮点方面,测得 430 个供预测执行的浮点寄存器,超过了官方宣传的 384 个 512 位浮点寄存器。考虑到 Zen5 引入了在 Rename 之前的 96-entry Non-Scheduling Queue(NSQ),在 NSQ 中的指令还没有经过重命名,因此不消耗物理寄存器:384+96=480
,再去掉至少 32 个架构寄存器 zmm0-zmm31,和观察到的 430 是比较接近的。
针对浮点寄存器,Zen5 的不同平台的设计不完全一样,上面的测试是在 9950X 上进行的,其他平台的测试以及分析见 Zen5's AVX512 Teardown + More...。
官方信息:48KB, 12-way set associative, index 是 VA[11:6]
使用不同 footprint 的随机的 pointer chasing load,测试性能,得到如下结果:
可以观察到明显的 48KB 的拐点,命中 L1 DCache 时 load to use latency 是 4 cycle,命中 L2 时增大到了 14 cycle。
复现论文 Take A Way: Exploring the Security Implications of AMD's Cache Way Predictors,可以看到 Zen 5 的 UTAG 哈希函数和 Zen 2 一样也是如下 8 bit:
如果两个虚拟地址映射到同一个 DCache Set 上的不同 Way(Set 根据 VA[11:6] 唯一确定),并且它们的 uTag 出现冲突,那么访问一个虚拟地址会把另一个虚拟地址从 L1 DCache 中清掉。
官方信息:每周期最多四个内存操作。每周期最多四个读,其中最多两个 128b/256b/512b 读;每周期最多两个写,其中最多一个 512b 写。load to use latency,整数是 4-5 个周期,浮点是 7-8 个周期。跨越 64B 边界的读会有额外的一个周期的延迟。支持 Store to load forwarding,要求先前的 store 包括了 load 的所有字节,不要求对齐。
实测 Zen 5 每个周期可以完成如下的访存操作:
简单来说,每周期支持 4 个 64b 的 Load/Store,其中 Store 最多两条。一个 128b 的 Load 相当于两个 64b,对应 IPC 减半。
构造串行的 load 链,观察到多数情况下 load to use latency 是 4 个周期,在跨越 64B 边界时,会增加一个周期变成 5 个周期。此外,如果涉及到 index 计算(即 offset(base, index, shift)
),也会增加一个周期。
经过实际测试,如下的情况可以成功转发:
对地址 x 的 Store 转发到对地址 y 的 Load 成功时 y-x 的取值范围:
Store\Load | 8b Load | 16b Load | 32b Load | 64b Load |
---|---|---|---|---|
8b Store | {0} | {} | {} | {} |
16b Store | [0,1] | {0} | {} | {} |
32b Store | [0,3] | [0,2] | {0} | {} |
64b Store | [0,7] | [0,6] | [0,4] | {0} |
可以看到,Zen 5 在 Store 完全包含 Load 的情况下都可以转发,没有额外的对齐要求。但当 Load 和 Store 只有部分重合时,就无法转发。两个连续的 32 位的 Store 和一个 64 位的 Load 重合也不能转发。
可见 Zen 5 的 Store to Load Forwarding 实现比较粗暴,只允许 Load 从单个完全包含 Load 的 Store 中转发数据。和 Neoverse V2 相比,Zen 5 对 Load 在 Store 内的偏移没有要求,但也不允许 Load 和 Store 只有一部分覆盖,也不支持一个 Load 从两个或更多的 Store 中获取数据。
成功转发时 8 cycle,有 Overlap 但转发失败时 14-15 cycle。
小结:Zen 5 的 Store to Load Forwarding:
官方信息:96-entry, fully associative
使用不同 footprint 的随机的 pointer chasing load 且每次 load 都在单独的页内,测试性能,得到如下结果:
可以观察到明显的 96 page 的拐点,命中 L1 DTLB 时 load to use latency 是 4 cycle,命中 L2 DTLB 时增大到了 11 cycle。
这个拐点也可以从性能计数器中看出,Zen 5 针对 L1 DTLB 有性能计数器 PMCx045 [L1 DTLB Misses] (Core::X86::Pmc::Core::LsL1DTlbMiss)
,根据这个事件记录 L1 DTLB Miss 次数,可以看到在 96 个页内时 Miss 次数为 0,之后开始增加,到 100 个页的时候 Miss 次数和访问次数相同,即 100% Miss Rate。
官方信息:4096-entry, 16-way set associative
官方信息:16-way set associative, inclusive, 1MB, >= 14 cycle load to use latency
官方信息:16-way set associative, exclusive
官方信息:Zen 5 的后端有 6 条 ALU 流水线,4 条访存流水线,4 条 512 位宽向量流水线(其中 2 条支持 FMA),2 条向量访存流水线
实测发现 Zen 5 每周期最多可以执行 2 条 AVX512 的浮点 FMA 指令,也就是说,每周期浮点峰值性能:
512/32*2*2=64
FLOP per cycle512/64*2*2=32
FLOP per cycle通过 512 位的浮点 datapath,终于达到了第一梯队的浮点峰值性能。注意移动端的 Zen 5 的浮点 datapath 砍半,只有 256 位。