L1D缓存冲突测试:GCC与Clang中DoNotOptimize和volatile的行为差异及原因问询
L1D缓存冲突测试:GCC与Clang中DoNotOptimize和volatile的行为差异及原因问询
我现在在x86机器上用Google Benchmark测试L1数据缓存的冲突情况,遇到了一个困惑的问题,想请教大家:
测试代码
void bmRead(benchmark::State& state) { struct alignas(64) Line { char x[64]; Line() {} }; constexpr int sz = 1024; Line* lines = new Line[sz]; int ind = 0; int steps = state.range(0); while (state.KeepRunning()) { for (int i = 0; i < 10; i++) { ind += steps; ind %= sz; /*volatile*/ char val = lines[ind].x[0]; benchmark::DoNotOptimize(val); benchmark::ClobberMemory(); } } } BENCHMARK(bmRead)->RangeMultiplier(2)->Range(1, 512);
预期行为
我的思路是触发L1缓存组冲突。这台CPU的L1D是VIPT结构(地址位[6–11]选择缓存组),8路组相联,所以每个缓存组最多只能同时容纳8个缓存行:
- 当测试
bmRead/64时,我们每隔64个缓存行访问一次→所有行都映射到同一个缓存组。工作集大小是1024/64=16个行,超过了8路的相联度,应该会触发缓存行驱逐,所以这个测试的耗时应该比其他情况明显更长。 - 而
bmRead/128的工作集只涉及8个缓存行(刚好能放进一个组),所以应该能正常高效执行。
测试结果
带volatile时的结果(符合预期)
首先是CPU缓存信息:
CPU Caches:
L1 Data 32 KiB (x8)
L1 Instruction 32 KiB (x8)
L2 Unified 512 KiB (x8)
L3 Unified 16384 KiB (x1)
Load Average: 0.29, 0.35, 0.19
基准测试结果:
----------------------------------------------------- Benchmark Time CPU Iterations ----------------------------------------------------- bmRead/1 15.5 ns 15.5 ns 45195244 bmRead/2 15.5 ns 15.5 ns 45187565 bmRead/4 15.5 ns 15.5 ns 45156433 bmRead/8 15.5 ns 15.5 ns 45107741 bmRead/16 15.6 ns 15.5 ns 45115926 bmRead/32 15.5 ns 15.5 ns 45614777 bmRead/64 25.2 ns 25.2 ns 26902942 bmRead/128 18.3 ns 18.3 ns 38179996 bmRead/256 15.6 ns 15.6 ns 44976974 bmRead/512 15.5 ns 15.5 ns 45184535
可以看到bmRead/64的耗时明显更高,符合预期;bmRead/128表现正常。
去掉volatile仅依赖DoNotOptimize(val)的结果(不符合预期)
所有测试的耗时几乎完全一致,缓存冲突的影响消失了:
----------------------------------------------------- Benchmark Time CPU Iterations ----------------------------------------------------- bmRead/1 14.2 ns 14.2 ns 48847532 bmRead/2 14.4 ns 14.4 ns 49097537 bmRead/4 14.4 ns 14.4 ns 48728445 bmRead/8 14.4 ns 14.4 ns 48684336 bmRead/16 14.6 ns 14.6 ns 48684349 bmRead/32 14.5 ns 14.5 ns 47601341 bmRead/64 14.4 ns 14.4 ns 48734250 bmRead/128 14.4 ns 14.4 ns 48585062 bmRead/256 14.4 ns 14.4 ns 48563453 bmRead/512 14.8 ns 14.8 ns 48398019
看起来加载操作被编译器完全优化掉了,我原本以为DoNotOptimize(val)能强制编译器保留这个加载操作,但实际并非如此。
反汇编分析
我查看了生成的汇编代码:
- 用GCC编译且不带
volatile时,循环里完全没有从lines[ind].x[0]加载数据的指令,相关片段如下:
0x55555555fa20 add %r12d,%ebx 0x55555555fa23 mov %ebx,%eax 0x55555555fa25 sar $0x1f,%eax 0x55555555fa28 shr $0x16,%eax 0x55555555fa2b add %eax,%ebx 0x55555555fa2d and $0x3ff,%ebx 0x55555555fa33 sub %eax,%ebx 0x55555555fa35 movslq %ebx,%rax 0x55555555fa38 shl $0x6,%rax 0x55555555fa3c dec %edx 0x55555555fa3e jne 0x55555555fa20
- 而用Clang编译时,即使不带
volatile,生成的机器码里也存在这个加载操作,测试结果也符合预期。
核心疑问
- 为什么GCC会把加载操作去掉?我以为
DoNotOptimize(val)应该能强制编译器保留这个内存读取啊? - 为什么Clang能正确保留加载并表现符合预期,而GCC不行?
- 是不是我对
DoNotOptimize的理解有错误?
附加信息
- 我试过在基准测试循环前给
Line* lines = new Line[sz];填充随机值,但对测试结果没有影响。 - 编译器版本:GCC 13.3.0,Clang 18.1.3
- 编译命令:
g++ -O3 -march=native -g file.cpp -lbenchmark -lpfm && ./a.out --benchmark_filter=Read DoNotOptimize的实现代码:
template <class Tp> inline BENCHMARK_ALWAYS_INLINE void DoNotOptimize(Tp& value) { #if defined(__clang__) asm volatile("" : "+r,m"(value) : : "memory"); #else asm volatile("" : "+m,r"(value) : : "memory"); #endif }
内容来源于stack exchange
相关产品推荐
相关产品推荐

