考虑这个计算一个数组的前缀和的简单C++函数:
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;
}
}
在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上对8 KiB输入/输出数组运行时,我一直测量到1.32(+/- 0.01)个周期/迭代。
该循环从uop缓存中提供服务,并且不跨越任何uop缓存边界,性能计数器也没有指示任何前端瓶颈。
它是4个融合的uops1,而此CPU可以维持每个周期的4个融合操作。
通过ecx
和rax
有1个周期的依赖链传递,但是这些add
uops可以发送到4个ALU端口中的任何一个,因此似乎不太可能发生冲突。融合cmp
需要发送到p6,这更令人担忧,但是我测量到只有1.1个uops/迭代发送到p6。这可以解释每个迭代使用1.1个周期,但不能解释1.4个周期。如果我将循环展开2倍,则端口压力要低得多:所有p0156的uops都小于0.7,但性能仍然意外地慢,每个迭代的周期为1.3个。
每次迭代有一个存储器操作,但我们可以每个周期进行一次存储操作。
每次迭代有一个加载操作,但我们可以每个周期执行两个这样的操作。
每个周期有两个复杂的AGU,但我们可以每个周期执行两个这样的操作。
问题出在哪里?
有趣的是,我尝试了Ithermal性能预测器,它几乎完全正确:估计1.314个周期,而我测量到1.32个周期。
1 我通过uops_issued.any
计数器确认了宏观和微观融合,该计数器在融合域中计算,并且对于此循环每次迭代读取4.0个融合uops。
ld_blocks_partial.address_alias
,它报告了一个低数字并且不随问题大小而增加。 两个数组都对齐到2 MiB。 是的,我应该提供MCVE,但这需要一些工作,因为当前的基准测试分布在十几个文件中,但我会在某个时候完成它。 - BeeOnRopeCYCLE_ACTIVITY.STALLS_MEM_ANY:u
的250万计数,而总周期数为27亿。虽然不高,但不为零。(如果不仅限于用户空间,大约为420万)。但是resource_stalls.sb:u
大约为70k到90k,并且嘈杂,低了约30倍。所以存储瓶颈可能只是噪声。 - Peter Cordes