Apple M4 微架构评测¶
背景¶
最近拿到了 Apple M4 的环境,借此机会测试一下 Apple M4 的微架构,和之前分析的 Apple M1 的微架构做比较。由于 Asahi Linux 尚不支持 Apple M4,所以这里的测试都在 macOS 上进行。
官方信息¶
Apple M4 的官方信息乏善可陈,关于微架构的信息几乎为零,但能从操作系统汇报的硬件信息中找到一些内容。
现有评测¶
网上已经有针对 Apple M4 微架构的评测和分析,建议阅读:
下面分各个模块分别记录官方提供的信息,以及实测的结果。读者可以对照已有的第三方评测理解。官方信息与实测结果一致的数据会加粗。
前端¶
取指带宽¶
P-Core¶
为了测试实际的 Fetch 宽度,参考 如何测量真正的取指带宽(I-fetch width) - JamesAslan 构造了测试。
其原理是当 Fetch 要跨页的时候,由于两个相邻页可能映射到不同的物理地址,如果要支持单周期跨页取指,需要查询两次 ITLB,或者 ITLB 需要把相邻两个页的映射存在一起。这个场景一般比较少,处理器很少会针对这种特殊情况做优化,但也不是没有。经过测试,把循环放在两个页的边界上,发现 M4 P-Core 微架构遇到跨页的取指时确实会拆成两个周期来进行。
在此基础上,构造一个循环,循环的第一条指令放在第一个页的最后四个字节,其余指令放第二个页上,那么每次循环的取指时间,就是一个周期(读取第一个页内的指令)加上第二个页内指令需要 Fetch 的周期数,多的这一个周期就足以把 Fetch 宽度从后端限制中区分开,实验结果如下:
图中蓝线(cross-page)表示的就是上面所述的第一条指令放一个页,其余指令放第二个页的情况,横坐标是第二个页内的指令数,那么一次循环的指令数等于横坐标 +1。纵坐标是运行很多次循环的总 cycle 数除以循环次数,也就是平均每次循环耗费的周期数。可以看到每 16 条指令会多一个周期,因此 M4 P-Core 的前端取指宽度确实是 16 条指令,和 Apple M1 的 P-Core 是相同的。
为了确认这个瓶颈是由取指造成的,又构造了一组实验,把循环的所有指令都放到一个页中,这个时候 Fetch 不再成为瓶颈(图中 aligned),两个曲线的对比可以明确地得出上述结论。
随着指令数进一步增加,最终瓶颈在每周期执行的 NOP 指令数,因此两条线重合。
E-Core¶
用相同的方式测试 M4 E-Core,结果如下:
由于两个曲线汇合的点太前(NOP 指令执行得不够快),无法确定 M4 E-Core 的取指宽度,但可以确认的是它每周期取值不少于 10 条指令,比 Apple M1 的 E-Core 要更快。如果读者想到什么办法来确认 M4 E-Core 的取指宽度,欢迎在评论区给出。
L1 ICache¶
官方信息:通过 sysctl 可以看到,P-Core 具有 192KB L1 ICache,E-Core 具有 128KB L1 ICache:
延续了从 Apple M1 以来的大小。
P-Core¶
为了测试 L1 ICache 容量,构造一个具有巨大指令 footprint 的循环,由大量的 nop 和最后的分支指令组成。观察在不同 footprint 大小下 M4 P-Core 的 IPC:
可以看到 footprint 在 192 KB 之前时可以达到 10 IPC,之后则快速降到 2.5 IPC,这里的 192 KB 就对应了 M4 P-Core 的 L1 ICache 的容量。虽然 Fetch 可以每周期 16 条指令,也就是一条 64B 的缓存行,由于后端的限制,只能观察到 10 的 IPC。
E-Core¶
用相同的方式测试 M4 E-Core,结果如下:
可以看到 footprint 在 128 KB 之前时可以达到 5 IPC,之后则快速降到 2.0 IPC,这里的 128 KB 就对应了 M4 E-Core 的 L1 ICache 的容量。
BTB¶
Apple M1 的 BTB 设计相对比较简单:1024 项的组相连 L1 BTB,接着是以 192KB L1 ICache 作为兜底的 3 周期的等效 BTB。但是 M4 上的 BTB 测试图像变化很大,下面进行仔细的分析。
P-Core¶
构造大量的无条件分支指令(B 指令),BTB 需要记录这些指令的目的地址,那么如果分支数量超过了 BTB 的容量,性能会出现明显下降。当把大量 B 指令紧密放置,也就是每 4 字节一条 B 指令时:
可以看到最低的 CPI 能达到接近 0.5(实际值在 0.55),说明 Apple M4 有了一定的每周期执行 2 taken branches 的能力,后面会着重讨论这一点。在经过 CPI 最低点之后,性能出现了先下降后上升再下降的情况,最终在 2048 个分支开始稳定在 2 左右(实际值在 2.10)的 CPI。
这个 2 左右的 CPI 一直稳定维持,一直延续到 49152 个分支。超出 BTB 容量以后,分支预测时,无法从 BTB 中得到哪些指令是分支指令的信息,只能等到取指甚至译码后才能后知后觉地发现这是一条分支指令,这样就出现了性能损失,出现了 2 CPI 的情况。49152 这个拐点,对应的是指令 footprint 超出 L1 ICache 的情况:L1 ICache 是 192KB,按照每 4 字节一个 B 指令计算,最多可以存放 49152 条 B 指令。
这个 2 CPI 的平台在 Apple M1 中是 3 CPI,这是一个巨大的优化,在大多数情况下,通过 L1 ICache 能以每 2 周期一条无条件分支的性能兜底。
接下来降低分支指令的密度,在 B 指令之间插入 NOP 指令,使得每 8 个字节有一条 B 指令,得到如下结果:
图像基本就是 4 字节间距情况下,整体左移的结果,说明各级 BTB 结构大概是组相连,当间距为 8 字节,PC[2] 恒为 0 的时候,只有一半的组可以被用到。
继续降低分支指令的密度,在 B 指令之间插入 NOP 指令,使得每 16 个字节有一条 B 指令,得到如下结果:
每 32 个字节有一条 B 指令:
从间距为 4 字节到间距为 32 字节,整个的图像都是类似的,只是不断在左移。但是当每 64 个字节有一条 B 指令的时候,情况就不同了:
整体的 CPI 有比较明显的下降,最低的 CPI 也在 2 以上,这和 Apple M1 上依然是在 4 字节间距的图像的基础上左移有显著的不同。
前面提到,Apple M4 P-Core 出现了每周期 2 taken branches,但是当分支不在同一个 64B 内的时候,性能会有明显下降;另一方面,以 ARM Neoverse V2 为例,它实现的每周期 2 taken branches,即使分支不在同一个 64B 内,也是可以做到的,下面是在 64B 间距下 ARM Neoverse V2 的测试结果:
根据这些现象,找到了 Apple 的一篇专利 Using a Next Fetch Predictor Circuit with Short Branches and Return Fetch Groups,它提到了一种符合上述现象的实现 2 taken branches 的方法:如果在一个 fetch group(在这里是 64B)内,有一条分支,它的目的地址还在这个 fetch group 内,由于 fetch group 的指令都已经取出来了,所以同一个周期内,可以从这条分支的目的地址开始,继续获取指令。下面是一个例子:
# the beginning of a fetch group
nop
# the branch
b target
# some instructions are skipped between branch and its target
svc #0
# the branch target resides in the same fetch group
target:
# some instructions after the branch target
add x3, x2, x1
ret
那么在传统的设计里,这段代码会被分成两个周期去取指,第一个周期取 nop
和 b target
,第二个周期取 add x3, x2, x1
和 ret
;按照这个专利的说法,可以在一个周期内取出所有指令,然后把中间被跳过的 svc #0
指令跳过去,不去执行它。当然了,分支预测器那边也需要做修改,能够去预测第二个分支的目的地址,用于下一个周期。
如果是这种实现方法,是可能在一个 Coupled 前端内,实现这种有限场景的每周期执行 2 taken branches,核心是每周期依然只访问一次 ICache。
E-Core¶
另一方面,M4 E-Core 的 BTB 设计和 Apple M1 的 E-Core 十分接近,当分支间距是 4 字节时:
可以看到 1024 的拐点,1024 之前是 1 IPC,之后增加到 3 IPC。比较奇怪的是,没有看到第二个拐点。
8B 的间距:
拐点前移到 512。
16B 的间距:
第一个拐点前移到 256,第二个拐点出现在 8192,而 M4 E-Core 的 L1 ICache 容量是 128KB,16B 间距下正好可以保存 8192 个分支。
可见 M4 E-Core 的前端设计和 M4 P-Core 有较大的不同。
L1 ITLB¶
P-Core¶
构造一系列的 B 指令,使得 B 指令分布在不同的 page 上,使得 ITLB 成为瓶颈,在 M4 P-Core 上进行测试:
第一个拐点是由于 L1 BTB 的冲突缺失,之后在 192 个页时从 3 Cycle 快速增加到 12 Cycle,则对应了 192 项的 L1 ITLB 容量。这和 M1 P-Core 是一样的。
E-Core¶
在 M4 E-Core 上重复实验:
第一个拐点是由于 L1 BTB 的冲突缺失,之后在 192 个页时从 3 Cycle 快速增加到 10 Cycle,则对应了 192 项的 L1 ITLB 容量。相比 M1 E-Core 的 128 项,容量变大了,和 M4 P-Core 看齐。
Decode¶
从前面的测试来看,M4 P-Core 最大观察到 10 IPC,M4 E-Core 最大观察到 5 IPC,那么 Decode 宽度也至少是这么多,暂时也不能排除有更大的 Decode 宽度。相比 M1 的 P-Core 8 IPC,E-Core 4 IPC 都有拓宽。
Return Stack¶
P-Core¶
构造不同深度的调用链,测试每次调用花费的平均时间,在 M4 P-Core 上得到下面的图:
可以看到调用链深度为 60 时性能突然变差,因此 M4 P-Core 的 Return Stack 深度为 60,比 M1 P-Core 的 50 要更大。这里测试的两个 Variant,对应的是 Return 的目的地址不变还是会变化。
E-Core¶
在 M4 E-Core 上测试:
可以看到调用链深度为 40 时性能突然变差,因此 M4 E-Core 的 Return Stack 深度为 40,比 M1 E-Core 的 32 要更大。
后端¶
物理寄存器堆¶
P-Core¶
为了测试物理寄存器堆的大小,一般会用两个依赖链很长的操作放在开头和结尾,中间填入若干个无关的指令,并且用这些指令来耗费物理寄存器堆。M4 P-Core 测试结果见下图:
- 32b int:测试 speculative 32 位整数寄存器的数量,拐点在 720 左右
- 64b int:测试 speculative 64 位整数寄存器的数量,拐点在 360 左右,只有 32b 的一半,可见实际的物理寄存器堆有 360 左右个 64 位整数寄存器,但是可以分成两半分别当成 32 位整数寄存器用,这一个优化在 M1 是没有的,即用 32b 或者用 64b 整数,测出来的整数寄存器数量相同
- flags:测试 speculative NZCV 寄存器的数量,拐点在 175 左右
- 32b fp:测试 speculative 32 位浮点寄存器的数量,没有观察到明显的拐点
寄存器堆大小和 M1 P-Core 比较类似,但是多了 32 位整数寄存器的优化。
E-Core¶
在 M4 E-Core 上复现相同的测试,发现性能非常不稳定,不确定是什么原因。
Load Store Unit + L1 DCache¶
L1 DCache 容量¶
官方信息:通过 sysctl 可以看到,M4 P-Core 具有 128KB L1 DCache,M4 E-Core 具有 64KB L1 DCache:
和 M1 相同。
P-Core¶
构造不同大小 footprint 的 pointer chasing 链,在每个页的开头放一个指针,测试不同 footprint 下每条 load 指令耗费的时间,M4 P-Core 上的结果:
可以看到 128KB 出现了拐点,对应的就是 128KB 的 L1 DCache 容量。当 footprint 比较小的时候,由于 Load Address/Value Predictor 的介入,打破了依赖链,所以出现了 latency 小于正常 load to use 的 3 cycle latency 的情况。
E-Core¶
M4 E-Core 上的结果:
可以看到 128KB 出现了明显的拐点,但实际上 M4 E-Core 的 L1 DCache 只有 64KB。猜测这是因为在测试的时候,是在每个 16KB 页的开头放一个指针,但如果 L1 DCache 的 Index 并非都在 16KB 内部,就会导致实际测出来的不是 L1 DCache 的大小。修改测试,使得每 8 字节一个指针,此时测出来的结果就是正确的 64KB 大小:
此时 64KB 对应的就是 64KB 的 L1 DCache 容量。L1 DCache 范围内延迟是 3 cycle,之后提升到 14+ cycle。由此可见 M4 E-Core 没有 Load Address/Value Predictor,不能打断依赖链。
L1 DTLB 容量¶
P-Core¶
用类似的方法测试 L1 DTLB 容量,只不过这次 pointer chasing 链的指针分布在不同的 page 的不同 cache line 上,使得 DTLB 成为瓶颈,在 M4 P-Core 上:
从 160 个页开始性能下降,到 200 个页时性能稳定在 9 CPI,认为 M4 P-Core 的 L1 DTLB 有 160 项,大小和 M1 P-Core 相同。9 CPI 包括了 L1 DTLB miss L2 TLB hit 带来的额外延迟。中间有时性能特别快,是 Load Address/Value Predictor 的功劳。
E-Core¶
M4 E-Core 测试结果:
从 192 个页开始性能下降,到 224 个页时性能稳定在 9 CPI,认为 M4 E-Core 的 L1 DTLB 有 192 项,比 M1 E-Core 的 128 项更大,甚至大过了 P-Core。9 CPI 包括了 L1 DTLB miss L2 TLB hit 带来的额外延迟,比 M1 E-Core 少了一个周期。
Load/Store 带宽¶
P-Core¶
针对 Load Store 带宽,实测 M4 P-Core 每个周期可以完成:
- 3x 128b Load + 1x 128b Store
- 2x 128b Load + 2x 128b Store
- 1x 128b Load + 2x 128b Store
- 2x 128b Store
如果把每条指令的访存位宽从 128b 改成 256b,读写带宽不变,指令吞吐减半。也就是说最大的读带宽是 48B/cyc,最大的写带宽是 32B/cyc,二者不能同时达到。和 M1 P-Core 相同。
E-Core¶
实测 M4 E-Core 每个周期可以完成:
- 2x 128b Load
- 1x 128b Load + 1x 128b Store
- 1x 128b Store
如果把每条指令的访存位宽从 128b 改成 256b,读写带宽不变,指令吞吐减半。也就是说最大的读带宽是 32B/cyc,最大的写带宽是 16B/cyc,二者不能同时达到。和 M1 E-Core 相同。
Memory Dependency Predictor¶
为了预测执行 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
指令时会出现性能下降。
P-Core¶
在 M4 P-Core 上测试:
数据依赖没有明显的阈值,但从 72 开始出现了一个小的增长,且斜率不为零;地址依赖的阈值是 39。相比 M1 P-Core 有所减小。
E-Core¶
M4 E-Core:
数据依赖的阈值是 20,地址依赖的阈值是 14。比 M1 E-Core 略大。
Store to Load Forwarding¶
P-Core¶
经过实际测试,M4 P-Core 上如下的情况可以成功转发,对地址 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 的数据时,也可以成功转发。即使数据跨越缓存行,也可以转发,只是多耗费 1-2 个周期。但在跨 64B 缓存行的时候,代价可能多于一个周期。相比 M1 P-Core,M4 P-Core 在跨越缓存行的情况下也可以得到比较好的性能。
成功转发时 7 cycle 左右。
小结:Apple M4 P-Core 的 Store to Load Forwarding:
- 1 ld + 1 st: 支持
- 1 ld + 2 st: 支持
- 1 ld + 4 st: 支持
- 1 ld + 8 st: 支持
- 跨 64B 缓存行边界时,性能略微下降
E-Core¶
在 M4 E-Core 上,如果 Load 和 Store 访问范围出现重叠,当需要转发一个到两个 Store 的数据时,需要 7 Cycle,无论是否跨缓存行。如果需要转发四个 Store 的数据,则需要 8 Cycle;转发八个 Store 的数据需要 11 Cycle。相比 M1 E-Core,多数情况下获得了性能提升。
Load to use latency¶
P-Core¶
实测 M4 P-Core 的 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:
- load 的目的寄存器作为 alu 的源寄存器(下称 load to alu latency)
ldr x0, [sp, x0, lsl #3]
:load 结果转发到 indexldp x1, x0, [x0]
:load pair 的第二个目的寄存器转发到基地址,无偏移
注意由于 Load Address/Value Predictor 的存在,测试的时候需要排除预测器带来的影响。延迟方面,和 M1 P-Core 相同。
E-Core¶
实测 M4 E-Core 的 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:
- load 的目的寄存器作为 alu 的源寄存器(下称 load to alu latency)
ldr x0, [sp, x0, lsl #3]
:load 结果转发到 indexldp x1, x0, [x0]
:load pair 的第二个目的寄存器转发到基地址,无偏移
延迟方面,和 M1 E-Core 相同。
Virtual Address UTag/Way-Predictor¶
Linear Address UTag/Way-Predictor 是 AMD 的叫法,但使用相同的测试方法,也可以在 Apple M1 上观察到类似的现象,猜想它也用了类似的基于虚拟地址的 UTag/Way Predictor 方案,并测出来它的 UTag 也有 8 bit,M4 P-Core 和 M4 E-Core 都是相同的:
- VA[14] xor VA[22] xor VA[30] xor VA[38] xor VA[46]
- VA[15] xor VA[23] xor VA[31] xor VA[39] xor VA[47]
- VA[16] xor VA[24] xor VA[32] xor VA[40]
- VA[17] xor VA[25] xor VA[33] xor VA[41]
- VA[18] xor VA[26] xor VA[34] xor VA[42]
- VA[19] xor VA[27] xor VA[35] xor VA[43]
- VA[20] xor VA[28] xor VA[36] xor VA[44]
- VA[21] xor VA[29] xor VA[37] xor VA[45]
一共有 8 bit,由 VA[47:14] 折叠而来。和 Apple M1 相同。
Load Address/Value Predictor¶
Apple 从 M2 开始引入 Load Address Predictor,从 M3 开始引入 Load Value Predictor,相关的信息如下:
- Load Address Predictor:支持 Constant 和 Striding Address 两种模式,专利是 Early load execution via constant address and stride prediction
- Load Value Predictor(也称 Load Output Predictor):只支持 Constant Value,专利是 Shared learning table for load value prediction and load address prediction
这两个 Predictor 会对已有的基于 Load 的各种 microbenchmark 带来深刻的影响。
网上已有针对这两个 Predictor 的逆向和攻击:SLAP: Data Speculation Attacks via Load Address Prediction on Apple Silicon;FLOP Breaking the Apple M3 CPU via False Load Output Predictions。
P-Core¶
构造依赖链,但是实际测试可以观察到打破依赖链的情况,比串行执行要更快。测试下来,大概可以跟踪 60 条 Load 指令的地址并实现预测。
E-Core¶
M4 E-Core 没有实现 Load Address/Value Predictor。
执行单元¶
P-Core¶
在 M4 P-Core 上测试如下各类指令的延迟和每周期吞吐:
Instruction | Latency | Throughput |
---|---|---|
asimd int add | 2 | 4 |
asimd aesd/aese | 2/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 | 3 | 4 |
asimd fmul | 3 | 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 | 2 | 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 | 7.5 |
int addi | 1 | 8 |
int bfm | 1 | 1 |
int crc | 3 | 1 |
int csel | 1 | 4 |
int madd (addend) | 1 | 2.8 |
int madd (others) | 4 | 2.8 |
int mrs nzcv | - | 2 |
int mul | 3 | 3 |
int nop | - | 10 |
int sbfm | 1 | 8 |
int sdiv | 7 | 0.5 |
int smull | 3 | 3 |
int ubfm | 1 | 8 |
int udiv | 7 | 0.5 |
not taken branch | - | 2 |
taken branch | - | 1-2 |
mem asimd load | - | 3 |
mem asimd store | - | 2 |
mem int load | - | 3 |
mem int store | - | 2 |
从上面的结果可以初步得到的信息:
- 标量浮点和 ASIMD 吞吐最大都是 4,意味着有 4 个浮点/ASIMD 执行单元,但并非完全对称,例如 fdiv/frecpe/frecpx/frsqrte/fsqrt/fjcvtzs 由于吞吐不超过 1,大概率只能在一个执行单元内执行。但这些指令是不是都只能在同一个执行单元内执行,还需要进一步的测试;这部分和 M1 P-Core 相同,但浮点乘法 fmla/fmul 的延迟从 4 周期降低到了 3 周期
- 浮点和整数之间的 move 或 convert 指令,fmov i2f/cvti2f 吞吐是 3,fmov f2i/cvtf2i 吞吐是 2,那么这些指令是在哪个执行单元里实现的,是否需要同时占用整数执行单元和浮点执行单元,需要进一步测试;这部分和 M1 P-Core 相同
- 整数方面,根据吞吐,推断出如下几类指令对应的执行单元数量:
- ALU: 8
- CSEL: 4
- Mul/MAdd: 3
- Br/MRS NZCV: 2
- CRC/BFM/Div: 1
- ALU/CSEL/Mul/MAdd 的执行单元相比 M1 P-Core 有扩充
- 访存方面,每周期最多 3 Load 或者 2 Store;这部分和 M1 P-Core 相同
首先来看浮点和 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.33=1/3 |
fp fdiv + fmov f2i | 0.5 |
fp fdiv + 2x fmov f2i | 0.33=1/3 |
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.75=1/1.33 |
根据以上测试结果,可以得到如下的推论:
- fp fdiv/frecpe/frecpx/frsqrte 混合的时候,吞吐只有一半,IPC 不变,说明这些指令在同一个执行单元中,混合并不能带来更高的 IPC;这部分和 M1 P-Core 相同
- fp fdiv 和 fp fsqrt 混合时,吞吐下降到 0.33 一个不太整的数字,猜测是因为它们属于同一个执行单元内的不同流水线,抢占寄存器堆写口;这部分和 M1 P-Core 相同
- fp fdiv + fmov f2i 的时候吞吐是 0.5,而 fdiv + 2x fmov f2i 时吞吐下降到 0.33,IPC 维持在 1,说明有 1 个执行单元执行 fdiv 或 fmov f2i,但奇怪的是单独执行 fmov f2i 可以达到 2 的 IPC;这部分吞吐比 M1 P-Core 要差
- fp fdiv + 3x fmov i2f 的时候吞吐是 1,而 fdiv + 4x fmov i2f 时吞吐下降到 0.75,此时每周期还是执行 3 条 fmov i2f 指令,意味着 fdiv 没有抢占 fmov i2f 的执行单元,它们用的执行单元是独立的;这部分和 M1 P-Core 相同
- fmov i2f + 4x fp fadd 的时候吞吐是 1,说明 fmov i2f 没有抢占 fp fadd 的执行单元;这部分和 M1 P-Core 相同
推断这四个执行单元支持的操作:
- basic fp/asimd ops + aes + fdiv + frecpe + frecpx + frsqrte + fsqrt + fmov f2i + cvtf2i
- basic fp/asimd ops + aes + fmov f2i + cvtf2i
- basic fp/asimd ops + aes
- basic fp/asimd ops + aes
当然还有很多指令没有测,不过原理是一样的。这部分和 M1 P-Core 相同。
访存部分,前面已经在测 LSU 的时候测过了,每周期 Load + Store 不超过 4 个,其中 Load 不超过 3 个,Store 不超过 2 个。虽然从 IPC 的角度来看 LSU 的 Load/Store Pipe 未必准确,比如可能它发射和提交的带宽是不同的,但先暂时简化为如下的执行单元:
- load + store
- load
- load
- store
这部分和 M1 P-Core 相同。
最后是整数部分。从 addi 的指令来看,有 8 个 ALU,能够执行基本的整数指令。但其他很多指令可能只有一部分执行单元可以执行:bfm/crc/csel/madd/mrs nzcv/mul/div/branch/fmov i2f。为了测试这些指令使用的执行单元是否重合,进行一系列的混合指令测试,吞吐的定义和上面相同:
指令 | 吞吐 |
---|---|
4x int csel + 3x fmov i2f | 1 |
int csel + 2x fmov f2i | 1 |
2x int csel + 2x fmov f2i | 0.80=1/1.25 |
3x int csel + int bfm | 1 |
4x int csel + int bfm | 0.80=1/1.25 |
4x int csel + int crc | 1 |
3x int csel + int madd | 1.33=1/0.75 |
4x int csel + int madd | 1 |
4x int csel + 2x int madd | 1 |
4x int csel + 3x int madd | 0.75=1/1.33 |
4x int csel + int mul | 1 |
3x int csel + int sdiv | 0.5 |
4x int csel + int sdiv | 0.45=1/2.23 |
3x int csel + mrs nzcv | 1 |
4x int csel + mrs nzcv | 0.80=1/1.25 |
3x int csel + not taken branch | 1 |
4x int csel + not taken branch | 0.80=1/1.25 |
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 |
2x int madd + int crc | 1 |
3x int madd + int crc | 0.75=1/1.33 |
2x int madd + int mul | 1 |
3x int madd + int mul | 0.75 |
2x int madd + int sdiv | 0.5 |
3x int madd + int sdiv | 0.5 |
3x int madd + mrs nzcv | 1 |
根据上述结果分析:
- 吞吐与不混合时相同,代表混合的指令对应的执行单元不重合
- 2x int madd + int mul 的 IPC 是 3,3x int add + int mul 的 IPC 也是 3,说明有三个执行单元可以执行 madd 和 mul:
- alu + madd + mul
- alu + madd + mul
- alu + madd + mul
- 2x int madd + int crc 的 IPC 是 3,3x int madd + int crc 的 IPC 也是 3,说明其中一个执行单元可以执行 crc:
- alu + madd + mul + crc
- alu + madd + mul
- alu + madd + mul
- 4x int csel + 2x int madd 的吞吐是 1,4x int csel + 3x int madd 的吞吐是 0.75,说明它们有一个重合的执行单元,并且由于 4x int csel + int crc 的吞吐是 1,所以重合的执行单元不是 crc 的那一个:
- alu + madd + mul + crc
- alu + madd + mul + csel
- alu + madd + mul
- alu + csel
- alu + csel
- alu + csel
- 4x int csel + mrs nzcv 的 IPC 等于 4,说明 mrs nzcv 的执行单元被包括在能执行 csel 的四个执行单元当中;而 3x int madd + mrs nzcv 的吞吐等于 1,说明 mrs nzcv 的执行单元和 int madd 不重合:
- alu + madd + mul + crc
- alu + madd + mul + csel
- alu + madd + mul
- alu + csel + mrs nzcv
- alu + csel + mrs nzcv
- alu + csel
- 因为 mrs nzcv + 2x not taken branch 的吞吐是 0.67,此时 IPC 等于 2,说明它们的执行单元重合:
- alu + madd + mul + crc
- alu + madd + mul + csel
- alu + madd + mul
- alu + csel + mrs nzcv + branch
- alu + csel + mrs nzcv + branch
- alu + csel
得到初步的结果:
- alu + madd + mul + crc
- alu + madd + mul + csel
- alu + madd + mul
- alu + csel + mrs nzcv + branch
- alu + csel + mrs nzcv + branch
- alu + csel
- alu
- alu
还有很多其他的指令没有测试,不过方法是类似的。从上面的结果里,可以看到一些值得一提的点:
- fmov f2i 同时占用了浮点执行单元和整数执行单元,这主要是为了复用寄存器堆读写口:fmov f2i 需要读浮点寄存器堆,又需要写整数寄存器堆,那就在浮点侧读寄存器,在整数侧写寄存器。
- fmov i2f 既不在浮点,也不在整数,那只能在访存了:而正好访存执行单元需要读整数,写整数或浮点,那就可以复用它的寄存器堆写口来实现 fmov i2f 的功能。
- 可见整数/浮点/访存执行单元并不是完全隔离的,例如一些微架构,整数和浮点是直接放在一起的。
小结:M4 P-Core 的执行单元如下:
- alu + madd + mul + crc
- alu + madd + mul + csel
- alu + madd + mul
- alu + csel + mrs nzcv + branch
- alu + csel + mrs nzcv + branch
- alu + csel
- alu
- alu
- load + store
- load
- load
- store
- basic fp/asimd ops + aes + fdiv + frecpe + frecpx + frsqrte + fsqrt + fmov f2i + cvtf2i
- basic fp/asimd ops + aes + fmov f2i + cvtf2i
- basic fp/asimd ops + aes
- basic fp/asimd ops + aes
相比 M1 P-Core,只在整数方面有扩充。
E-Core¶
接下来用类似的方法测试 M4 E-Core:
Instruction | Latency | Throughput |
---|---|---|
asimd int add | 2 | 3 |
asimd aesd/aese | 2.5/3 | 3 |
asimd aesimc/aesmc | 2 | 3 |
asimd fabs | 2 | 3 |
asimd fadd | 2.5 | 3 |
asimd fdiv 64b | 11 | 0.5 |
asimd fdiv 32b | 9 | 0.5 |
asimd fmax | 2 | 3 |
asimd fmin | 2 | 3 |
asimd fmla | 4 | 2 |
asimd fmul | 4 | 2 |
asimd fneg | 2 | 3 |
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) | - | 2 |
fp cvti2f (scvtf) | - | 1.5 |
fp fabs | 2 | 3 |
fp fadd | 2.5 | 3 |
fp fdiv 64b | 10 | 1 |
fp fdiv 32b | 8 | 1 |
fp fjcvtzs | - | 2 |
fp fmax | 2 | 3 |
fp fmin | 2 | 3 |
fp fmov f2i | - | 2 |
fp fmov i2f | - | 2 |
fp fmul | 4 | 2 |
fp fneg | 2 | 3 |
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 |
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 | - | 5 |
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 |
从上面的结果可以初步得到的信息:
- 标量浮点和 ASIMD 吞吐最大都是 3,意味着有 3 个浮点/ASIMD 执行单元,但并非完全对称,例如 fdiv/frecpe/frecpx/frsqrte/fsqrt 由于吞吐不超过 1,大概率只能在一个执行单元内执行;fmla/fmul 的吞吐只有 2,只能在其中两个执行单元内执行。但这些指令是不是都只能在同一个执行单元内执行,还需要进一步的测试;相比 M1 E-Core,添加了一个浮点/ASIMD 执行单元
- 整数方面,根据吞吐,推断出如下几类指令对应的执行单元数量:
- ALU: 4
- CSEL/MRS NZCV/SBFM/UBFM: 3
- Br: 2
- Mul/CRC/BFM/MAdd/Div: 1
- 相比 M1 E-Core 增加了 ALU 的数量
- 虽然 Br 的吞吐可以达到 2,但是每周期只能有一个 taken branch,和 M1 E-Core 相同
- 访存方面,每周期最多 2 Load 或者 1 Store,和 M1 E-Core 相同
还是先看浮点,基本指令 add/aes/fabs/fadd/fmax/fmin/fneg 都能做到 3 的吞吐,也就是这三个执行单元都能执行这些基本指令。接下来测其余指令的混合吞吐(吞吐定义见上):
指令 | 吞吐 |
---|---|
fp fdiv + fp frecpe | 0.5 |
fp fdiv + fp frecpx | 0.5 |
fp fdiv + fp frsqrte | 0.5 |
fp fdiv + fp fsqrt | 0.31=1/3.25 |
fp fdiv + fmov f2i | 0.5 |
fp fdiv + 2x fmov f2i | 0.66=1/1.50 |
fp fdiv + 2x fmov i2f | 1 |
fp fdiv + 3x fmov i2f | 0.67=1/1.50 |
fp fdiv + fp fmul | 1 |
fp fdiv + 2x fp fmul | 0.6 |
fp fmul + fmov f2i | 1 |
2x fp fmul + fmov f2i | 0.67=1/1.50 |
可见 fdiv/frecpe/frecpx/frsqrte/fsqrt 都在同一个执行单元内:
- basic fp/asimd ops + aes + fdiv + frecpe + frecpx + frsqrte + fsqrt
- basic fp/asimd ops + aes
- basic fp/asimd ops + aes
由于 fp fdiv + 2x fmov f2i 的 IPC 是 2,说明它们有重合的执行单元:
- basic fp/asimd ops + aes + fdiv + frecpe + frecpx + frsqrte + fsqrt + fmov f2i
- basic fp/asimd ops + aes + fmov f2i
- basic fp/asimd ops + aes
因为 2x fp fmul + fmov f2i 的 IPC 也只有 2,说明 fp fmul 和 fmov f2i 是重合的:
- basic fp/asimd ops + aes + fdiv + frecpe + frecpx + frsqrte + fsqrt + fmov f2i + fmul
- basic fp/asimd ops + aes + fmov f2i + fmul
- basic fp/asimd ops + aes
还有很多指令没有测,不过原理是一样的。访存在前面测 LSU 的时候已经测过了:
- load + store
- load
最后是整数部分。从 add 的指令来看,有 4 个 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 的两个执行单元不重合,因此整数侧的执行单元有:
- alu + csel + mrs nzcv + branch
- alu + csel + mrs nzcv + branch
- alu + csel + mrs nzcv + madd + mul + crc
- alu
小结:M4 E-Core 的执行单元如下:
- alu + csel + mrs nzcv + branch
- alu + csel + mrs nzcv + branch
- alu + csel + mrs nzcv + madd + mul + crc
- alu
- load + store
- load
- basic fp/asimd ops + aes + fdiv + frecpe + frecpx + frsqrte + fsqrt + fmov f2i + fmul
- basic fp/asimd ops + aes + fmov f2i + fmul
- basic fp/asimd ops + aes
相比 M1 E-Core,整数和浮点方面都有扩充。
Scheduler¶
P-Core¶
在 M4 P-Core 上测试,结果如下:
指令 | 可调度 + 不可调度 | 可调度 |
---|---|---|
ld | 66 | 51 |
st addr | 66 | 51 |
st data | 86 | 70 |
alu | 198 | 175 |
fp | 266 | 243 |
crc | 36 | 23 |
idiv | 36 | 23 |
bfm | 31 | 18 |
fjcvtzs | 72 | 60 |
fmov f2i | 144 | 121 |
csel | 98 | 85 |
mrs nzcv | 60 | 47 |
首先看浮点:
- 可调度部分 fp 是 243,fmov f2i 是 121,fjcvtzs 是 60,有明显的 4:2:1 的关系
- fp/fmov f2i/fjcvtzs 吞吐刚好也是 4:2:1 的关系
- 因此四个执行单元前面各有一个独立的 60 entry 的 Scheduler
- 不可调度部分,266-243=23,144-121=23,72-60=12,猜测有两个 Non Scheduling Queue,每个 Non Scheduling Queue 12 entry,分别对应两个 Scheduler
相比 M1 P-Core 有比较大的扩充:Scheduler 大小从 36 扩大到 60,Non Scheduling Queue 从 6 扩大到了 12。
下面是访存部分,load 和 store addr 一样,但 store data 要更多,可能做了不同的处理。
最后是整数部分,由于有 8 个整数执行单元,情况会比较复杂:
- 可调度部分 alu 一共是 175,其中 csel 是 85,crc/idiv 都是 23,bfm 是 18,mrs nzcv 是 47,结合 8 个整数执行单元,可以得到这 8 个执行单元对应的 Scheduler 大小关系:
- alu + madd + mul + crc: 23 entries
- alu + madd + mul + csel: a entries
- alu + madd + mul: b entries
- alu + csel + mrs nzcv + branch: c entries
- alu + csel + mrs nzcv + branch: 47-c entries
- alu + csel: 38-a entries
- alu: d entries
- alu: 67-b-d entries
- alu 不可调度部分是 198-175=23,crc/idiv/bfm/csel/mrs nzcv 不可调度部分都是 13,应该是其中四个执行单元共享一个 12 entry 的 Non Scheduling Queue;另外四个执行单元共享剩下的 12 entry 的 Non Scheduling Queue
- 最后只差 a 到 d 的取值没有求出来,可以通过进一步测试来更加精确地求出
Scheduler 大小相比 M1 P-Core 有比较大的扩充,Non Scheduling Queue 没有变化。
E-Core¶
在 M4 E-Core 上测试,结果很不稳定,需要进一步研究。
Reorder Buffer¶
P-Core¶
首先用不同数量的 fsqrt 依赖链加 NOP 指令测试 M4 P-Core 的 ROB 大小:
可以看到当 fsqrt 数量足够多的时候,出现了统一的拐点,在 3184 条指令左右。
为了测 Coalesced ROB 的大小,改成用 load/store 指令,可以测到拐点在 313 左右:
3184 除以 313 约等于 10,意味着每个 group 可以保存 10 条指令,一共有 313 左右个 group。相比 M1 P-Core,Group 数量差不多,但是能保存的指令数量有了很大的提升。
E-Core¶
首先用 NOP 指令测试 M4 E-Core 的 ROB 大小:
可以看到拐点是 513 条指令。
为了测 Coalesced ROB 的大小,改成用 load/store 指令,可以测到拐点在 121 左右:
但是 513 除以 121 是 4.24,离 4 或者 5 都有一段距离,比较奇怪,不确定每个 group 可以放多少条指令。容量上比 M1 E-Core 有明显提升。
L2 Cache¶
官方信息:通过 sysctl 可以看到,4 个 M4 P-Core 核心共享一个 16MB L2 Cache,6 个 M4 E-Core 核心共享一个 4MB L2 Cache:
hw.perflevel0.l2cachesize: 16777216
hw.perflevel0.cpusperl2: 4
hw.perflevel1.l2cachesize: 4194304
hw.perflevel1.cpusperl2: 6
L2 TLB¶
P-Core¶
沿用之前测试 L1 DTLB 的方法,把规模扩大到 L2 Unified TLB 的范围,就可以测出来 L2 Unified TLB 的容量,下面是 M4 P-Core 上的测试结果:
可以看到拐点是 3072 个 Page,说明 M4 P-Core 的 L2 TLB 容量是 3072 项。这和 M1 P-Core 是一样的。
E-Core¶
在 M4 E-Core 上测试:
可以看到拐点是 1024 个 Page,说明 M4 E-Core 的 L2 TLB 容量是 1024 项。这和 M4 E-Core 是一样的。
总结¶
M4 相比 M1,在很多方面做了迭代:
- P-Core 的前端有较大改进,尤其是 BTB 部分
- 各种结构相比 M1 有了容量的增加
- 寄存器堆增加了对 32 位整数寄存器的优化
- 引入了 Load Address/Value Predictor
- 扩充了执行单元,P-Core 主要扩充了整数,E-Core 则是整数和浮点都做了扩充
- 添加了 SME 指令集
但也有一些遗憾,例如访存方面没有每周期带宽上的增加,P-Core 的浮点也没有增加。