首页 > 解决方案 > C/C++:轻松的 std::atomicvs X64 架构上的未锁定布尔值

问题描述

std::atomic<bool>使用未锁定的布尔值是否比使用始终以宽松的内存顺序完成操作的地方有任何效率优势?我假设两者最终都编译为相同的机器代码,因为单个字节实际上在 X64 硬件上是原子的。我错了吗?

标签: c++performancesynchronizationx86-64atomic

解决方案


是的,有潜在的巨大优势,特别是对于局部变量或在同一函数中重复使用的任何变量。 变量不能优化为寄存器atomic<>

如果您在没有优化的情况下编译,代码生成将是相似的,但在启用正常优化的情况下编译可能会有很大差异。 未优化的代码类似于制作每个变量volatile


当前的编译器也从不将一个atomic变量的多次读取合并为一个,就像您使用过一样volatile atomic<T>,因为这是人们所期望的,并且尚未解决如何允许有用的优化同时禁止您想要的优化。(为什么编译器不合并冗余的 std::atomic 写入?并且编译器可以优化两个原子负载吗?)。

这不是一个很好的例子,但想象一下检查布尔值是在一个内联函数中完成的,并且循环中还有其他东西。(否则你会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;
}

请参阅 Godbolt 上的 asm 输出

但是对于 non-atomic bool,编译器可以通过提升负载为您进行转换,然后自动矢量化简单的 sum 循环(或根本不运行它)。

atomic_bool它不能。使用 atomic_bool,asm 循环很像 C++ 源代码,实际上在每次循环迭代中对变量的值进行测试和分支。这当然会破坏自动矢量化。

(C++ as-if 规则将允许编译器提升负载,因为它是放松的,因此它可以使用非原子访问重新排序。并且合并是因为每次读取相同的值是读取一个值的全局顺序的一种可能结果。但正如我所说,编译器不会那样做。)


bool循环遍历can 自动矢量化的数组,但不能遍历atomic<bool> [].


此外,用类似的东西反转布尔值b ^= 1;orb++可以只是一个常规 RMW,而不是原子 RMW,所以它不必使用lock xoror 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 周期循环携带的依赖链(在具有 5 周期存储的 Intel CPU 上)转发延迟)而不是通过寄存器的 1 个周期的 dep 链。


推荐阅读