您已经发现了导致直方图出现瓶颈的影响之一。该问题的解决方法是保留多个计数器数组并循环遍历它们,因此同一索引的重复运行会分布在内存中的 2 或 4 个不同计数器上。
(然后循环计数器数组,将它们汇总为最终一组计数。这部分可以从 SIMD 中受益。)
情况 1 很快,因为现代 CPU 知道我们正在读/写相同的内存位置,从而缓冲操作
不,这不是CPU,而是编译时优化。
++*pointer[0]
速度很快,因为编译器可以将存储/重新加载提升到循环之外,实际上只是增加一个寄存器。 (如果您不使用结果,它甚至可能会优化掉。)
假设没有数据争用 UB 让编译器假设没有其他东西正在修改pointer[0]
所以每次都会增加同一个对象。假设规则让它保持*pointer[0]
在寄存器中而不是实际执行内存目标增量。
所以这意味着增量有 1 个周期的延迟,当然它可以将多个增量合并为一个并执行*pointer[0] += n
如果它完全展开并优化循环。
当我们通过指针 a 写入内存位置,然后尝试通过指针 b 读取它时,我们必须等待写入完成。这会停止超标量执行。
是的,通过该内存位置的数据依赖性就是问题所在。在编译时不知道指针都指向同一个位置的情况下,编译器将使 asm 实际上增加指向的内存位置。
不过,“等待写入完成”并不严格准确。 CPU 有一个存储缓冲区,用于将存储执行与高速缓存未命中解耦,以及将无序推测执行与实际提交到 L1d 并对其他内核可见的存储解耦。重新加载最近存储的数据不必等待其提交到缓存;存储转发一旦 CPU 检测到它,从存储缓冲区到重新加载就会发生。
在现代 Intel CPU 上,存储转发延迟约为 5 个周期,因此内存目标添加具有 6 个周期延迟。 (1 表示添加,5 表示存储/重新加载(如果位于关键路径上)。)
是的,乱序执行可以让这两个 6 周期延迟依赖链并行运行。循环开销也被 OoO exec 隐藏在延迟之下。
Related:
-
x86 处理器中的存储到加载转发和内存消歧 http://blog.stuffedcow.net/2014/01/x86-memory-disambiguation/在 stuffedcow.net 上
- 存储转发地址与数据:英特尔优化指南中 STD 和 STA 之间有什么区别? https://stackoverflow.com/questions/47701898/store-forwarding-address-vs-data-what-the-difference-between-std-and-sta-in-the
- 在未对齐的内存访问情况下,存储到加载转发如何发生? https://stackoverflow.com/questions/42210733/how-does-store-to-load-forwarding-happens-in-case-of-unaligned-memory-access
- IvyBridge 上指针追逐循环中附近的依赖存储对性能产生奇怪的影响。添加额外的负载会加快速度吗? https://stackoverflow.com/questions/54084992/weird-performance-effects-from-nearby-dependent-stores-in-a-pointer-chasing-loop
-
为什么一个进程共享同一个HT核心时,另一个进程的执行时间会更短 https://stackoverflow.com/questions/58106459/why-is-execution-time-of-a-process-shorter-when-another-process-shares-the-same(在 Sandybridge 系列上,存储转发延迟可以是reduced如果您不尝试立即重新加载。)
有什么方法可以在不改变指针数组的情况下使情况3更快吗?
是的,如果这种情况是预料之中的,也许branch on it:
int *current_pointer = pointer[0];
int repeats = 1;
...
loop {
if (pointer[i] == current_pointer) {
repeats++;
} else {
*current_pointer += repeats;
current_pointer = pointer[i];
repeats = 1;
}
}
我们优化通过计算重复同一指针的游程长度.
这被案例2彻底打败了如果长跑的话会表现不佳not common.
短运行可以被乱序执行隐藏;只有当 dep 链变得足够长以填充 ROB(重新排序缓冲区)时,我们才会真正停止。