GCC汇编了这个:
#include <atomic>
std::atomic<int> a;
int b(0);
void func()
{
b = 2;
a = 1;
}
到此:
func():
mov DWORD PTR b[rip], 2
mov DWORD PTR a[rip], 1
mfence
ret
所以,为我澄清一些事情:
另外,clang(v3.5.1 -O3)这样做:
mov dword ptr [rip + b], 2
mov eax, 1
xchg dword ptr [rip + a], eax
ret
这似乎对我的小脑子更直接,但为什么不同的方法,每个方法的优势是什么?
答案 0 :(得分:14)
我把你的例子放在Godbolt compiler explorer, and added some functions上来读取,增加或组合(a+=b
)两个原子变量。我还使用a.store(1, memory_order_release);
而不是a = 1;
来避免获得超出需求的订单,因此它只是x86上的一个简单商店。
请参阅下文(希望是正确的)解释。 更新:我只有"release" semantics与StoreStore屏障混淆了。我想我已经解决了所有错误,但可能会留下一些错误。
首先是一个简单的问题:
'a'的写入是否保证是原子的?
是的,任何读取a
的线程都将获得旧值或新值,而不是某些半值。这个happens for free on x86和大多数其他架构都有适合寄存器的对齐类型。 (例如,在32位上不是int64_t
。)因此,在许多系统上,b
也是如此,这也是大多数编译器生成代码的方式。
有些类型的商店在x86上可能不是原子的,包括跨越高速缓存行边界的未对齐商店。但是std::atomic
当然保证了必要的对齐。
读取 - 修改 - 写入操作是有趣的。一次在多个线程中完成的a+=3
的1000次评估将始终产生a += 3000
。如果a
不是原子的,那么你可能会减少。
有趣的事实:签名的原子类型保证了两个补码环绕,与普通的签名类型不同。 C和C ++仍然坚持在其他情况下保留有符号整数溢出的想法。有些CPU没有算术右移,所以保留负数的右移不确定是有道理的,但是否则只是感觉像是一个荒谬的箍,现在所有CPU都使用2的补码和8位字节。 </rant>
读取'a'为1的任何其他线程是否保证将'b'读为2。
是的,因为std::atomic
提供了保证。
现在我们正在进入该语言的memory model及其运行的硬件。
C11和C ++ 11的内存排序模型非常弱,这意味着允许编译器重新排序内存操作,除非你告诉它不要。 (来源:Jeff Preshing's Weak vs. Strong Memory Models)。即使x86是您的目标计算机,您也必须停止编译器在 compile 时重新排序存储。 (例如,通常你希望编译器将a = 1
从一个也写入b
的循环中提升出来。)
默认情况下,使用C ++ 11原子类型可以为程序的其余部分提供关于它们的操作的完全顺序一致性排序。这意味着他们不仅仅是原子的。请参阅下文,了解订购所需的内容,以避免昂贵的围栏操作。
为什么在写入“a”之后才发生MFENCE。
StoreStore fences是x86强大内存模型的无操作,因此编译器只需将商店放到b
商店之前a
来实现源代码排序。
完整的顺序一致性还要求商店在按程序顺序加载之前全局排序/全局可见。
x86可以在加载后重新订购商店。实际上,发生的是乱序执行在指令流中看到一个独立的负载,并在仍在等待数据准备就绪的存储之前执行它。无论如何,顺序一致性禁止这样做,因此gcc使用MFENCE
,这是一个完整的障碍,包括StoreLoad(the only kind x86 doesn't have for free。(LFENCE/SFENCE
仅对弱序操作有用,例如{{1} }}))
另一种表达方式是C ++文档使用的方式:顺序一致性保证所有线程都能看到相同顺序中的所有更改。每个原子存储之后的MFENCE保证该线程看到来自其他线程的存储。 否则,在其他线程的负载看到我们的商店之前,我们的货物会看到我们的商店。 StoreLoad屏障(MFENCE)将我们的负载延迟到需要先发生的商店之后。
movnt
的ARM32 asm是:
b=2; a=1;
我不了解ARM asm,但到目前为止我所知道的通常是# get pointers and constants into registers
str r1, [r3] # store b=2
dmb sy # Data Memory Barrier: full memory barrier to order the stores.
# I think just a StoreStore barrier here (dmb st) would be sufficient, but gcc doesn't do that. Maybe later versions have that optimization, or maybe I'm wrong.
str r2, [r3, #4] # store a=1 (a is 4 bytes after b)
dmb sy # full memory barrier to order this store wrt. all following loads and stores.
,但是加载和存储总是首先拥有寄存器操作数而且内存操作数第二。如果您习惯使用x86,这真的很奇怪,其中内存操作数可以是大多数非向量指令的源或目标。加载立即数常量也需要很多指令,因为固定的指令长度只为op dest, src1 [,src2]
(移动字)/ movw
(移动顶部)的16b有效载荷留出空间。
单向内存屏障的release
and acquire
命名来自锁:
请注意,即使对于与load-acquire或store-release操作稍有不同的独立fence,std:atomic也会使用这些名称。 (参见下面的atomic_thread_fence)。
发布/获取语义比生产者 - 消费者需要的更强。这只需要单向StoreStore(生产者)和单向LoadLoad(消费者),而不需要LoadStore订购。
由读取器/写入器锁(例如)保护的共享哈希表需要获取 - 加载/释放 - 存储原子读 - 修改 - 写操作来获取锁。 x86 movt
是一个完整的障碍(包括StoreLoad),但ARM64具有load-linking-store-release版本的load-linked / store-conditional,用于执行原子读取 - 修改 - 写入。据我了解,即使是锁定,也可以避免使用StoreLoad屏障。
默认情况下,对lock xadd
类型的写入按源代码(加载和存储)中的每个其他内存访问进行排序。您可以使用std::memory_order
来控制强加的排序。
在您的情况下,您只需要生产商确保商店以正确的顺序全局可见,即商店之前的StoreStore屏障std::atomic
。 a
包含此内容以及更多内容。 store(memory_order_release)
只是所有商店的单向StoreStore屏障。 x86免费提供StoreStore,因此编译器所要做的就是将存储按源顺序放置。
发布而不是seq_cst将是一场重大的表现胜利,尤其是。在像x86这样的架构上,发布很便宜/免费。如果无争用案例很常见,情况就更是如此。
读取原子变量还会对所有其他负载和存储施加负载的完全顺序一致性。在x86上,这是免费的。 LoadLoad和LoadStore障碍是无操作,并隐含在每个内存操作中。您可以使用std::atomic_thread_fence(memory_order_release)
。
请注意std::atomic standalone fence functions confusingly reuse the "acquire" and "release" names for StoreStore and LoadLoad fences that order all stores (or all loads) in at least the desired direction。实际上,它们通常会发出硬件指令,这些指令是双向StoreStore或LoadLoad障碍。 This doc是关于什么成为当前标准的提案。您可以看到memory_order_release如何映射到SPARC RMO上的a.load(std::memory_order_acquire)
,我认为其中包含的部分是因为它分别具有所有屏障类型。 (嗯,cppref网页只提到订购商店,而不是LoadStore组件。但它不是C ++标准,所以也许完整的标准会说更多。)
#LoadStore | #StoreStore
对于此用例来说还不够强大。 This post讨论了使用标记表明其他数据已准备就绪的情况,并讨论了memory_order_consume
。
memory_order_consume
的指针,或者甚至是指向结构或数组的指针,那么 consume
就足够了。但是,没有编译器知道如何进行依赖关系跟踪以确保它在asm中按正确顺序放置,因此当前实现始终将b
视为consume
。这太糟糕了,因为除了DEC alpha(和C ++ 11的软件模型)之外的每个架构都免费提供这种排序。 According to Linus Torvalds, only a few Alpha hardware implementations actually could have this kind of reordering, so the expensive barrier instructions needed all over the place were pure downside for most Alphas.
生产者仍然需要使用acquire
语义(StoreStore屏障),以确保在更新指针时新的有效负载可见。
使用release
编写代码并不是一个坏主意,如果您确定自己了解其含义并且不依赖于consume
无法做出的任何事情&# 39;保证。将来,一旦编译器变得更加智能,即使在ARM / PPC上,您的代码也将在没有屏障指令的情况下进行编译。实际数据移动仍然必须在不同CPU上的缓存之间发生,但在弱内存模型机器上,您可以避免等待任何不相关的写入可见(例如,生产者中的暂存缓冲区)。
请记住您实际上无法通过实验测试consume
代码,因为当前的编译器为您提供了比代码请求更强的排序。
无论如何,很难通过实验来测试这些,因为它对时间敏感。此外,除非编译器重新命令操作(因为你没有告诉它),生产者 - 消费者线程永远不会在x86上出现问题。您需要在ARM或PowerPC上进行测试,甚至需要尝试查找实际中发生的排序问题。
的引用:
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=67458:我在x86上报告了memory_order_consume
生成b=2; a.store(1, MO_release); b=3;
时发现的gcc错误,而不是a=1;b=3
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=67461:我还报告了一个事实,即ARM gcc连续使用两个b=3; a=1;
dmb sy
,而x86 gcc可能会使用更少的mfence操作。我不确定每个商店之间是否需要a=1; a=1;
来保护信号处理程序做出错误的假设,或者它是否只是缺少优化。
The Purpose of memory_order_consume in C++11 (already linked above)完全涵盖了使用标志在线程之间传递非原子有效负载的情况。
StoreLoad障碍(x86 mfence)适用于:展示需求的工作示例程序:http://preshing.com/20120515/memory-reordering-caught-in-the-act/
控制依赖障碍:http://www.mjmwired.net/kernel/Documentation/memory-barriers.txt#592
Doug Lea表示对于使用&#34;流媒体&#34;写的数据,x86只需要mfence
像LFENCE
或movntdqa
这样的写作。 (NT =非时间)。除了绕过缓存,x86 NT加载/存储具有弱有序语义。
http://preshing.com/20120612/an-introduction-to-lock-free-programming/(指向他推荐的书籍及其他内容)。
有趣的thread on realworldtech关于无处不在的障碍或强大的记忆模型是否更好,包括数据依赖在HW中几乎是免费的,所以愚蠢的跳过它并放大软件负担。 (Alpha(和C ++)没有,但其他一切都有)。回过头来看看Linus Torvalds&#39;在他解释他的论点的更详细/技术原因之前,有趣的侮辱。