c++ - fetch_add(0, memory_order_relaxed/release) 到 mfence + mov 的转换是否合法?

标签 c++ llvm x86-64 memory-model stdatomic

论文 N4455 No Sane Compiler Would Optimize Atomics讨论编译器可以应用于原子的各种优化。栏目下Optimization Around Atomics ,对于 seqlock 示例,它提到了在 LLVM 中实现的转换,其中 fetch_add(0, std::memory_order_release)变成了mfence然后是普通负载,而不是通常的 lock addxadd .这个想法是这样避免了对缓存行的独占访问,并且相对便宜。 mfence无论提供何种排序约束以防止 StoreLoad 仍然需要重新订购 mov指令生成。
transformation对此类执行 read-don't-modify-write无论顺序如何,都可以操作,并为 fetch_add(0, memory_order_relaxed). 生产等效的组件
但是,我想知道这是否合法。 C++ 标准在 [atomic.order] 下明确指出那:

Atomic read-modify-write operations shall always read the last value (in the modification order) written before the write associated with the read-modify-write operation.


关于 RMW 操作看到“最新”值的这一事实也已被注意到 previously通过安东尼威廉姆斯。
我的问题是:基于原子变量的修改顺序,基于编译器是否发出 lock add,线程可以看到的值的行为是否存在差异?对比 mfence其次是普通负载?这种转换是否可能导致执行 RMW 操作的线程加载比最新值更旧的值?这是否违反了 C++ 内存模型的保证?

最佳答案

(我不久前开始写这个,但停滞不前;我不确定它加起来是一个完整的答案,但我认为其中一些可能值得发布。我认为@LWimsey 的评论更能触及核心答案比我写的要好。)
是的,它很安全。
请记住,as-if 规则的适用方式是,在真实机器上的执行必须始终产生与 C++ 抽象机器上的一种可能执行相匹配的结果。优化使某些 C++ 抽象机允许在目标上不可能的执行是合法的。即使完全针对 x86 进行编译也会使所有 IRIW 重新排序成为不可能,例如,无论编译器是否喜欢它。 (见下文;一些 PowerPC 硬件是唯一可以在实践中做到这一点的主流硬件。)

我认为专门针对 RMW 的措辞的原因是它将负载与 ISO C++ 要求为每个原子对象分别存在的“修改顺序”联系起来。 (也许。)
请记住,C++ 正式定义其排序模型的方式是同步与每个对象的修改顺序的存在(所有线程都可以达成一致)。不像硬件那样有一致性缓存1的概念,创建每个内核访问的内存的单一一致 View 。连贯共享内存的存在(通常使用 MESI 始终保持连贯性)使许多事情变得隐含,例如无法读取“陈旧”值。 (尽管硬件内存模型通常像 C++ 一样明确记录它)。
因此转换是安全的。
ISO C++ 在另一部分的注释中确实提到了一致性的概念:http://eel.is/c++draft/intro.races#14

The value of an atomic object M, as determined by evaluation B, shall be the value stored by some side effect A that modifies M, where B does not happen before A.
[Note 14: The set of such side effects is also restricted by the rest of the rules described here, and in particular, by the coherence requirements below. — end note]

...

[Note 19: The four preceding coherence requirements effectively disallow compiler reordering of atomic operations to a single object, even if both operations are relaxed loads. This effectively makes the cache coherence guarantee provided by most hardware available to C++ atomic operations. — end note]

[Note 20: The value observed by a load of an atomic depends on the “happens before” relation, which depends on the values observed by loads of atomics. The intended reading is that there must exist an association of atomic loads with modifications they observe that, together with suitably chosen modification orders and the “happens before” relation derived as described above, satisfy the resulting constraints as imposed here. — end note]


所以 ISO C++ 本身注意到缓存一致性给出了一些排序,而 x86 有一致性缓存。 (我没有完整地论证这是足够的排序,抱歉。LWimsey 关于修改顺序中最新意味着什么的评论是相关的。)
(在许多 ISA(但不是全部)上,当您存储到 2 个单独的对象时,内存模型也会排除 IRIW reordering。(例如,在 PowerPC 上,2 个读取器线程可能会不同意 2 个存储到 2 个单独对象的顺序)。很少有实现可以创建这样的重新排序:如果共享缓存是数据可以在内核之间获取的唯一方式,就像在大多数 CPU 上一样,它会创建存储顺序。)

Is it possible for this transformation to cause the thread performing the RMW operation to instead load values older than the latest one?


特别是在 x86 上,很容易推理。 x86 有一个 strongly-ordered memory model (TSO = 总存储订单 = 程序订单 + 带有存储转发的存储缓冲区)。
脚注 1:std::thread 的所有内核可以跑过有一致的缓存。适用于所有 ISA 的所有真实 C++ 实现,而不仅仅是 x86-64。有一些具有独立 CPU 的异构板,在没有缓存一致性的情况下共享内存,但同一进程的普通 C++ 线程不会跨这些不同的内核运行。见 this answer有关更多详细信息。

关于c++ - fetch_add(0, memory_order_relaxed/release) 到 mfence + mov 的转换是否合法?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64976395/

相关文章:

c++ - 如何在C++中检查用户输入

llvm - 如何在没有优化的情况下在llvm中获得 "phi"指令

compiler-construction - LLVM 中函数的最后一个基本 block

assembly - 从 64 位 asm 调用 Printf 时如何传递参数?

c++ - Type** name 和 Type* name[] 有什么区别?

c++ - 在 QTableView 模型字段中显示图像和文本

c++ - 知道变量占用多少内存的正确方法是什么

c++ - 如何在 LLVM 模块中增加全局变量?

windows - 与 x86_64 Windows 调用约定混淆

windows - Windows x64 的 44 位虚拟内存地址限制背后