英特尔的优化手册确实描述了SnB系列CPU中的L2空间预取器.是的,当第一条线路接入时有空闲内存带宽(非核心请求跟踪插槽),它会try 完成128B对齐的64B线路对.
微基准没有显示64字节与128字节间隔之间的任何显著时间差.在没有任何actual次错误共享(在同一个64字节行内)的情况下,经过一些初始的混乱之后,它很快就会达到一种状态,即每个核心都拥有它正在修改的缓存线的独占所有权.这意味着没有进一步的L1d未命中,因此没有对L2的请求会触发L2空间预取器.
与if不同,例如two pairs of threads contending over separate 100 variables in adjacent (or not) cache lines.或与他们虚假共享.然后L2空间预取可以将争用耦合在一起,因此所有4个线程都在相互争用,而不是两个独立的线程对.基本上,在任何情况下,缓存线实际上在核心之间来回反弹,如果不小心,二级空间预取可能会使情况变得更糟.
(The L2 prefetcher doesn't keep trying indefinitely来完成它缓存的每一条有效线的成对线;这会对这样的情况造成伤害,即不同的内核反复接触相邻的线,这比任何帮助都大.)
Understanding std::hardware_destructive_interference_size and std::hardware_constructive_interference_size包括一个较长基准的答案;我最近没有看过它,但我认为它应该演示64字节的 destruct 性干扰,而不是128字节.不幸的是,这里的答案没有提到二级空间预取是可能导致some destruct 性干扰的影响之一(尽管没有外部缓存中128字节的行大小那么大,尤其是如果它是包容性缓存的话).
性能计数器揭示了一个差异,即使与你的基准
There is more initial chaos that we can measure with performance counters for your benchmark.在我的i7-6700k(四核Skylake with Hyperreading;4c8t,运行Linux 5.16)上,我改进了源代码,这样我就可以在不 destruct 内存访问的情况下进行优化编译,并使用CPP宏,这样我就可以从编译器命令行设置步长(以字节为单位).请注意,当我们使用相邻的行时,大约500个内存顺序错误推测管道核(machine_clears.memory_ordering
).实际数量变化很大,从200到850,但对总时间的影响仍然可以忽略不计.
相邻线路,500+-300机器清除
$ g++ -DSIZE=64 -pthread -O2 false-share.cpp && perf stat --all-user -etask-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,uops_issued.any,uops_executed.thread,machine_clears.memory_ordering -r25 ./a.out
Performance counter stats for './a.out' (25 runs):
560.22 msec task-clock # 3.958 CPUs utilized ( +- 0.12% )
0 context-switches # 0.000 /sec
0 cpu-migrations # 0.000 /sec
126 page-faults # 224.752 /sec ( +- 0.35% )
2,180,391,747 cycles # 3.889 GHz ( +- 0.12% )
2,003,039,378 instructions # 0.92 insn per cycle ( +- 0.00% )
1,604,118,661 uops_issued.any # 2.861 G/sec ( +- 0.00% )
2,003,739,959 uops_executed.thread # 3.574 G/sec ( +- 0.00% )
494 machine_clears.memory_ordering # 881.172 /sec ( +- 9.00% )
0.141534 +- 0.000342 seconds time elapsed ( +- 0.24% )
与128字节分隔的vs相比,只有极少数机器清除
$ g++ -DSIZE=128 -pthread -O2 false-share.cpp && perf stat --all-user -etask-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,uops_issued.any,uops_executed.thread,machine_clears.memory_ordering -r25 ./a.out
Performance counter stats for './a.out' (25 runs):
560.01 msec task-clock # 3.957 CPUs utilized ( +- 0.13% )
0 context-switches # 0.000 /sec
0 cpu-migrations # 0.000 /sec
124 page-faults # 221.203 /sec ( +- 0.16% )
2,180,048,243 cycles # 3.889 GHz ( +- 0.13% )
2,003,038,553 instructions # 0.92 insn per cycle ( +- 0.00% )
1,604,084,990 uops_issued.any # 2.862 G/sec ( +- 0.00% )
2,003,707,895 uops_executed.thread # 3.574 G/sec ( +- 0.00% )
22 machine_clears.memory_ordering # 39.246 /sec ( +- 9.68% )
0.141506 +- 0.000342 seconds time elapsed ( +- 0.24% )
在这台4c8t机器上,Linux如何将线程调度到逻辑核心,这可能有一定的依赖性.相关的:
与一行内的实际错误共享相比:10万台机器清除
存储缓冲区(和存储转发) for each 错误的共享机器清除了一系列增量,所以它并不像人们预期的那么糟糕.(对于原子RMW,比如std::atomic<int>
fetch_add
,情况会糟糕得多,因 for each 增量在执行时都需要直接访问L1d缓存.)Why does false sharing still affect non atomics, but much less than atomics?
$ g++ -DSIZE=4 -pthread -O2 false-share.cpp && perf stat --all-user -etask-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,uops_issued.any,uops_executed.thread,machine_clears.memory_ordering -r25 ./a.out
Performance counter stats for './a.out' (25 runs):
809.98 msec task-clock # 3.835 CPUs utilized ( +- 0.42% )
0 context-switches # 0.000 /sec
0 cpu-migrations # 0.000 /sec
122 page-faults # 152.953 /sec ( +- 0.22% )
3,152,973,230 cycles # 3.953 GHz ( +- 0.42% )
2,003,038,681 instructions # 0.65 insn per cycle ( +- 0.00% )
2,868,628,070 uops_issued.any # 3.596 G/sec ( +- 0.41% )
2,934,059,729 uops_executed.thread # 3.678 G/sec ( +- 0.30% )
10,810,169 machine_clears.memory_ordering # 13.553 M/sec ( +- 0.90% )
0.21123 +- 0.00124 seconds time elapsed ( +- 0.59% )
改进的基准测试-调整数组,并允许优化
我使用了volatile
,这样我就可以进行优化.我假设您在编译时禁用了优化功能,因此int j
也在循环中存储/重新加载.
我用了alignas(128) counter[]
,所以我们可以确定数组的开头是两对128字节的行,而不是三行.
#include <thread>
alignas(128) volatile int counter[1024]{};
void update(int idx) {
for (int j = 0; j < 100000000; j++) ++counter[idx];
}
static const int stride = SIZE/sizeof(counter[0]);
int main() {
std::thread t1(update, 0*stride);
std::thread t2(update, 1*stride);
std::thread t3(update, 2*stride);
std::thread t4(update, 3*stride);
t1.join();
t2.join();
t3.join();
t4.join();
}