6

我不确定我是否完全理解(我可能完全错了)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'将 16 分配给“a”。所以对我来说,作为原子变量,它保证我在“b 移动指令”之前有“a 移动指令”,正如我在源代码中所说的那样。在那之后,有处理器和他的执行单元、预取指令和他的乱序框。并且该处理器可以在“a指令”之前处理“b指令”,无论汇编代码中的指令顺序是什么。

据我了解,这就是内存排序模型出现的地方。从那一刻起,如果我让默认模型顺序一致。可以保证我在主内存中清除这些值(10 和 16)将尊重我在源代码中存储的顺序。这样处理器将开始刷新寄存器或高速缓存,其中 16 存储到主存储器中以更新“a”,然后它将刷新主存储器中的 10 以用于“b”。

所以这种行为确实让我明白,如果我使用宽松的记忆模型。只有最后一部分不能保证,因此主内存中的刷新可能完全无序。

抱歉,如果您阅读我的内容有困难,我的英语仍然很差。但是谢谢你们的时间。

4

2 回答 2

4

C++ 内存模型是关于抽象机器和值的可见性,而不是关于诸如“主内存”、“写入队列”或“刷新”之类的具体事物。

在您的示例中,内存模型指出,由于写入a发生在写入之前b,任何从 读取 10 的线程b必须在后续读取a16 时看到 16(当然,除非这已被覆盖)。

这里重要的是建立先发生的关系和价值可见性。这如何映射到缓存和内存取决于编译器。在我看来,最好停留在那个抽象级别,而不是试图将模型映射到您对硬件的理解,因为

  • 您对硬件的理解可能是错误的。硬件比 C++ 内存模型还要复杂。
  • 即使您现在的理解是正确的,硬件的更高版本也可能具有不同的模型,至少在子系统中是这样。
  • 通过映射到硬件模型,您可能会对不同硬件模型的含义做出错误的假设。例如,如果您了解内存模型如何映射到 x86 硬件,您将不会理解 PowerPC 上消耗和获取之间的细微差别。
  • C++ 模型非常适合推理正确性。
于 2015-04-16T09:39:04.457 回答
3

您没有指定您使用的架构,但基本上每个架构都有自己的内存排序模型(有时不止一个,您可以从中选择),并且作为“合同”。编译器应该意识到这一点,并相应地使用轻量级或重量级指令来保证它需要什么来提供语言的内存模型。

引擎盖下的硬件实现可能非常复杂,但简而言之 - 您无需刷新即可获得全局可见性。现代缓存系统提供窥探功能,因此值可以全局可见和全局排序,同时仍驻留在某些私有核心缓存中(并且在较低缓存级别具有陈旧副本),MESI 协议控制如何正确处理。

写入的生命周期从乱序引擎开始,它仍然是推测性的(即 - 可以由于较旧的分支错误预测或故障而被清除)。自然地,在那段时间从外面看不到写入,所以这里的乱序执行是不相关的。一旦提交,如果系统保证存储顺序(如 x86),它仍然必须排队等待轮到它变得可见,因此它被缓冲。其他核心看不到它,因为它的观察时间尚未达到(尽管该核心中的本地负载可能会在 x86 的某些实现中看到它 - 这是 TSO 和真正的顺序一致性之间的区别之一)。一旦旧的存储完成,存储可能会变得全局可见——它不必去核心之外的任何地方,它可以在内部保持缓存。实际上,一些 CPU 甚至可以在存储缓冲区中使其可观察,或者推测性地将其写入缓存 - 实际决策点是何时使其响应外部窥探,其余的是实现细节。除非被栅栏/屏障明确阻止,否则具有更宽松排序的架构可能会更改顺序。

基于此,您的代码片段无法在 x86 上重新排序商店,因为商店不会在那里相互重新排序,但它可能能够在 arm 上这样做。如果在这种情况下语言需要强排序,编译器将不得不决定它是否可以依赖硬件,或者添加一个栅栏。无论哪种方式,任何从另一个线程(或套接字)读取此值的人都必须窥探它,并且只能看到响应的写入。

于 2015-05-17T23:58:34.970 回答