【问题标题】:C/C++: relaxed std::atomic<bool> vs unlocked bool on X64 architectureC/C++:放松的 std::atomic<bool> 与 X64 架构上的未锁定 bool
【发布时间】:2018-11-11 18:10:43
【问题描述】:

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

【问题讨论】:

  • “因为单个字节在硬件中实际上是原子的” - 这不是一个既定的事实。
  • 甚至不在 X64 架构上? (注意我在标题中写的)
  • @JesperJuhl:我怀疑是否存在字节加载或存储不是原子的架构。 (除了罕见的 ISA,如早期的 DEC Alpha,它们没有字节加载/存储指令,只有字。或字可寻址 DSP。但在它们上,bool 将是一个字宽,而不是字节。)

标签: c++ performance synchronization x86-64 atomic


【解决方案1】:

是的,这具有潜在的巨大优势,尤其是对于局部变量或在同一函数中重复使用的任何变量。 atomic&lt;&gt; 变量无法优化为寄存器。

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


当前的编译器也从未将atomic 变量的多次读取合并为一个,就像您使用过volatile atomic&lt;T&gt; 一样,因为这是人们所期望的,而关于如何允许有用的优化同时禁止你想要的。 (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&lt;bool&gt; [] 上循环。


此外,用 b ^= 1;b++ 之类的东西反转布尔值可以只是常规 RMW,而不是原子 RMW,因此不必使用 lock xorlock 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 个周期的深度链。

【讨论】:

    【解决方案2】:

    检查Godbolt,加载常规boolstd::atomic&lt;bool&gt; 会生成不同的代码,尽管不是因为同步问题。相反,编译器 (gcc) 似乎不愿意假设 std::atomic&lt;bool&gt; 保证为 0 或 1。奇怪,那。

    Clang 做同样的事情,虽然生成的代码在细节上略有不同。

    【讨论】:

    • 使用cout &lt;&lt; 会使代码非常混乱。 godbolt.org/z/hFEQ5f 使用返回全局值的单独函数更易于阅读,例如编译为单个 movzx 的 bool load_regular() { return regular_bool; }。 (并且原子版本仍然没有明显的原因进行布尔化。)
    • @Peter 我这样做是为了阻止编译器优化负载。尽管我从您的示例中看到将负载移到单独的函数中会生成更好的代码。
    • 是的,我知道,我的意思是从函数返回值而不是编写 main 可以更干净地解决相同的问题。见How to remove "noise" from GCC/clang assembly output?。请记住,您只是在编写代码,因此您可以查看 asm,而不是运行它。
    • @Peter Ah,我看到你从不费心调用函数,这样 gcc 就无法内联它们或优化它们。一个有用的技巧。
    • 即使您确实编写了调用者,您仍然可以查看独立定义以及,如果您不将它们设为staticinline
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-19
    • 2017-11-24
    • 2017-02-13
    • 1970-01-01
    • 2012-03-22
    • 1970-01-01
    相关资源
    最近更新 更多