C/C++:放松 std::atomic<bool> 与 X64 架构上的解锁 bool
C/C++: relaxed std::atomic<bool> vs unlocked bool on X64 architecture
与使用 std::atomic<bool>
相比,使用解锁的布尔值是否有任何效率优势,其中操作总是以宽松的内存顺序完成?我假设两者最终都编译为相同的机器代码,因为单个字节在 X64 硬件上实际上是原子的。我错了吗?
在 Godbolt 检查,加载常规 bool
和 std::atomic<bool>
生成不同的代码,尽管不是因为同步问题。相反,编译器 (gcc) 似乎不愿意假设 std::atomic<bool>
保证为 0 或 1。奇怪,那个。
Clang 做同样的事情,尽管生成的代码在细节上略有不同。
是的,有潜在的巨大优势,特别是对于局部变量,或在同一函数中重复使用的任何变量。 无法将atomic<>
变量优化为寄存器。
如果你在没有优化的情况下编译,代码生成会很相似,但是在启用正常优化的情况下编译可能会有很大的不同。未优化的代码类似于制作每个变量 volatile
.
当前的编译器也永远不会将对 atomic
变量的多次读取合并为一个,就好像您使用了 volatile atomic<T>
一样,因为这是人们所期望的,而且尘埃尚未落定允许进行有用的优化,同时禁止您 不 想要的优化。 ( and ).
这不是一个很好的例子,但想象一下,检查布尔值是在内联函数中完成的,并且循环中还有其他东西。 (否则你会像正常人一样将 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)
(单独的原子loads/stores),你仍然会在循环中得到一个store/reload,通过创建一个6周期循环携带的依赖链store/reload(在具有 5 周期存储转发延迟的 Intel CPU 上)而不是通过寄存器的 1 周期 dep 链。
与使用 std::atomic<bool>
相比,使用解锁的布尔值是否有任何效率优势,其中操作总是以宽松的内存顺序完成?我假设两者最终都编译为相同的机器代码,因为单个字节在 X64 硬件上实际上是原子的。我错了吗?
在 Godbolt 检查,加载常规 bool
和 std::atomic<bool>
生成不同的代码,尽管不是因为同步问题。相反,编译器 (gcc) 似乎不愿意假设 std::atomic<bool>
保证为 0 或 1。奇怪,那个。
Clang 做同样的事情,尽管生成的代码在细节上略有不同。
是的,有潜在的巨大优势,特别是对于局部变量,或在同一函数中重复使用的任何变量。 无法将atomic<>
变量优化为寄存器。
如果你在没有优化的情况下编译,代码生成会很相似,但是在启用正常优化的情况下编译可能会有很大的不同。未优化的代码类似于制作每个变量 volatile
.
当前的编译器也永远不会将对 atomic
变量的多次读取合并为一个,就好像您使用了 volatile atomic<T>
一样,因为这是人们所期望的,而且尘埃尚未落定允许进行有用的优化,同时禁止您 不 想要的优化。 (
这不是一个很好的例子,但想象一下,检查布尔值是在内联函数中完成的,并且循环中还有其他东西。 (否则你会像正常人一样将 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)
(单独的原子loads/stores),你仍然会在循环中得到一个store/reload,通过创建一个6周期循环携带的依赖链store/reload(在具有 5 周期存储转发延迟的 Intel CPU 上)而不是通过寄存器的 1 周期 dep 链。