是的,这具有潜在的巨大优势,尤其是对于局部变量或在同一函数中重复使用的任何变量。 atomic<> 变量无法优化为寄存器。
如果你在没有优化的情况下编译,代码生成会相似,但在启用正常优化的情况下编译可能会有很大差异。 未优化的代码类似于使每个变量volatile。
当前的编译器也从未将atomic 变量的多次读取合并为一个,就像您使用过volatile atomic<T> 一样,因为这是人们所期望的,而关于如何允许有用的优化同时禁止你不想要的。 (Why don't compilers merge redundant std::atomic writes? 和 Can and does the compiler optimize out two atomic loads?)。
这不是一个很好的例子,但想象一下检查布尔值是在一个内联函数内完成的,并且循环内还有其他东西。 (否则你会像普通人一样将if 放在循环中。)
int sumarr_atomic(int arr[]) {
int sum = 0;
for(int i=0 ; i<10000 ; i++) {
if (atomic_bool.load (std::memory_order_relaxed)) {
sum += arr[i];
}
}
return sum;
}
See the asm output on Godbolt.
但是对于非原子bool,编译器可以通过提升负载为您进行转换,然后自动矢量化简单的求和循环(或根本不运行它)。
atomic_bool 不能。使用 atomic_bool,asm 循环很像 C++ 源代码,实际上在每次循环迭代中对变量的值进行测试和分支。这当然会破坏自动矢量化。
(C++ as-if 规则将允许编译器提升负载,因为它是放松的,因此它可以使用非原子访问重新排序。并合并,因为每次读取相同的值是读取的全局顺序的一个可能结果一个值。但正如我所说,编译器不会这样做。)
在bool 的数组上循环可以自动矢量化,但不能在atomic<bool> [] 上循环。
此外,用 b ^= 1; 或 b++ 之类的东西反转布尔值可以只是常规 RMW,而不是原子 RMW,因此不必使用 lock xor 或 lock btc。 (x86 原子 RMW 仅适用于顺序一致性与运行时重新排序,即 lock 前缀也是一个完整的内存屏障。)
修改非原子布尔值的代码可以优化掉实际的修改,例如
void loop() {
for(int i=0 ; i<10000 ; i++) {
regular_bool ^= 1;
}
}
编译为将regular_bool 保存在寄存器中的asm。不幸的是,它并没有优化到什么都没有(这可能是因为将布尔值翻转偶数次会将其恢复为原始值)。但它可以使用更智能的编译器。
loop():
movzx edx, BYTE PTR regular_bool[rip] # load into a register
mov eax, 10000
.L17: # do {
xor edx, 1 # flip the boolean
sub eax, 1
jne .L17 # } while(--i);
mov BYTE PTR regular_bool[rip], dl # store back the result
ret
即使写成atomic_b.store( !atomic_b.load(mo_relaxed), mo_relaxed)(单独的原子加载/存储),您仍然会在循环中获得存储/重新加载,通过存储/重新加载创建一个 6 周期循环承载的依赖链(在 Intel CPU 上)具有 5 个周期的存储转发延迟)而不是通过寄存器的 1 个周期的深度链。