当我在 Ryzen 9 3900X 上运行这个小汇编程序时:
_start: xor rax, rax
xor rcx, rcx
loop0: add rax, 1
mov rdx, rax
and rdx, 1
add rcx, rdx
cmp rcx, 1000000000
jne loop0
如果从loop0到jne(包括jne)之间的所有指令都完全包含在一个高速缓存行中,则它会在450毫秒内完成。也就是说,如果:
round((loop0的地址)/64) == round((jne指令末尾的地址)/64)
但是,如果上述条件不成立,则循环将花费 900 毫秒。
我已经使用代码 https://github.com/avl/strange_performance_repro 创建了一个存储库.
为什么在某些特定情况下内循环要慢得多?
编辑:删除了带有测试错误结论的声明。
最佳答案
您的问题在于 jne
指令的可变成本。
首先,要了解效果的影响,我们需要分析整个循环本身。 Ryzen 9 3900X的架构是Zen2。我们可以在AMD website上检索有关此的信息。或者也WikiChip 。 该架构有 4 个 ALU 和 3 个 AGU。这大致意味着它每个周期最多可以执行 4 条指令,例如 add/and/cmp。
以下是循环中每条指令的成本(基于 Zen1 的 Agner Fog instruction table):
# Reciprocal throughput
loop0: add rax, 1 # 0.25
mov rdx, rax # 0.2
and rdx, 1 # 0.25
add rcx, rdx # 0.25
cmp rcx, 1000000000 # 0.25 | Fused 0.5-2 (2 if jumping)
jne loop0 # 0.5-2 |
可以看到,循环的前4条计算指令大约可以在1个周期内执行完毕。您的处理器可以将最后 2 条指令合并为更快的指令。
您的主要问题是,与循环的其余部分相比,最后一个 jne
指令可能相当慢。因此,您很可能只测量该指令的开销。从这时起,事情开始变得复杂起来。
工程师和研究人员在过去的几十年里努力,以(几乎)不惜一切代价降低此类指令的成本。如今,处理器(如 Ryzen 9 3900X)使用无序指令调度来执行jne
指令所需的相关指令,如下所示尽快。大多数处理器还可以预测在jne
和获取新指令之后执行的下一条指令的地址(例如,下一个循环的指令)迭代),同时执行当前循环迭代的其他指令。
尽快执行提取对于防止处理器执行管道中出现任何停顿(特别是在您的情况下)非常重要。
从 AMD 文档“AMD 系列 17h 型号 30h 及更高处理器的软件优化指南”中,我们可以读到:
2.8.3 循环对齐:
For the processor loop alignment is not usually a significant issue. However, for hot loops, some further knowledge of trade-offs can be helpful. Since the processor can read an aligned 64-byte fetch block every cycle, aligning the end of the loop to the last byte of a 64-byte cache line is the best thing to do, if possible.
2.8.1.1 下一个地址逻辑
The next-address logic determines addresses for instruction fetch. [...]. When branches are identified, the next-address logic is redirected by the branch target and branch direction prediction hardware to generate a non-sequential fetch block address. The processor facilities that are designed to predict the next instruction to be executed following a branch are detailed in the following sections.
因此,对位于另一个高速缓存行中的指令执行条件分支会带来额外的延迟开销,因为如果整个高速缓存行都不需要读取操作高速缓存(比 L1 更快的指令高速缓存),则会产生额外的延迟开销。循环适合 1 个高速缓存行。事实上,如果循环跨越高速缓存线,则需要 2 次线高速缓存读取,这需要不少于 2 个周期。如果整个循环适合高速缓存行,则仅需要一次 1 行高速缓存读取,这仅需要 1 个周期。因此,由于循环迭代速度非常快,因此额外支付 1 个周期会导致速度显着减慢。但多少钱?
你说程序大约需要450毫秒。
由于 Ryzen 9 3900X Turbo 频率为 4.6 GHz 并且循环执行了 2e9 次,因此每个循环迭代的周期数为 1.035。请注意,这比我们之前预期的要好(我猜这个处理器能够重命名寄存器,忽略 mov
指令,并行执行 jne
指令,只需 1循环,而循环的其他指令则完美地流水线化;这是令人震惊的)。这还表明,支付 1 个周期的额外获取开销将使执行每个循环迭代所需的周期数加倍,从而使整体执行时间加倍。
如果您不想支付此开销,请考虑展开循环以显着减少条件分支和非顺序提取的数量。
此问题可能会发生在其他架构上,例如 Intel Skylake。事实上,i5-9600KF 上的相同循环在循环对齐的情况下需要 0.70 秒,在没有循环对齐的情况下需要 0.90 秒(也是由于额外的 1 个周期获取延迟)。展开 8 倍时,结果为 0.53 秒(无论对齐方式如何)。
关于performance - 为什么当我的循环包含在一个缓存行中时,它会快得多?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60497432/