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

Is the transformation of fetch_add(0, memory_order_relaxed/release) to mfence + mov legal?

论文N4455 No Sane Compiler Would Optimize Atomics talks about various optimizations compilers can apply to atomics. Under the section 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 操作看到 'latest' 值的事实也已被 Anthony Williams previously 注意到。

我的问题是:基于原子变量的修改顺序,基于编译器是否发出 lock addmfence,线程可以看到的值的行为是否存在差异然后是普通负载?此转换是否可能导致执行 RMW 操作的线程改为加载比最新值更旧的值?这是否违反了 C++ 内存模型的保证?

(我刚开始写这个但是停滞了;我不确定它加起来是否是一个完整的答案,但我认为其中一些可能值得发布。我认为@LWimsey 的评论做得更好比我写的更能触及答案的核心。)

是的,很安全。

请记住,as-if 规则的应用方式是真实机器上的执行必须始终产生与 C++ 抽象机上的一种可能执行相匹配的结果。优化使 C++ 抽象机允许在目标上不可能执行某些执行是合法的。即使完全针对 x86 进行编译也会使所有 IRIW 重新排序变得不可能,例如,无论编译器是否喜欢它。 (见下文;一些 PowerPC 硬件是唯一可以在实践中做到这一点的主流硬件。)


我认为 RMW 特有措辞的原因是它将负载与 ISO C++ 要求每个原子对象分别存在的“修改顺序”联系起来。 (也许。)

请记住,C++ 正式定义其排序模型的方式是根据同步和每个对象存在的修改顺序(所有线程都可以同意)。 不像硬件那样有一致缓存的概念1创建每个核心访问的内存的单一一致视图。连贯共享内存的存在(通常使用 MESI 始终保持连贯性)使很多事情变得隐含,比如不可能读取“陈旧”值。 (虽然 HW 内存模型通常会像 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 个单独的对象时。(例如,在 PowerPC 上,2 reader 线程可能不同意2 个存储到 2 个 separate 对象的顺序。很少有实现可以创建这样的重新排序:如果共享缓存是 only 数据可以在两者之间获取的方式核心,就像在大多数 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++ 线程不会 运行ning 跨这些不同的内核。有关详细信息,请参阅 this answer