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]
语句[1]是否比语句[2]&[3] 在多线程环境中
假设ai
可能被写入也可能不被写入另一个线程,当[2]&[3] 正在执行。
加载项:假设基础整数的准确值不是必需的,那么读取原子变量的最快方法是什么?
这取决于体系结构,但通常负载很便宜,但与具有严格内存排序的存储配对可能会很昂贵。
在x86_64上,高达64位的加载和存储本身是原子的(但读-修改-写显然是而不是)。
正如您所知,C++中的默认内存顺序是std::memory_order_seq_cst
,这为您提供了顺序一致性,即:所有线程都会按照一定的顺序看到加载/存储。要在x86(实际上是所有多核系统)上实现这一点,需要在存储上设置内存围栏,以确保在存储读取新值后发生的加载。
在这种情况下,读取不需要强有序x86上的内存围栏,但写入需要。在大多数弱序ISAs上,即使是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写入变量时,我们使用存储释放指令(stlr),在+40加载时使用加载获取(ldar)。它们各自提供一个部分内存围栏(并一起形成一个完整围栏)。
只有当有来推断变量的访问顺序时,才应该使用atomic。要回答您的附加组件问题,请使用std::memory_order_relaxed
使内存读取原子,而不保证与写入同步。只有原子性是有保证的。
给出的3种情况具有不同的语义,因此对它们的相对性能进行推理可能毫无意义,除非在线程启动后从未写入值。
情况1:
int i = 0;
if(i == 10) {...} // may actually be optimized away since `i` is clearly 0 now
如果i
被多个线程访问,其中包括写入,则行为未定义。
在没有同步的情况下,编译器可以自由地假设没有其他线程可以修改i
,并可以重新排序/优化对它的访问。例如,它可以将i
加载到寄存器中一次,然后从不从内存中重新读取,也可以将写操作从循环中取出,最后只写一次。
情况2:
std::atomic<int> ai{0};
if(ai == 10) {...} // [2]
默认情况下,对atomic
的读取和写入按std::memory_order_seq_cst
(顺序一致)内存顺序进行。这意味着不仅对ai
原子进行读/写,而且它们对其他线程也是可见的,包括任何其他变量在其之前/之后的读/写。
因此,读/写atomic
就像一道记忆屏障。然而,这要慢得多,因为(1)SMP系统必须同步处理器之间的缓存,以及(2)编译器在围绕原子访问优化代码方面的自由度要小得多。
情况3:
std::atomic<int> ai{0};
if(ai.load(std::memory_order_relaxed) == 10) {...} // [3]
此模式仅允许并保证ai
读取/写入的原子性。因此,编译器可以再次自由地重新排序对它的访问,并且只保证写入在合理的时间内对其他线程可见。
它的适用性非常有限,因为它使得很难推理程序中事件的顺序。例如
std::atomic<int> ai{0}, aj{0};
// thread 1
aj.store(1, std::memory_order_relaxed);
ai.store(10, std::memory_order_relaxed);
// thread 2
if(ai.load(std::memory_order_relaxed) == 10) {
aj.fetch_add(1, std::memory_order_relaxed);
// is aj 1 or 2 now??? no way to tell.
}
这种模式可能(而且经常)比情况1慢,因为编译器必须确保每次读/写都能真正到达缓存/RAM,但比情况2快,因为仍然可以优化它周围的其他变量。
有关原子论和内存排序的更多详细信息,请参阅Herb Sutter的优秀原子<gt;武器谈话。
关于您对UB的评论,是否只会影响数据的准确性,或者会导致系统崩溃(有点像UB)?
如果在应该读取的时候不使用atomic<>
,通常的结果是MCU编程之类的东西-循环时C++O2优化中断
例如CCD_ 15环路通过提升负载而变成CCD_。
只是不要这么做;如果可以的话,手动提升源中的原子负载,如int localtmp = shared_var.load(std::memory_order_relaxed);