【问题标题】:Do we ever need memory barriers with C++ atomics on Intel x86?我们是否需要在 Intel x86 上使用 C++ atomics 的内存屏障?
【发布时间】:2015-11-13 07:28:24
【问题描述】:

我认为只有字符串和(某些)流指令需要 Intel x86 上的内存屏障?对于所有其他指令,英特尔强内存排序模型可确保实现一致性?

假设以上是正确的,当我们的代码只在 Intel x86 上执行时,为什么我们需要使用 C++ 原子(不包括比较和交换)?

我真的让自己感到困惑,我们需要在哪里使用原子,以及使用原子是否会由于内存障碍以及整个 MESI 协议而抑制乱序执行。

MESI 只是确保缓存在所有处理器之间保持一致?

内存屏障在其他架构上很有用,因为它们会将 CPU 存储缓冲区刷新到缓存中,以允许 MESI 确保一致性?

我们什么时候需要在 Intel X86 CPU 上使用原子?

【问题讨论】:

  • 假设以上是正确的 - 它不是。即使是获取/释放语义,英特尔也需要障碍。 x86 为实际编程提供了令人难以置信的昂贵且同时仍然相当有限的排序保证。基本上你需要一个用于 StoreLoad 屏障的屏障(mfence,锁定指令)。
  • @Voo:我不是专家,但 AFAIK 并不经常需要 StoreLoad 屏障。我不明白如何将 x86 内存排序描述为“相当有限的实际编程”。对于实际编程I 通常需要发布/获取语义,英特尔提供的语义没有任何明确的障碍。
  • @Voo:IDK 如果我们谈论的是同一件事。我所说的是具有获取语义的操作和具有释放语义的操作。它既不需要 StoreLoad 屏障也不需要完整的栅栏。并且 GCC 不会std::atomic<int>::store(x, memory_order_release) 之后生成 mfence。见goo.gl/XcozsM
  • @Voo:mfence 当然是完整屏障所必需的。获取/释放不需要完整的屏障。因此,获取/释放不需要 mfence。这并不意味着它完全没用。这只意味着我们不需要它,只要我们可以只用获取和释放语义来做我们需要的事情。 ps:更好的例子,包括获取和释放之间的负载:goo.gl/oJjVXG

标签: c++ multithreading assembly concurrency atomic


【解决方案1】:

您需要忘记编译所针对的特定架构。您正在编写 C++,这意味着您只能依赖特定 C++ 实现为您提供的保证。这应该当然包括标准保证的任何内容。此外,您可能会从您的特定 C++ 实现中获得一些额外的保证。

原因是 C++ 实现几乎可以做任何事情,只要它不违反 C++ 标准的规则。这意味着它可能会“删除”或“破坏”您正在编译的平台将“正常”提供的一些保证(意思是:当您在汇编程序中对其进行编程时)。例如。它可能会在您不期望的地方使用 string/MMX/SSE/SSE2/... 指令、重新排序指令、合并写入、将非原子数据放置在不适合原子性对齐的地址等处。

话虽如此,至少有一种 C++ 实现可以为您提供关于内存排序的相当强的额外保证,那就是 Visual C++。它保证易失性加载总是具有获取语义,易失性存储总是具有释放语义。 (至少在具有默认编译器设置的 x86/amd64 上。)它还保证正确对齐的易失性读写是原子的。详情请见MSDN

【讨论】:

  • Visual C++ 以 ARM(可能还有 MIPS)为目标至少十年了。 Windows CE 和更高版本的 Windows Phone 需要它。
  • @MSalters,感谢您的提示,我删除了猜测。
  • 您可以参考/volatile。这解释了 ARM、x86 和 x64 的设置以及默认设置。
  • 完成。但这表明我最初的猜测毕竟是真的。控制该扩展的/volatile 开关是VS 2012 中的新功能,IIRC 是第一个提供非x86 编译器的(非“嵌入式”)VS 版本。所以 VC++ 可能在内部针对其他平台,但没有这样的编译器在普通 VS 版本中公开可用。
  • ARM 编译器确实没有在 Visual Studio for Desktop 产品中提供。但是 Visual Studio 的最后一个“嵌入式”版本是 eVC++ 4.0。 VS2005 没有附带 ARM 编译器,但该编译器作为 Windows Mobile SDK 的一部分提供,并与 VS2005 集成。 VS2012 的变化是针对 Windows Phone 8,它不再是 Windows CE 系列的一部分,但现在在 Windows NT 系列上。这也意味着不需要单独的 SDK,因此您也无法从那里获得 ARM 编译器。
【解决方案2】:

我认为只有字符串和(某些)流指令需要 Intel x86 上的内存屏障?对于所有其他指令,英特尔强内存排序模型可确保实现一致性?

这是真的在汇编语言级别

假设以上是正确的,当我们的代码只在 Intel x86 上执行时,为什么我们需要使用 C++ 原子(不包括比较和交换)?

您还会如何执行比较和交换等原子操作?原子性和内存可见性几乎没有任何关系。

我真的让自己感到困惑,我们需要在哪里使用原子,以及使用原子是否会由于内存障碍以及整个 MESI 协议而抑制乱序执行。

我们需要在需要原子性的地方使用原子。我们需要在需要内存可见性的地方使用内存屏障,因为即使 x86 的汇编语言内存模型可能不需要它们,也不能保证这会“通过”编译器无缝地到达 C++ 级别。

【讨论】:

    【解决方案3】:

    虽然 X86 是缓存一致的,但这并不意味着它可以为您提供您期望找到的保证。原子保存和常规保存有不同的指令,它们的行为也不同。 同样重要的是,原子变量可以防止“破坏性”编译器优化。没有这些,编译器很容易根据单线程执行模型优化你的代码,你的程序就会行为不端。

    【讨论】:

    • 原子操作是否可以防止多线程程序中的处理器重新排序问题,或者是否需要内存屏障操作?
    • 它们在相应使用时会这样做。您可以将 6 种不同的内存模型与原子一起使用,其中一种是“宽松”的,它根本不给您任何保证。默认(顺序)确保“完全重新排序块”(关于加载和存储)。实际上,从语言上讲,您要么使用栅栏,要么使用原子——原子和内存模型从不提及栅栏(这是实现细节),相反,它们谈论的是顺序和可预测性。
    • @SergeyA:“原子保存和常规保存的指令不同,它们的行为不同” 是什么意思? AFAIK 如果地址对齐,则 x86 上的正常加载/存储始终是原子的,每个加载都有获取语义和每个存储释放语义。除了 CAS 之外,这几乎就是您所需要的。此外,您所说的内存模型实际上是“内存顺序”。
    • @Paul,我正在回答被问到的问题。非芯片特定,而是一个没有特定芯片亲和力的 C++ 问题。如果是这样的话,我希望你同意我的回答。至于术语,你纠正我是对的。记忆模型来自我内心深处。
    • @SergeyA:啊,好的。您的第一句话明确提到了 x86,所以我假设当您写 "there are different instructions for ..." 时,您仍然指的是 x86(="x86 has different instructions...")。然而,我可以看到人们也可以用不同的方式解释它,即“C++ 有不同的指令......”。我仍然觉得措辞有点混乱。
    猜你喜欢
    • 2018-10-02
    • 1970-01-01
    • 2011-10-12
    • 2011-06-09
    • 1970-01-01
    • 2017-01-05
    • 2012-09-08
    • 2014-01-20
    相关资源
    最近更新 更多