非原子负载可以在原子获取负载之后重新排序吗?

Can non-atomic-load be reordered after atomic-acquire-load?

众所周知,自 C++11 以来,有 6 个内存顺序,在有关 std::memory_order_acquire:

的文档中

memory_order_acquire

A load operation with this memory order performs the acquire operation on the affected memory location: no memory accesses in the current thread can be reordered before this load. This ensures that all writes in other threads that release the same atomic variable are visible in the current thread.

1.非原子加载可以在原子获取加载后重新排序:

即不保证在acquire-atomic-load之后不能重新排序non-atomic-load。

static std::atomic<int> X;
static int L;
...

void thread_func() 
{
    int local1 = L;  // load(L)-load(X) - can be reordered with X ?

    int x_local = X.load(std::memory_order_acquire);  // load(X)

    int local2 = L;  // load(X)-load(L) - can't be reordered with X
}

加载 int local1 = L; 可以在 X.load(std::memory_order_acquire); 之后重新排序吗?

2。我们可以认为non-atomic-load在atomic-acquire-load之后是不能重新排序的:

一些文章包含一张图片,展示了获取-释放语义的本质。这很容易理解,但可能会造成混淆。

例如,我们可能会认为std::memory_order_acquire不能对任何一系列的Load-Load操作进行重新排序,即使是非atomic-load也不能在atomic-acquire-load之后进行重新排序。

3。非原子加载可以在原子获取加载后重新排序:

澄清的好事:获取语义防止使用任何读取或写入操作读取-获取的内存重新排序程序顺序http://preshing.com/20120913/acquire-and-release-semantics/

而且 known, that:在强顺序系统(x86、SPARC TSO、IBM 大型机)上,发布-获取顺序是自动的 用于大多数操作。

第 34 页的 Herb Sutter 显示:https://onedrive.live.com/view.aspx?resid=4E86B0CF20EF15AD!24884&app=WordPdf&authkey=!AMtj_EflYn2507c

4. IE。同样,我们可以认为非原子加载不能在原子获取加载之后重新排序:

即对于 x86:

  • release-acquire 大多数操作的顺序是自动的
  • 读取不会与 any 读取一起重新排序。 (任何 - 即无论是否年长)

那么在 C++11 中非原子加载是否可以在原子获取加载之后重新排序?

您引用的参考非常清楚:您不能在此加载之前移动读取。在您的示例中:

static std::atomic<int> X;
static int L;


void thread_func() 
{
    int local1 = L;  // (1)
    int x_local = X.load(std::memory_order_acquire);  // (2)
    int local2 = L;  // (3)
}

memory_order_acquire 意味着 (3) 不能发生在 (2) 之前((2) 中的加载顺序在 (3) 中的加载之前)。它没有说明(1)和(2)之间的关系。

我相信这是在 C++ 标准中推理您的示例的正确方法:

  1. X.load(std::memory_order_acquire)(姑且称之为"operation (A)")可能与X上的某个释放操作同步(操作(R))——大致就是赋值的操作X (A) 正在阅读。

[atomics.order]/2 An atomic operation A that performs a release operation on an atomic object M synchronizes with an atomic operation B that performs an acquire operation on M and takes its value from any side effect in the release sequence headed by A.

  1. 这种同步关系可能有助于在 L 的某些修改和赋值 local2 = L 之间建立先行关系。如果 L 的修改发生在 (R) 之前,那么,由于 (R)(A) 同步并且 (A) 在读取之前排序LL 的修改发生在 L.

  2. 的读取之前
  3. 但是 (A) 对赋值 local1 = L 没有任何影响。它既不会导致涉及此分配的数据竞争,也不会帮助防止它们。如果程序是无竞争的,那么它必须采用一些其他机制来确保 L 的修改与这次读取同步(如果它不是无竞争的,那么它会表现出未定义的行为并且标准没有任何内容进一步说一下)。


