这取决于架构,但一般来说,负载是便宜的,与具有严格内存排序的存储器配对可能会很昂贵。
在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;
}
使用 -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
我们可以看到,在+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;
}
那么在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
请注意,如果我们将相同的代码编译为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
当我们写入变量位于+36时,我们使用Store-Release指令(stlr),而+40处的加载使用Load-Acquire(ldar)。这些都提供了部分内存屏障(并共同形成完整的屏障)。
只有在必须推断变量的访问顺序时才应使用atomic(原子性)。为回答您的附加问题,请使用std :: memory_order_relaxed来读取atomic中的内存,不能保证与写入同步。只有原子性是得到保证的。
i
可以在多个线程中被读写,那么就会出现数据竞争和未定义行为,这将完全抵消任何性能提升。 - NathanOliver[1]
大约比[2]
快40倍。但这可能因平台和编译器(以及编译器的优化)而有很大差异。 - Eljayatomic<>
,但没有使用它,通常的后果是像MCU编程-C++ O2优化中断while循环那样的情况-通过提升负荷,while(!read){}
循环变成了if(!ready) infinite_loop();
。 - Peter Cordes