为什么在执行指针追逐时这条跳转指令如此昂贵?

bry*_*rto 2 x86 assembly pointers cpu-architecture perf

我有一个执行指针追踪的程序,我正在尝试尽可能地优化指针追踪循环。我注意到perf record检测到函数中大约 20% 的执行时间myFunction()用于执行跳转指令(用于在读取特定值后退出循环)。

需要注意的一些事项:

  • 指针追踪路径可以轻松放入 L1 数据缓存
  • 使用__builtin_expect以避免分支预测错误的成本没有显着影响

perf record 有以下输出:

Samples: 153K of event 'cycles', 10000 Hz, Event count (approx.): 35559166926                                                                                                                                                               
myFunction  /tmp/foobar [Percent: local hits]                                                                                                                                                                            
Percent?      endbr64                                                                                                                                                                                                                       
      ...
 80.09 ?20:   mov     (%rdx,%rbx,1),%ebx                                                                                                                                                                                                    
  0.07 ?      add     $0x1,%rax                                                                                                                                                                                                             
       ?      cmp     $0xffffffff,%ebx                                                                                                                                                                                                      
 19.84 ?    ? jne     20                                                                                                                                                                                                                    
      ...
Run Code Online (Sandbox Code Playgroud)

我希望在这个循环中花费的大部分周期都用于从内存中读取值,这是由 perf 确认的。我还希望剩余的周期在执行循环中剩余的指令时会有些均匀地花费。相反, perf 报告的是剩余周期的很大一部分用于执行跳转。

我怀疑通过了解用于执行这些指令的微操作,我可以更好地了解这些成本,但我对从哪里开始有点迷茫。

Pet*_*des 5

请记住cycles,即使mov-load 和宏融合的cmp-and-branch uops 都在等待结果,事件也必须选择要归咎于的指令。这不是运行时的一个或另一个“成本计算周期”的问题;他们都在并行等待。(现代微处理器 90 分钟指南!https://agner.org/optimize/

但是当“周期”事件计数器溢出时,它必须选择一个特定的指令来“责备”,因为您使用的是统计采样。在这种情况下,一个有数百个 uops 的 CPU 必须发明一张不准确的现实图片。通常是等待缓慢输入的人受到指责,我认为因为它通常是 ROB 或 RS 中最古老的并且阻止前端分配新的 uops。

确切地选择哪条指令的细节可能会告诉我们一些关于 CPU 内部的信息,但只是非常间接的。可能与它如何退出 4(?) uop 组有关,并且此循环有 3 个,因此在发生 perf 事件异常时哪个 uop 最旧。

出于某种原因,4:1 拆分可能很重要,也许是因为 4+1 = 5 周期延迟,加载具有非简单寻址模式。(我假设这是一个 Intel Sandybridge 系列 CPU,也许是 Skylake 派生的?)就像也许如果数据在与 perf 事件溢出(并选择采样)的同一周期从缓存到达,mov则不会受到指责,因为它真的可以执行并让开吗?

IIRC、BeeOnRope 或其他人通过实验发现,Skylake CPU 倾向于让最旧的未退休指令在异常到达后退出,至少在它不是缓存未命中的情况下。在您的情况下,这将是cmp/jne循环底部的 ,它按程序顺序出现在下一次迭代顶部的加载之前。