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

时间:2016-07-30 18:13:25

标签: c++ multithreading c++11 concurrency memory-fences

从C ++ 11开始就知道有6个内存命令,并且在关于std::memory_order_acquire的文档中已知:

  

memory_order_acquire

     

具有此内存顺序的加载操作将执行获取操作   在受影响的内存位置:当前没有内存访问   线程可以在此加载之前重新排序。这确保了所有写入   在其他线程中,可以看到释放相同原子变量的内容   当前的主题。

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

即。它不能保证在获得原子载荷之后不能重新排序非原子载荷。

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。我们可以认为在原子获取负载之后无法重新排序非原子负载:

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

enter image description here

enter image description here

例如,我们可能认为std::memory_order_acquire无法对任何一系列的Load-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

enter image description here

4。即再次,我们可以认为在原子获取负载之后无法重新排序非原子负载:

即。对于x86:

  • 发布 - 获取对大多数操作自动排序
  • 读取不会与任何读取重新排序。 (任何 - 无论是否老年人)

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

4 个答案:

答案 0 :(得分:4)

您引用的引用非常明确:在此加载之前无法移动读取。在您的示例中:

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)中的thr加载之前被排序)。它没有说明(1)和(2)之间的关系。

答案 1 :(得分:3)

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

  1. X.load(std::memory_order_acquire)(我们称之为“操作(A)”)可以与X上的某个发布操作同步(操作(R)) - 粗略地说,分配了X正在阅读的(A)的值。
  2.   

    [atomics.order] / 2 对原子对象A执行释放操作的原子操作M与原子同步   对B执行获取操作的操作M,并从A为首的发布序列中的任何副作用中获取其值。

    1. 此同步关系可能有助于在L的某些修改与作业local2 = L之间建立先发生关系。如果L的修改发生在(R)之前,那么,由于(R)(A)(A)同步的事实在读取之前L的{​​{1}}的修改发生在L之前。

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

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

答案 2 :(得分:0)

  

具有此存储顺序的装入操作执行获取操作   在受影响的内存位置上:当前没有内存访问   线程可以在此加载之前重新排序。

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

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

在很多情况下,有些情况是可检测的,有些则需要更多的工作,其中可以用A上的原子操作X证明对V上的存储器Op的操作进行重新排序。

两个最明显的情况:

  • 当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的操作建立了同步关系:两者之间存在一个:

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

A的修改M的定义顺序很明确,对应于在不同线程中对do_rel()的调用。每次致电do_acq()之一:

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

另一方面,该值始终为0,因此调用代码仅从do_acq()获得0,无法确定返回值所发生的情况。它可以先验地知道A的修饰已经发生,但它不能仅知道后验。先验知识可以来自另一个同步操作。先验知识是线程Y的历史记录的一部分。无论哪种方式,获取操作都没有知识,也不添加过去的历史记录:获取操作的已知部分为空,它无法可靠地获取线程中的任何内容。线程Y在pY_i的过去。因此,对A的获取是没有意义的,可以对其进行优化。

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

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

摘要:在该特定示例中,不仅内存操作可以相对于原子获取操作上下移动,而且原子操作可以完全删除。

答案 3 :(得分:0)

只需回答您的标题问题:是的,可以在原子负载之后重新排序任何负载(无论是原子负载还是非原子负载)。同样,任何存储都可以在原子存储之前重新排序。

但是,不一定要在原子加载之后对原子存储进行重新排序,反之亦然(在原子存储之前对原子负载进行重新排序)。

请参见44:00左右的Herb Sutter的talk