从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。我们可以认为在原子获取负载之后无法重新排序非原子负载:
一些文章包含一张显示获取 - 释放语义本质的图片。这很容易理解,但可能引起混淆。
例如,我们可能认为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
4。即再次,我们可以认为在原子获取负载之后无法重新排序非原子负载:
即。对于x86:
在C ++ 11中原子获取加载后,非原子加载是否可以重新排序?
答案 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 ++标准中推理你的例子的正确方法:
X.load(std::memory_order_acquire)
(我们称之为“操作(A)
”)可以与X
上的某个发布操作同步(操作(R)
) - 粗略地说,分配了X
正在阅读的(A)
的值。[atomics.order] / 2 对原子对象
A
执行释放操作的原子操作M
与原子同步 对B
执行获取操作的操作M
,并从A
为首的发布序列中的任何副作用中获取其值。
此同步关系可能有助于在L
的某些修改与作业local2 = L
之间建立先发生关系。如果L
的修改发生在(R)
之前,那么,由于(R)
与(A)
和(A)
同步的事实在读取之前L
的{{1}}的修改发生在L
之前。
但L
对作业(A)
没有任何影响。它既不会导致涉及此任务的数据争用,也不会有助于防止它们。如果该程序是无竞争的,那么它必须采用一些其他机制来确保local1 = L
的修改与此读取同步(并且如果它不是无竞争的,那么它表现出未定义的行为并且标准没有任何内容进一步说吧。
在C ++标准的四个角落里谈论“指令重新排序”是没有意义的。人们可以谈论由特定编译器生成的机器指令,或者由特定CPU执行这些指令的方式。但从标准的角度来看,这些只是不相关的实现细节,只要该编译器和该CPU产生的可观察行为与标准(the As-If rule)描述的抽象机器的一个可能的执行路径一致。
答案 2 :(得分:0)
具有此存储顺序的装入操作执行获取操作 在受影响的内存位置上:当前没有内存访问 线程可以在此加载之前重新排序。
这就像编译器代码生成的经验法则。
但这绝对是不是C ++的公理。
在很多情况下,有些情况是可检测的,有些则需要更多的工作,其中可以用A上的原子操作X证明对V上的存储器Op的操作进行重新排序。
两个最明显的情况:
(请注意,编译器对这两种重新排序对于为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的操作建立了同步关系:两者之间存在一个:
do_rel()
的线程X do_acq()
A的修改M的定义顺序很明确,对应于在不同线程中对do_rel()
的调用。每次致电do_acq()
之一:
do_rel()
的调用结果,并通过在pX_i处获取X的历史记录来与线程X同步另一方面,该值始终为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。