我有一个执行 pointer chasing 的程序,我正在尝试尽可能地优化指针追踪循环。
我注意到 perf record
检测到函数 myFunction()
中约 20% 的执行时间用于执行跳转指令(用于在读取特定值后退出循环)。
注意事项:
__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
...
我希望在这个循环中花费的大部分周期都用于从内存中读取值,这是由 perf 确认的。 我还希望剩余的周期在执行循环中剩余的指令时会有些均匀地花费。相反, perf 报告的是剩余周期的很大一部分用于执行跳转。
我怀疑通过了解用于执行这些指令的微操作,我可以更好地了解这些成本,但我有点不知从何开始。
答案 0 :(得分:5)
请记住,cycles
事件必须选择要归咎的指令,即使 mov
-load 和宏融合的 cmp
-and-branch uops 都在等待结果.这不是运行时的一个或另一个“成本计算周期”的问题; 他们都在并行等待。 (Modern Microprocessors
A 90-Minute Guide! 和 https://agner.org/optimize/)
但是当“cycles”事件计数器溢出时,它必须选择一个特定的指令来“责备”,因为您使用的是统计采样。在这种情况下,一个有数百个 uops 的 CPU 必须发明一张不准确的现实图片。通常是等待缓慢输入的人受到指责,我认为是因为它通常是 ROB 或 RS 中最老的并且阻止前端分配新的 uop。
具体选择哪条指令的细节可能会告诉我们一些关于 CPU 内部的信息,但只是非常间接的。可能与它如何退出 4(?) uop 组有关,并且此循环有 3 个,因此在发生 perf 事件异常时哪个 uop 最旧。
出于某种原因,4:1 拆分可能很重要,也许是因为 4+1 = 5 周期延迟,加载具有非简单寻址模式。 (我假设这是一个 Intel Sandybridge 系列 CPU,也许是 Skylake 派生的?)就像也许如果数据在与 perf 事件溢出(并选择采样)的同一周期从缓存到达,mov
不会受到指责是因为它实际上可以执行并让开?
IIRC、BeeOnRope 或其他人通过实验发现,Skylake CPU 倾向于让最旧的未退休指令在异常到达后退出,至少在它不是缓存未命中的情况下。在您的情况下,这将是循环底部的 cmp/jne
,它按程序顺序出现在下一次迭代顶部的加载之前。