在 C++0x 中原子变量的文档之一中,在描述内存顺序时,它提到:
发布-获取订购
在强排序系统(x86、SPARC、IBM 大型机)上,发布-获取排序是自动的。此同步模式不会发出额外的 CPU 指令,只会影响某些编译器优化...
首先,x86 遵循严格的内存排序是真的吗?总是强加这一点似乎效率很低。意味着每次写入和读取都有围栏?
另外,如果我有一个对齐的 int,在 x86 系统上,原子变量是否有任何用途?
是的,x86 确实有严格的内存排序,请参阅Intel 手册第 3A 卷第 8.2 章。较旧的 x86 处理器(例如 386)提供了真正严格的排序(称为强排序)语义,而更现代的 x86 处理器在少数情况下稍微宽松的条件,但您无需担心。例如,奔腾和 486 允许读取缓存未命中在写入缓存命中时先于缓冲写入(因此与读取的地址不同)。
是的,它可能效率低下。有时,高性能软件仅针对内存排序要求较宽松的其他架构而编写。
是的,原子变量在 x86 上仍然有用。它们与编译器具有特殊的语义,因此典型的读-修改-写操作以原子方式发生。如果您有两个线程同时递增一个原子变量(我的意思是std::atomic<T>
C++11 中的类型变量),您可以确信该值会大 2;如果没有std::atomic
,您可能会得到错误的值,因为一个线程在执行增量时将当前值缓存在寄存器中,即使在 x86 上存储到内存是原子的。
确实,在 x86 上,所有存储都有释放,所有加载都有获取语义。
这不会也不应该影响您编写 C++ 的方式:要编写并发的、无竞争的代码,您必须使用std::atomic
构造或锁。
架构细节的意思是,在 x86 上,只要您最多要求获取/释放顺序,就会为原子字大小类型的操作生成很少或没有额外代码。(但是,顺序一致性会发出mfence
指令。)但是,您仍然必须使用 C++ 原子类型,并且不能仅仅为了获得正确、格式良好的程序而忽略它们。原子变量的一个重要特性是它们可以防止编译器重新编码,这对于程序的正确性至关重要。
(在 C++11 之前,您将不得不使用编译器提供的扩展,例如 GCC 的__sync_*
函数套件,这将使编译器正常运行。如果您真的想使用裸变量,您至少必须插入编译器为自己设障。)
有一张很好的表格,列出了可能发生的不同重新排序操作,并且(例如)x86 很少做这些操作。其他架构(众所周知的 Alpha)几乎可以做任何事情。
由于内存模型是由标准定义的,x86 et al 本质上是兼容的。
您关于原子变量的问题的答案略有不同。对变量的任何修改都涉及竞争条件,因此当多个线程更新同一个变量时,可能会丢失更新。原子变量的定义使得它们是原子操作的正确类型,从而消除了这种竞争条件。所以他们的目的之一不是为了订购。
请注意,释放/获取语义不一定意味着在每条指令之后都有一个 mfence。可以在@Adam Rosenfield 引用的手册或快速浏览Wikipedia中看到在x86 上的保留。尽管如此,x86 具有用于存储的释放语义和用于加载的获取语义。
来自 Kerrek SB 的回答:
架构细节的意思是,在 x86 上,只要您最多要求获取/释放顺序,就会为原子字大小类型的操作生成很少或没有额外代码。(不过,顺序一致性会发出 mfence 指令。)
请注意,顺序一致性是默认设置!(参见例如cppreference)。
这意味着...
#include <atomic>
#include <cassert>
#include <string>
std::atomic<std::string*> ptr;
void producer()
{
std::string* p = new std::string("Hello");
ptr = p;
}
void consumer()
{
std::string* p2;
while (!(p2 = ptr))
;
assert(*p2 == "Hello"); // never fails
}
(x86 上的 g++ -std=c++11 -S -O3)
...实际上将导致在mfence
生产者函数中发出一个,以解释上述 x86 ( ) 上的松弛。
而对于...
#include <atomic>
#include <cassert>
#include <string>
std::atomic<std::string*> ptr;
void producer()
{
std::string* p = new std::string("Hello");
ptr.store(p, std::memory_order_release);
}
void consumer()
{
std::string* p2;
while (!(p2 = ptr.load(std::memory_order_acquire)))
;
assert(*p2 == "Hello"); // never fails
}
(x86 上的 g++ -std=c++11 -S -O3)
...不会插入 mfence,因为 x86 具有用于存储的释放语义和用于加载的获取语义。