在C++标准的四个角落里谈论"instruction reordering"是没有意义的。人们可能会谈论由特定编译器生成的机器指令,或者这些指令由特定 CPU 执行的方式。但从标准的角度来看,这些只是不相关的实现细节,只要该编译器和 CPU 产生与标准 (the As-If rule) 描述的抽象机的一种可能执行路径一致的可观察行为。

A load operation with this memory order performs the acquire operation on the affected memory location: no memory accesses in the current thread can be reordered before this load.

这就像编译器代码生成的经验法则。

但这绝对不是 C++ 的公理。

有很多情况,有些可以简单地检测到,有些需要更多的工作,其中 V 上的内存 Op 操作可以证明用 A 上的原子操作 X 重新排序。

两个最明显的案例:

  • 当 V 是一个严格的局部变量时:不能被任何其他线程(或信号处理程序)访问的变量,因为它的地址在函数外部不可用;
  • 当 A 是这样一个严格的局部变量时。

(请注意,编译器进行的这两次重新排序对于为 X 指定的任何可能的内存排序都是有效的。)

在任何情况下,转换都是不可见的,它不会改变有效程序的可能执行。

在不太明显的情况下,这些类型的代码转换是有效的。有些是做作的,有些是现实的。

我可以很容易地想出这个人为的例子:

using namespace std;

static atomic<int> A;

int do_acq() {
  return A.load(memory_order_acquire);
}

void do_rel() {
  A.store(0, memory_order_release);
} // that's all folks for that TU

注:

使用静态变量能够在单独编译的代码中看到对象的所有操作;访问原子同步对象的函数不是静态的,可以从所有程序调用。

作为同步原语,对 A 的操作建立同步关系:有一个介于:

  • 在点 pX
  • 调用 do_rel() 的线程 X
  • 和在点 pY
  • 调用 do_acq() 的线程 Y

A 的修改 M 有一个定义明确的顺序,对应于不同线程中对 do_rel() 的调用。每次调用 do_acq() 要么:

  • 观察在 pX_i 调用 do_rel() 的结果,并通过在 pX_i
  • 提取 X 的历史记录与线程 X 同步
  • 观察A的初值

另一方面,该值始终为 0,因此调用代码仅从 do_acq() 获得 0,无法确定 return 值发生了什么。它可以先验地知道 A 的修改已经发生,但它不能仅后验地知道。先验知识可以来自另一个同步操作。先验知识是线程 Y 历史的一部分。无论哪种方式,获取操作都没有知识,也不会添加过去的历史:获取操作的已知部分是空的,它不能可靠地获取任何在线程 Y 在 pY_i 的过去。所以A上的acquire没有意义,可以优化掉

换句话说:当 do_acq() 看到 Y 历史上最近的 do_rel(),即在 A 的所有修改之前的那个时,对 M 所有可能值有效的程序必须有效可见。所以 do_rel() 通常不添加任何内容:do_rel() 可以在某些执行中添加非冗余同步,但它添加的最小值 Y 什么也没有,所以一个正确的程序,一个不t 具有竞争条件(表示为:其行为取决于 M,例如其正确性是获取 M 允许值的某个子集的函数)必须准备好处理从 do_rel() 中获取任何内容;所以编译器可以使 do_rel() 成为 NOP。

[注意:参数行不能轻易推广到所有读取 0 并存储 0 的 RMW 操作。它可能不适用于 acq-rel RMW。换句话说,acq+rel RMW 比单独的加载和存储更强大,因为它们有“副作用”。]

总结:在那个特定的例子中,不仅内存操作可以相对于原子获取操作上下移动,原子操作可以完全删除。

只是回答您的标题问题:是的,任何加载(无论是原子加载还是 non-atomic)都可以在原子加载之后 re-ordered。同样,任何商店都可以在原子商店之前 re-ordered。

但是,原子存储不一定允许 re-ordered 在原子加载之后,反之亦然(原子加载 re-ordered 在原子存储之前)。

查看 Herb Sutter 在 44:00 附近的 talk