我不确定我是否完全理解(并且我可能完全错误)C ++ 11中的原子性和内存排序的概念。 让我们以单线程为例:
int main()
{
std::atomic<int> a(0);
std::atomic<int> b(0);
a.store(16);
b.store(10);
return 0;
}
在这个单线程代码中,如果a和b不是原子类型,编译器可能会以汇编代码中的方式对指令进行重新排序,例如我有一个移动指令,指定10到&b; b& #39;在移动指令之前指定16到&#39; a&#39;。 所以对我来说,作为原子变量,它保证了我有一个移动指令&#34;在&#34; b移动指令之前&#34;正如我在源代码中所述。 在那之后,处理器有他的执行单元,预取指令,以及他的无序框。并且该处理器可以处理&#34; b指令&#34;在&#34;指令&#34;之前,无论汇编代码中的指令排序如何。 因此,在将16存储在寄存器/存储缓冲区或缓存中之前,我可以将10存储在寄存器或处理器的存储缓冲区或高速缓冲存储器中。
根据我的理解,它是内存排序模型出现的地方。从那一刻起,如果我让默认模型顺序一致。我保证在主内存中清除这些值(10和16)将遵循我在源代码中执行存储的顺序。这样处理器就会开始刷新寄存器或缓存,其中16存储在主存储器中以便更新&#39; a&#39;之后,它将在主存储器中刷新10,用于&#39; b&#39;。
因此,如果我使用宽松的内存模型,这种行为确实让我理解。只有最后一部分不能保证,以便主记忆中的冲洗完全无序。
对不起,如果你读书我有困难,我的英语仍然很差。但是谢谢你们的时间。
答案 0 :(得分:3)
C ++内存模型是关于抽象机器和价值可见性的,而不是关于具体事物,例如&#34;主内存&#34;,&#34;写入队列&#34;或&#34;冲洗&#34;。
在您的示例中,内存模型指出,由于写入a
发生在写入b
之前,任何从b
读取10的线程必须在后续读取时发生来自a
,见16(除非此后已被覆盖)。
重要的是建立先发生过的关系和价值可见性。这如何映射到缓存和内存取决于编译器。在我看来,最好保持在抽象层面,而不是试图将模型映射到您对硬件的理解,因为
答案 1 :(得分:2)
您没有指定使用哪种架构,但基本上每种架构都有自己的内存订购模式(有时候可以选择不止一种),这可以作为&#34;合同&#34 ;。编译器应该意识到这一点,并相应地使用轻量级或重量级指令来保证它所需的内容,以便提供该语言的内存模型。
引擎盖下的硬件实现可能非常复杂,但简而言之 - 您不需要刷新以获得全局可见性。现代缓存系统提供窥探功能,因此值可以全局可见并全局排序,同时仍驻留在某些私有核心缓存中(并且在较低缓存级别中具有过时副本),MESI协议控制如何正确处理该值。
写入的生命周期始于乱序引擎,它仍然是推测性的(即 - 由于较早的分支错误预测或错误而可以清除)。当然,在此期间无法从外部看到写入,因此这里的无序执行与此无关。一旦提交,如果系统保证商店订购(如x86),它仍然必须排队等待轮到它变得可见,所以它是缓冲的。其他核心无法看到它,因为它的观察时间尚未到达(尽管该核心中的局部负载可能会在x86的某些实现中看到它 - 这是TSO与实际序列之间的差异之一一致性)。 一旦旧商店完成,商店可能会全局可见 - 它不必去核心之外的任何地方,它可以保持在内部缓存。实际上,有些CPU甚至可以在存储缓冲区中使其可观察,或者以推测方式将其写入缓存 - 实际的决策点是何时使其响应外部窥探,其余的是实现细节。订单更轻松的架构可能会更改订单,除非被栅栏/屏障明确阻止。
基于此,您的代码段无法对x86上的商店进行重新排序,因为商店不会在那里重新排序,但它可以在手臂上执行此操作。如果在这种情况下语言需要强排序,则编译器必须决定它是否可以依赖HW,或者添加围栏。无论哪种方式,从另一个线程(或套接字)读取此值的任何人都必须窥探它,并且只能看到响应的写入。