为什么这个跳转指令在执行指针追逐时如此昂贵?
Why is this jump instruction so expensive when performing pointer chasing?
我有一个执行 pointer chasing 的程序,我正在尝试尽可能优化指针追踪循环。
我注意到 perf record
检测到函数 myFunction()
中约 20% 的执行时间用于执行跳转指令(用于在读取特定值后退出循环)。
一些注意事项:
- 指针追逐路径可以轻松放入 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 [=10=]x1,%rax
│ cmp [=10=]xffffffff,%ebx
19.84 │ ↑ jne 20
...
我预计此循环中花费的大部分周期用于从内存中读取值,这已由 perf 确认。
我还希望剩余的周期能够均匀地用于执行循环中的剩余指令。相反,perf 报告剩余周期中有很大一部分用于执行跳跃。
我怀疑我可以通过理解用于执行这些指令的微操作来更好地理解这些成本,但我有点不知从何入手。
记住 cycles
事件必须选择一个指令来指责,即使 mov
-load 和宏融合 cmp
-and-branch 微指令都在等待结果。这不是一个或另一个“成本计算周期”的问题,而它是 运行; 他们都在并行等待。 (现代微处理器
90 分钟指南! 和 https://agner.org/optimize/)
但是当“周期”事件计数器溢出时,它必须选择一个特定的指令来“责备”,因为您使用的是统计抽样。这是一个不准确的现实画面必须由一个有数百个微指令的 CPU 发明的地方。通常是等待缓慢输入的那个被指责,我认为因为它通常是 ROB 或 RS 中最旧的并且阻止前端分配新的 uops。
具体选择哪条指令的细节可能会告诉我们一些关于 CPU 内部结构的信息,但只是非常间接的。可能与它如何退出 4(?) 微指令组有关,而这个循环有 3 个微指令,所以当出现性能事件异常时,哪个微指令最旧。
4:1 拆分可能出于某种原因很重要,可能是因为具有非简单寻址模式的负载有 4+1 = 5 个周期延迟。 (我假设这是一个 Intel Sandybridge 系列 CPU,也许是 Skylake 派生的?)就像如果数据在与性能事件溢出(并选择采样)相同的周期从缓存到达,mov
不挨骂是因为它真的可以执行并让开?
IIRC、BeeOnRope 或其他人通过实验发现 Skylake CPUs 倾向于让最旧的未退休指令在异常到达后退休,至少在它不是缓存未命中的情况下。在您的情况下,这将是循环底部的 cmp/jne
,它按程序顺序出现在下一次迭代顶部的加载之前。
我有一个执行 pointer chasing 的程序,我正在尝试尽可能优化指针追踪循环。
我注意到 perf record
检测到函数 myFunction()
中约 20% 的执行时间用于执行跳转指令(用于在读取特定值后退出循环)。
一些注意事项:
- 指针追逐路径可以轻松放入 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 [=10=]x1,%rax
│ cmp [=10=]xffffffff,%ebx
19.84 │ ↑ jne 20
...
我预计此循环中花费的大部分周期用于从内存中读取值,这已由 perf 确认。 我还希望剩余的周期能够均匀地用于执行循环中的剩余指令。相反,perf 报告剩余周期中有很大一部分用于执行跳跃。
我怀疑我可以通过理解用于执行这些指令的微操作来更好地理解这些成本,但我有点不知从何入手。
记住 cycles
事件必须选择一个指令来指责,即使 mov
-load 和宏融合 cmp
-and-branch 微指令都在等待结果。这不是一个或另一个“成本计算周期”的问题,而它是 运行; 他们都在并行等待。 (现代微处理器
90 分钟指南! 和 https://agner.org/optimize/)
但是当“周期”事件计数器溢出时,它必须选择一个特定的指令来“责备”,因为您使用的是统计抽样。这是一个不准确的现实画面必须由一个有数百个微指令的 CPU 发明的地方。通常是等待缓慢输入的那个被指责,我认为因为它通常是 ROB 或 RS 中最旧的并且阻止前端分配新的 uops。
具体选择哪条指令的细节可能会告诉我们一些关于 CPU 内部结构的信息,但只是非常间接的。可能与它如何退出 4(?) 微指令组有关,而这个循环有 3 个微指令,所以当出现性能事件异常时,哪个微指令最旧。
4:1 拆分可能出于某种原因很重要,可能是因为具有非简单寻址模式的负载有 4+1 = 5 个周期延迟。 (我假设这是一个 Intel Sandybridge 系列 CPU,也许是 Skylake 派生的?)就像如果数据在与性能事件溢出(并选择采样)相同的周期从缓存到达,mov
不挨骂是因为它真的可以执行并让开?
IIRC、BeeOnRope 或其他人通过实验发现 Skylake CPUs 倾向于让最旧的未退休指令在异常到达后退休,至少在它不是缓存未命中的情况下。在您的情况下,这将是循环底部的 cmp/jne
,它按程序顺序出现在下一次迭代顶部的加载之前。