Kum*_*edi 8 c++ x86-64 llvm memory-model stdatomic
论文N4455 No Sane Compiler Will Optimize Atomics讨论了编译器可以应用于原子的各种优化。在有关原子的优化部分,对于 seqlock 示例,它提到了在 LLVM 中实现的转换,其中 afetch_add(0, std::memory_order_release)变成了 amfence后跟一个普通负载,而不是通常的lock addor xadd。这个想法是这样避免了对缓存行的独占访问,并且相对便宜。将mfence仍需要无论供给,以防止序限制的StoreLoad重新排序的mov生成的指令。
无论顺序如何,都会为此类操作执行此转换read-don't-modify-write,并为fetch_add(0, memory_order_relaxed).
但是,我想知道这是否合法。C++ 标准在[atomic.order]下明确指出:
原子读-修改-写操作应始终读取在与读-修改-写操作关联的写之前写入的最后一个值(按修改顺序)。
安东尼威廉姆斯之前也注意到了有关 RMW 操作看到“最新”值的事实。
我的问题是:基于原子变量的修改顺序,基于编译器是否发出lock addvsmfence后跟普通加载,线程可以看到的值的行为是否存在差异?这种转换是否可能导致执行 RMW 操作的线程加载比最新值更旧的值?这是否违反了 C++ 内存模型的保证?
(我不久前开始写这篇文章,但陷入了停滞;我不确定它是否能构成完整的答案,但我认为其中一些内容可能值得发布。我认为@LWimsey 的评论可以更好地获取比我写的更接近答案的核心。)
\n是的,很安全。
\n请记住,as-if 规则的应用方式是,真实机器上的执行必须始终生成与 C++ 抽象机上可能的执行相匹配的结果。优化使 C++ 抽象机允许在目标上不可能执行的某些执行是合法的。例如,即使是针对 x86 进行编译,也无法进行所有 IRIW 重新排序,无论编译器是否喜欢。(见下文;某些 PowerPC 硬件是唯一可以在实践中做到这一点的主流硬件。)
\n我认为专门针对 RMW 的措辞的原因是它将负载与 ISO C++ 要求每个原子对象单独存在的“修改顺序”联系起来。(或许。)
\n请记住,C++ 正式定义其排序模型的方式是根据同步以及每个对象的修改顺序的存在(所有线程都可以达成一致)。 与硬件不同,硬件具有一致缓存的概念1创建每个核心访问的单个一致内存视图。连贯共享内存的存在(通常使用 MESI 来始终保持连贯性)使许多事情变得隐含,例如无法读取“陈旧”值。(尽管硬件内存模型通常会像 C++ 那样明确地记录它)。
\n因此,转变是安全的。
\nISO C++ 在另一节的注释中确实提到了一致性的概念: http: //eel.is/c++draft/intro.races#14
\n\n\n由评估 B 确定的原子对象 M 的值应为修改 M 的某些副作用 A 存储的值,其中 B 不会在 A 之前发生。
\n
\n[注 14:此类副作用的集合也是受到此处描述的其余规则的限制,特别是受到下面的一致性要求的限制。\xe2\x80\x94 尾注]...
\n[注释 19:前面的四个一致性要求实际上不允许编译器将原子操作重新排序到单个对象,即使这两个操作都是宽松加载。这有效地使大多数硬件提供的缓存一致性保证可用于 C++ 原子操作。\xe2\x80\x94 尾注]
\n[注释20:原子负载观察到的值取决于\n\xe2\x80\x9之前发生的\xe2\x80\x9d关系,该关系取决于原子负载观察到的值。预期的解读是,原子负载与他们观察到的修改之间必须存在关联,\n加上适当选择的修改顺序和如上所述导出的 \xe2\x80\x9d 关系之前的 \xe2\x80\x9d 关系,满足此处施加的\n结果约束。\xe2\x80\x94 尾注]
\n
因此 ISO C++ 本身指出缓存一致性提供了一些排序,并且 x86 具有一致性缓存。(抱歉,我并没有完整地论证这是足够的排序。LWimsey 关于修改顺序中最新的含义的评论是相关的。)
\n(在许多 ISA(但不是全部)上,当您存储到 2 个单独的对象时,内存模型还排除了IRIW 重新排序。(例如,在 PowerPC 上,2 个读取器线程可能不同意 2 个存储到 2 个单独对象的顺序)。很少有实现可以创建这样的重新排序:如果共享缓存是数据在内核之间传输的唯一方式(就像在大多数 CPU 上一样),那么就会创建存储顺序。)
\n\n\n此转换是否有可能导致执行 RMW 操作的线程改为加载早于最新值的值?
\n
特别是在 x86 上,这很容易推理。x86 具有强有序内存模型(TSO = 总存储顺序 = 程序顺序 + 具有存储转发功能的存储缓冲区)。
\n脚注 1:所有std::thread可以跨运行的核心都具有一致的缓存。对于跨所有 ISA 的所有实际 C++ 实现都是如此,而不仅仅是 x86-64。有一些异构板具有单独的 CPU 共享内存而没有缓存一致性,但同一进程的普通 C++ 线程不会在这些不同的内核上运行。有关更多详细信息,请参阅此答案。