与普通变量相比,仅读取原子变量是否有任何性能差异?

iam*_*ind 5 c++ performance multithreading atomic stdatomic

int i = 0;
if(i == 10)  {...}  // [1]

std::atomic<int> ai{0};
if(ai == 10) {...}  // [2]
if(ai.load(std::memory_order_relaxed) == 10) {...}  // [3]
Run Code Online (Sandbox Code Playgroud)

在多线程环境中,语句 [1] 是否比语句 [2] & [3] 更快?
假设ai在执行 [2] 和 [3] 时,可能会或可能不会在另一个线程中写入。

附加:如果不需要底层整数的准确值,那么读取原子变量的最快方法是什么?

gct*_*gct 6

这取决于架构,但通常负载很便宜,但与具有严格内存排序的存储配对可能会很昂贵。

在 x86_64 上,最多 64 位的加载和存储本身是原子的(但读取-修改-写入显然不是)。

正如您所拥有的,C++ 中的默认内存排序是std::memory_order_seq_cst,它为您提供了顺序一致性,即:所有线程都会看到加载/存储发生的某种顺序。要在 x86(实际上是所有多核系统)上实现这一点需要存储上的内存栅栏以确保在存储读取新值后发生的加载。

阅读在这种情况下也不会要求对强有序的x86内存栅栏,但写作一样。在大多数弱序 ISA 上,即使是 seq_cst 读取也需要一些屏障指令,但不需要完整屏障。如果我们看一下这段代码:

#include <atomic>
#include <stdlib.h>

int main(int argc, const char* argv[]) {
    std::atomic<int> num;

    num = 12;
    if (num == 10) {
        return 0;
    }
    return 1;
}
Run Code Online (Sandbox Code Playgroud)

用 -O3 编译:

   0x0000000000000560 <+0>:     sub    $0x18,%rsp
   0x0000000000000564 <+4>:     mov    %fs:0x28,%rax
   0x000000000000056d <+13>:    mov    %rax,0x8(%rsp)
   0x0000000000000572 <+18>:    xor    %eax,%eax
   0x0000000000000574 <+20>:    movl   $0xc,0x4(%rsp)
   0x000000000000057c <+28>:    mfence 
   0x000000000000057f <+31>:    mov    0x4(%rsp),%eax
   0x0000000000000583 <+35>:    cmp    $0xa,%eax
   0x0000000000000586 <+38>:    setne  %al
   0x0000000000000589 <+41>:    mov    0x8(%rsp),%rdx
   0x000000000000058e <+46>:    xor    %fs:0x28,%rdx
   0x0000000000000597 <+55>:    jne    0x5a1 <main+65>
   0x0000000000000599 <+57>:    movzbl %al,%eax
   0x000000000000059c <+60>:    add    $0x18,%rsp
   0x00000000000005a0 <+64>:    retq
Run Code Online (Sandbox Code Playgroud)

我们可以看到从 +31 处的原子变量读取不需要任何特殊的东西,但是因为我们在 +20 处写入原子变量,编译器必须在mfence之后插入一条指令,以确保该线程等待其存储变为在执行任何后续加载之前可见。这是昂贵的,停止这个核心直到存储缓冲区耗尽。(在某些 x86 CPU 上仍然可以执行后续非内存指令的乱序执行。)

如果我们改为std::memory_order_release在写入时使用较弱的排序(例如):

#include <atomic>
#include <stdlib.h>

int main(int argc, const char* argv[]) {
    std::atomic<int> num;

    num.store(12, std::memory_order_release);
    if (num == 10) {
        return 0;
    }
    return 1;
}
Run Code Online (Sandbox Code Playgroud)

然后在 x86 上我们不需要围栏:

   0x0000000000000560 <+0>:     sub    $0x18,%rsp
   0x0000000000000564 <+4>:     mov    %fs:0x28,%rax
   0x000000000000056d <+13>:    mov    %rax,0x8(%rsp)
   0x0000000000000572 <+18>:    xor    %eax,%eax
   0x0000000000000574 <+20>:    movl   $0xc,0x4(%rsp)
   0x000000000000057c <+28>:    mov    0x4(%rsp),%eax
   0x0000000000000580 <+32>:    cmp    $0xa,%eax
   0x0000000000000583 <+35>:    setne  %al
   0x0000000000000586 <+38>:    mov    0x8(%rsp),%rdx
   0x000000000000058b <+43>:    xor    %fs:0x28,%rdx
   0x0000000000000594 <+52>:    jne    0x59e <main+62>
   0x0000000000000596 <+54>:    movzbl %al,%eax
   0x0000000000000599 <+57>:    add    $0x18,%rsp
   0x000000000000059d <+61>:    retq   
Run Code Online (Sandbox Code Playgroud)

但请注意,如果我们为 AArch64 编译相同的代码:

   0x0000000000400530 <+0>:     stp  x29, x30, [sp,#-32]!
   0x0000000000400534 <+4>:     adrp x0, 0x411000
   0x0000000000400538 <+8>:     add  x0, x0, #0x30
   0x000000000040053c <+12>:    mov  x2, #0xc
   0x0000000000400540 <+16>:    mov  x29, sp
   0x0000000000400544 <+20>:    ldr  x1, [x0]
   0x0000000000400548 <+24>:    str  x1, [x29,#24]
   0x000000000040054c <+28>:    mov  x1, #0x0
   0x0000000000400550 <+32>:    add  x1, x29, #0x10
   0x0000000000400554 <+36>:    stlr x2, [x1]
   0x0000000000400558 <+40>:    ldar x2, [x1]
   0x000000000040055c <+44>:    ldr  x3, [x29,#24]
   0x0000000000400560 <+48>:    ldr  x1, [x0]
   0x0000000000400564 <+52>:    eor  x1, x3, x1
   0x0000000000400568 <+56>:    cbnz x1, 0x40057c <main+76>
   0x000000000040056c <+60>:    cmp  x2, #0xa
   0x0000000000400570 <+64>:    cset w0, ne
   0x0000000000400574 <+68>:    ldp  x29, x30, [sp],#32
   0x0000000000400578 <+72>:    ret
Run Code Online (Sandbox Code Playgroud)

当我们在 +36 处写入变量时,我们使用 Store-Release 指令 (stlr),而在 +40 处加载则使用 Load-Acquire (ldar)。这些每个都提供了一个部分内存栅栏(并一起形成了一个完整的栅栏)。

只有在必须对变量的访问顺序进行推理时才应该使用 atomic 。要回答您的附加问题,请使用std::memory_order_relaxedfor 内存读取原子,不保证与写入同步。只保证原子性。