为何AMD CPU上原子操作后的附加指令能提升执行速度?
原子操作基准测试中的异常提速问题
我编写了以下C语言测试用例,对不同内存操作和计算操作进行性能基准测试:
#define BENCH_ROUNDS 1000000000 // 10**9 static volatile UINT64 _test_argument, _test_result; static _Atomic(UINT64) _test_atom; // 简化循环写法 #define bench_loop(var) for (UINT64 var = 0; var < BENCH_ROUNDS; var++) // 空循环参考 VOID test_empty(VOID) { bench_loop (i); } // 寄存器内计算参考 VOID test_calc(VOID) { UINT64 res = _test_argument; bench_loop (i) { res += i; } _test_result = res; } // 单纯内存存储 VOID test_store(VOID) { bench_loop (i) { _test_result = i; } } // 内存读写修改 VOID test_modify(VOID) { _test_result = _test_argument; bench_loop (i) { _test_result += i; } } // 内存修改+寄存器计算 VOID test_modify__calc(VOID) { _test_result = _test_argument; UINT64 res = _test_argument; bench_loop (i) { _test_result += i; res += i; } _test_result = res; } // 原子内存修改 VOID test_atom_modify(VOID) { bench_loop (i) { _test_atom += i; } } // 原子修改+寄存器计算 VOID test_atom_modify__calc(VOID) { UINT64 res = _test_argument; bench_loop (i) { _test_atom += i; res += i; } _test_result = res; } // 原子修改+更多寄存器计算 VOID test_atom_modify__calc2(VOID) { UINT64 res = _test_argument; bench_loop (i) { _test_atom += i; res += i, res ^= i; res += i, res ^= i; res += i, res ^= i; res += i, res ^= i; } _test_result = res; }
测试环境与结果
测试通过QueryPerformanceCounter统计执行时间,使用gcc -Wall -Og -m64编译(该选项不会消除可预测循环)。
AMD Ryzen 7 4800H @4.3GHz 测试结果
000233.787ms test_empty 000467.274ms test_calc 000233.846ms test_store 001869.585ms test_modify 001869.459ms test_modify__calc 004102.050ms test_atom_modify 004102.818ms test_atom_modify__calc 003928.742ms test_atom_modify__calc2
异常现象
test_atom_modify__calc2的执行时间比其他test_atom_modify系列测试快150~200ms(平均每次迭代不到1个时钟周期),且多次测试结果一致。但类似逻辑的test_modify__calc2(非原子操作版本)并未出现提速。
Intel Xeon E5-2643 v3 对比测试结果
循环次数降至100万次,测试间添加休眠固定CPU频率,结果无异常提速:
000000.835ms test_empty 000000.836ms test_calc 000000.836ms test_store 000006.264ms test_modify 000006.264ms test_modify__calc 000015.926ms test_atom_modify 000015.873ms test_atom_modify__calc 000015.900ms test_atom_modify__calc2
关键循环汇编代码对比
# test_modify: jmp .L11 .L12: movq _test_result(%rip), %rdx addq %rax, %rdx movq %rdx, _test_result(%rip) addq $1, %rax .L11: cmpq $999999999, %rax jbe .L12 # test_atom_modify: jmp .L20 .L21: lock addq %rax, _test_atom(%rip) addq $1, %rax .L20: cmpq $999999999, %rax jbe .L21 # test_atom_modify__calc: jmp .L23 .L24: lock addq %rax, _test_atom(%rip) addq %rax, %rdx addq $1, %rax .L23: cmpq $999999999, %rax jbe .L24 # test_atom_modify__calc2: jmp .L26 .L27: lock addq %rdx, _test_atom(%rip) addq %rdx, %rax xorq %rdx, %rax addq %rdx, %rax xorq %rdx, %rax addq %rdx, %rax xorq %rdx, %rax addq %rdx, %rax xorq %rdx, %rax addq $1, %rdx .L26: cmpq $999999999, %rdx jbe .L27
计时包装程序
#include <stdio.h> #include <stdatomic.h> #include <Windows.h> /* 测试代码粘贴在此处 */ static VOID bench_time(VOID (*func)(VOID)) { UINT64 freq, st, ed; QueryPerformanceFrequency((LARGE_INTEGER*) &freq); QueryPerformanceCounter((LARGE_INTEGER*) &st); (*func)(); QueryPerformanceCounter((LARGE_INTEGER*) &ed); UINT64 t = ed - st; printf("%010.3lfms\n", t * 1000.0 / freq); } INT main() { _test_argument = GetCurrentProcessId(); atomic_init(&_test_atom, 0); bench_time(&test_empty); bench_time(&test_calc); bench_time(&test_store); bench_time(&test_modify); bench_time(&test_modify__calc); // bench_time(&test_modify__calc2); bench_time(&test_atom_modify); bench_time(&test_atom_modify__calc); bench_time(&test_atom_modify__calc2); return 0; }
原因分析
从汇编代码和CPU架构特性可以解释这一差异:
- 指令级并行与延迟隐藏:
lock addq是总线锁定的原子操作,在AMD Zen2架构(Ryzen 7 4800H基于该架构)上,这类操作会产生较长的内存延迟。test_atom_modify__calc2中,lock addq之后紧跟了多组寄存器级的addq/xorq操作——这些操作完全在CPU寄存器内执行,无需等待内存响应,可以和lock addq的内存延迟重叠,从而隐藏部分等待时间。而其他原子测试中,lock addq之后仅跟随addq $1, %rax这类简单指令,可重叠的指令少,无法有效隐藏延迟。 - 寄存器分配差异:
test_atom_modify__calc2使用rdx作为循环变量和原子操作的源操作数,rax用于寄存器计算;而其他原子测试使用rax同时承担循环变量和原子操作数。这种寄存器分配方式让CPU的指令流水线调度更高效,减少了寄存器依赖冲突。 - Intel架构的特性差异:Intel Xeon E5-2643 v3基于Haswell架构,其原子操作的延迟特性或流水线调度逻辑与Zen2不同,无法通过后续寄存器操作有效隐藏
lock add的延迟,因此未出现提速现象。
内容的提问来源于stack exchange,提问作者Wilderness Ranger
相关产品推荐
相关产品推荐

