Itanium 的 seq_cst fence 与 relaxed atomics 实现疑问
结论
mf 主要约束当前处理器上前后 memory accesses 的顺序。它不会等待此前所有 memory references 被 external platform 接收,也不能让普通 st 自动获得 multi-copy atomicity。可以将它理解为对 plain accesses 提供本地排序,但不会作为 cumulative fence 传播普通写入。
这并不表示 mf 在全局内存模型中没有作用。Itanium 会将 mf、st.rel 等操作纳入 release ordering。mf 与 ld.acq、st.rel 配合时可以形成传递性,其中依赖的是 release store 的 remote write atomicity,而非 mf 对此前普通写入的刷新或广播。
plain ld 和 st 无法完整实现 C++ memory_order_relaxed,主要有两个原因:
- plain
ld可能违反 C++ 对同一 atomic object 的 read-read coherence。例如,同一线程按程序顺序执行两次读取时,可能先看到新值,再看到旧值。 - plain
st不保证 release store 具有的 remote write atomicity。一次写入可能先被某个处理器观察到,随后才被另一个处理器观察到。如果 relaxed atomic 与 acquire-release 链组合使用,就可能产生 C++ 不允许的 WRC 结果。
Itanium 将 relaxed load/store 映射为 ld.acq/st.rel,是为了用更强的机器指令满足 relaxed atomic 本身要求的原子性和单对象一致性,并未给 relaxed 操作增加 C++ 层面的同步语义。
memory_order_relaxed 仍有单对象一致性要求
memory_order_relaxed 不建立 synchronizes-with 关系,也不要求不同位置之间具有 acquire/release ordering,但同一个 atomic object 上的观察结果不能任意倒退。
例如,两个 relaxed loads 按程序顺序执行:
int a = x.load(std::memory_order_relaxed);
int b = x.load(std::memory_order_relaxed);
如果第一次读取已经观察到 x 的某次较新修改,第二次读取就不能再观察到 modification order 中更早的值。这属于 C++ atomic coherence 的要求。
Itanium 的普通 ld 允许较激进的推测和重排序,包括对同一地址的 normal loads 进行重排序。若直接用普通 ld 实现上述代码,处理器可能先执行第二条 load,再执行第一条 load。两条指令的结果最终回到各自的程序位置时,程序可能表现为先读到新值,随后又读到旧值。
ld.acq 会约束其后的 memory accesses。连续使用 ld.acq,能够满足 C++ 要求的 read-read coherence。这也说明了 Itanium Linux 为什么需要让 READ_ONCE() 最终使用具有相应排序能力的 load,不能只依赖普通 load。
plain st 的跨处理器传播问题
relaxed store 需要 st.rel,可以通过下面的例子说明:
// Initially: x = 0, y = 0
// Thread 1
int r3 = y.load(std::memory_order_acquire);
int r1 = x.load(std::memory_order_relaxed);
// Thread 2
x.store(1, std::memory_order_relaxed);
// Thread 3
int r2 = x.load(std::memory_order_relaxed);
y.store(1, std::memory_order_release);
考虑以下结果:
r2 = 1
r3 = 1
r1 = 0
这个结果不符合 C++ atomic coherence:
- Thread 3 对
x的 load 读到了1。 - 该 load sequenced-before 对
y的 release store。 - Thread 1 对
y的 acquire load 读到了这个 release store。 - 因此,Thread 3 对
x的 load happens-before Thread 1 对x的 load。 - 两次 load 操作的是同一个 atomic object。前一次已经读到
1,后一次不能再读到 modification order 中更早的0。
如果 Thread 2 使用 plain st,x = 1 可能已经对 Thread 3 可见,却尚未对 Thread 1 可见。Thread 3 随后发布 y = 1,Thread 1 即使看到 y = 1,仍可能读到旧的 x = 0。这是一种典型的 WRC 传播问题。
WB memory 上的 st.rel 具有 remote write atomicity。该 release store 会进入 release operations 的全局排序,并受相应远程可见性规则的约束。某个处理器观察到这次 store 后,再通过 release/acquire 链发布后续状态时,不会产生上述违反 C++ coherence 的结果。
remote write atomicity 与 other-MCA 的区别
两者描述的现象相近,但含义并不相同。
other-MCA 通常允许写入处理器通过 store forwarding 提前观察自己的写入,同时要求该写入对其他所有处理器只有一个全局可见点。
Itanium 的 remote write atomicity 适用范围更窄:
- 它明确适用于 WB memory 上的 store-release。
- 该属性不能直接推广到 plain
st。 - 它规定远程观察者以一致顺序观察 release stores,并不要求所有普通写入都具有 MCA。
- “所有处理器按相同顺序观察”描述的是架构顺序关系,不代表所有 cache 必须在同一物理时刻完成更新。
某代 Itanium 即使因为 shared bus 或 single chipset,使普通写入在具体微架构上表现得较为原子,编译器也不能将这种实现行为视为架构保证。C++ mapping 必须以 ISA 允许的最弱行为为依据。
mf 提供的保证
mf 的语义可以分为三个层次。
1. 建立 program order
mf 约束当前处理器上 fence 前后的 data memory accesses,避免后续访问越过 fence,提前到达架构定义的可见点。
因此,在 Itanium memory model 的形式化描述中,mf 主要出现在 preserved program order 规则里。
2. 不保证此前的访问已经完成
手册明确指出,mf 不保证此前所有 memory references 已被 external platform 接收,也不保证这些 references 已经全局可见。
不能将其理解为:
执行 mf
等待所有旧写入传播到每个处理器
然后继续
mf 约束的是架构认可的 memory access ordering point,不会等待所有底层事务完成。手册中 access 和 reference 的用词不容易直接映射到现代 cumulative-barrier 术语。判断 mf 的能力边界时,应以“不保证完成或全局可见”这项明确限制为准。
3. 参与 release ordering
将 mf 称为 serializing instruction,通常是在讨论 release operations 的全局排序及其传递性。它不会为此前的每个 plain store 补上 remote write atomicity。
传递性来自以下规则的共同作用:
ld.acq / 已观察到的 release
+
当前处理器上的 ordering
+
st.rel 的 remote write atomicity
+
后继处理器的 acquire
因此,mf 对普通 memory accesses 的直接作用是增加本地 program order;当它与 release/acquire primitives 配合时,也会参与 release operations 的全局顺序。这两个描述对应不同层面的架构效果。
relaxed 操作为何使用 acquire/release 指令
Itanium mapping 中常见的概念性对应关系如下:
| C++ 操作 | Itanium 实现方向 | 需要解决的问题 |
|---|---|---|
| relaxed load | ld.acq | 防止同一对象的 load-load 倒退,满足 read-read coherence |
| relaxed store | st.rel | 提供所需的远程写原子性,并满足跨 acquire-release 链的 coherence |
| seq_cst fence | mf | 在已经使用强原子 primitives 的基础上补足前后访问顺序 |
ld.acq 和 st.rel 比 C++ relaxed 操作更强。只要没有改变程序允许观察到的值和行为,这种 over-implementation 就是允许的。
SC fence 的正确性不能只由 mf 判断,实际实现依赖以下组合:
ld.acq / st.rel 提供 atomic coherence 与远程可见性约束
+
mf 提供 fence 两侧的 program order
如果将原子访问换成 plain ld/st,仅在中间插入 mf,仍然无法获得 plain store 缺少的 remote write atomicity。
如何理解 C++20 修订
C++20 之前,标准并未普遍保证在每对 relaxed accesses 之间加入一个 memory_order_seq_cst fence,就等价于全部使用 seq_cst operations。其中一项考虑是兼容这样的架构:普通写入不具有 MCA,fence 也无法修复传播问题。
后来发现,这部分标准余量对 Itanium 没有实际作用。Itanium 的 plain accesses 无法完整满足 C++ relaxed atomics 所需的单对象 coherence,因此编译器原本就必须使用 ld.acq 和 st.rel。经过这种映射后,relaxed accesses 已经具备更强的底层属性,mf 也能够实现更强的 SC fence。
C++20 的调整,是在标准中承认现有 SC fence implementations 已经提供的能力。它没有让 mf 突然获得 cumulative propagation,也没有让 plain ld/st 足以实现 Itanium 上的 relaxed atomics。
实际编程建议
程序需要 sequential consistency 时,应直接对相关原子操作使用 std::memory_order_seq_cst:
std::atomic<int> x{0};
x.store(1, std::memory_order_seq_cst);
int value = x.load(std::memory_order_seq_cst);
不要假设 relaxed operations 加若干 fences 一定等价于 seq_cst operations,尤其是在分析旧标准、旧编译器或特殊架构时。
实现编译器后端或验证历史 mapping 时,需要分别检查:
- load 是否满足 C++ read-read coherence;
- store 是否具有 atomic modification order 所需的远程观察属性;
- release/acquire 链能否传递此前已经观察到的写入;
- fence 只建立本地 program order,还是也能使普通写入 cumulative;
- 所依赖的属性来自 ISA 保证,还是某一代芯片因总线结构偶然表现出的行为。
C++ relaxed 没有 acquire/release 同步语义,但在 Itanium 上,满足其最低要求的硬件 primitive 恰好是 ld.acq 和 st.rel。
备注:内容仅供参考。
