【发布时间】:2019-12-19 10:15:24
【问题描述】:
考虑这个简单的 C++ 函数来计算数组的prefix sum:
void prefix_sum(const uint32_t* input, uint32_t* output, size_t size) {
uint32_t total = 0;
for (size_t i = 0; i < size; i++) {
total += input[i];
output[i] = total;
}
}
循环 compiles 到 gcc 5.5 上的以下程序集:
.L5:
add ecx, DWORD PTR [rdi+rax*4]
mov DWORD PTR [rsi+rax*4], ecx
add rax, 1
cmp rdx, rax
jne .L5
我没有看到任何可以阻止它在每次迭代中运行 1 个周期的东西,但我始终在 Skylake i7-6700HQ 上以 1.32 (+/- 0.01) 周期/迭代来测量它,当它针对 8 KiB 运行时输入/输出数组。
循环在 uop 缓存之外提供服务,不会跨越任何 uop 缓存边界,性能计数器也不会指示任何前端瓶颈。
它是 4 fused uops1,这个 CPU 可以维持 4 fused ops/cycle。
有通过ecx和rax的依赖链,每个1个周期,但是这些add uop可以去4个ALU端口中的任何一个,所以看起来不太可能发生冲突。融合的cmp 需要转到 p6,这更令人担忧,但我只测量到 p6 的 1.1 uops/迭代。这可以解释每次迭代 1.1 个周期,但不是 1.4 个。如果我以 2 倍端口压力展开循环,则要低得多:所有 p0156 小于 0.7 微秒,但每次迭代 1.3 个周期时性能仍然出乎意料地慢。
每次迭代有一个存储,但我们可以每个周期进行一个存储。
每次迭代有一个负载,但我们每个周期可以做两个。
每个周期有两个复杂的 AGU,但我们每个周期可以做两个。
这里的瓶颈是什么?
有趣的是,我尝试了 Ithermal performance predictor,它几乎完全正确:估计 1.314 个周期与我测量的 1.32 个周期。
1 我通过 uops_issued.any 计数器确认了宏和微融合融合,该计数器在融合域中计数,并在此循环中每次迭代读取 4.0 个融合微指令。
【问题讨论】:
-
您是否检查了 4k 混叠?如果你有一个方便的 MCVE 调用程序,我会在我的桌面上测试运行它。
-
@PeterCordes 我检查了
ld_blocks_partial.address_alias报告的数字很低,并且不会随着问题的大小而增加。两个阵列都与 2 MiB 对齐。是的,我应该提供一个 MCVE,但是由于当前的基准测试分布在十几个文件中,所以这有点工作,但我会在某个时候完成它。 -
@HadiBrais:在 27 亿个循环中,我得到了
CYCLE_ACTIVITY.STALLS_MEM_ANY:u的 250 万个计数。所以它不高但非零。 (不限于用户空间,大约为 4.2M)。但是resource_stalls.sb:u大约是 70k 到 90k 并且嘈杂,降低了约 30 倍。所以商店瓶颈可能只是噪音。 -
我想知道是否存在某种寄存器读取限制。例如agner.org/optimize/blog/read.php?i=415#857 还展示了读取更多寄存器(或使用复杂寻址模式?)会减慢 Skylake。因此,我的更改带来的加速可能是由于从循环条件中消除了一个寄存器。
-
我注意到每次迭代的 p4 计数高于 1 并且接近于循环/迭代,即可以解释大部分性能差异。例如,原始版本的展开版本以 1.26 个周期/迭代运行,并显示 1.25 uops/迭代到 p4。表示可能因为它们的操作数没有准备好而正在重放存储?不过,这更有可能是症状而不是原因。
标签: c++ optimization x86 intel micro-optimization