为什么我的循环在包含在一个缓存线中时要快得多?

人气:853 发布:2022-10-16 标签: caching performance cpu

问题描述

当我在我的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((循环0的地址)/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。

这里是循环的每条指令的开销(基于Agner Fog instruction tableforZen1):

                                        # 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个周期内执行。最后两条指令可以由您的处理器合并成速度更快的指令。 您的主要问题是,与循环的其余部分相比,最后这条jne指令可能非常慢。因此,您很可能只测量此指令的开销。从这一点开始,事情开始变得复杂起来。

工程师和研究人员在过去几十年里努力工作,以(几乎)不惜一切代价降低此类指令的成本。如今,处理器(如Ryzen 9 3900X)使用乱序指令调度来尽快执行jne指令所需的依赖指令。大多数处理器还可以预测在jne和Fetch新指令(例如,下一个循环迭代中的一个),而当前循环迭代的另一条指令正在被执行。 尽快执行提取对于防止处理器的执行流水线中出现任何停顿非常重要(在您的情况下尤其如此)。

从AMD文档《AMD系列17H型号30H和更高处理器的软件优化指南》中,我们可以阅读:

2.8.3循环对齐:

对于处理器来说,循环对齐通常不是一个重要的问题。然而,对于热循环,进一步了解一下权衡可能会有所帮助。由于处理器可以在每个周期读取对齐的64字节读取块,因此如果可能,最好将循环末尾与64字节高速缓存线的最后一个字节对齐。

2.8.1.1下一地址逻辑

下一个地址逻辑确定取指令的地址。[...]。当识别分支时,下一地址逻辑由分支目标和分支方向预测硬件重定向以生成非顺序提取块地址。以下各节详细介绍了用于预测分支之后要执行的下一条指令的处理器设施。

因此,执行到位于另一个高速缓存线中的指令的条件分支会引入额外的等待时间开销,这是因为如果整个循环适合于一个高速缓存线,则不需要提取操作高速缓存(比一级更快的指令高速缓存)。事实上,如果循环正在跨越高速缓存线,则需要2次线高速缓存获取,这需要不少于2个周期。如果整个循环适合高速缓存线,则只需要一次1行高速缓存提取,这只需要1个周期。因此,由于您的循环迭代非常快,因此额外支付1个周期会显著降低速度。但是多少钱呢?

您说该程序大约需要450毫秒。 由于Ryzen93900X的涡轮频率为4.6千兆赫,并且您的循环执行2e9次,因此每个循环迭代的周期数为1.035。请注意,这比我们之前预期的要好(我猜这个处理器能够重命名寄存器,忽略mov指令,仅在1个周期内并行执行jne指令,而循环的其他指令是完全流水线的;这是令人震惊的)。这还表明,支付1个周期的额外获取开销将使执行每个循环迭代所需的周期数增加一倍,从而降低总体执行时间。

如果您不想支付此开销,请考虑展开循环以显著减少条件分支和非顺序提取的数量。

此问题可能发生在其他架构上,例如在Intel Skylake上。事实上,在i5-9600KF上,相同的循环在循环对齐的情况下需要0.7秒,而在没有循环对齐的情况下需要0.9秒(也是因为额外的1周期获取延迟)。展开8倍后,结果为0.53s(无论对齐方式)。

316