x86和ARM的memory ordering比较
文章目录
本文将比较x86和ARM的memory ordering的差异。
本文讨论 AArch64(Armv8-A/Armv9-A)上普通 CPU 之间的硬件 Memory Ordering。示例假定访问的是可共享、cacheable 的 Normal Memory,访问自然对齐且单次访问自身不撕裂。对照表中的不同地址访问还假定没有地址依赖、数据依赖、Acquire、Release 或 Barrier。本文不讨论编译器重排、C/C++ data race、MMIO、DMA、Device Memory、非对齐访问和 mixed-size 访问。
x86 与 ARM 的核心差异可以先浓缩成一句话:
x86 对普通内存访问提供较强的隐含顺序,主要放松不同地址的 Store→Load;Arm 对普通、独立、跨地址访问提供的隐含顺序更少,程序需要通过 Acquire、Release 或 Barrier 明确建立所需顺序。
Prerequisite
ARM 的基本模型:Weakly Ordered
AArch64 对普通 LDR、STR 提供的是较弱的顺序保证。
对于不同地址、没有依赖关系的普通访问,可以先用下面这个入门模型理解:
| 较老操作 → 较新操作 | x86 | ARM 普通 LDR/STR |
|---|---|---|
| Load → Load | 保持 | 可能表现为重排 |
| Load → Store | 保持 | 可能表现为重排 |
| Store → Store | 保持 | 可能表现为重排 |
| Store → Load | 不同地址时可能重排;同地址不能读回旧值 | 不同地址时可能表现为重排 |
这个表是便于建立直觉的简化模型。
Arm对其模型的官方概括是:
Arm 是弱顺序架构,允许内存访问以不同于程序顺序的方式被观察和完成。
与此同时,同一位置的写仍然被序列化;弱顺序并不意味着同一变量可以毫无规则地变化。
Message Passing
初始状态:
1 | data = 0 // 数据 |
生产者:
1 | CPU 0 |
消费者:
1 | CPU 1 |
我们关心:
1 | r1 == 1 |
也就是:
消费者已经看见”数据准备完成”的 flag,却仍然读到旧 data。
x86 为什么禁止这个结果?
x86 保持:
1 | Store → Store |
因此 CPU 0 对外不能表现为:
1 | flag = 1 |
x86 也保持:
1 | Load → Load |
因此 CPU 1 对外不能表现为:
1 | 先读 data |
所以在普通 x86 WB 内存的机器指令层面:
1 | flag == 1 && data == 0 |
是不允许的。
ARM 为什么可能允许?
ARM 对普通独立访问不自动保留这两条跨地址顺序:
1 | CPU 0: |
Acquire 和 Release 是怎么解决问题的?
正确的 ARM 版本可以写成:
1 | 生产者: |
Release:挡住前面的操作
Release store 的含义是:
1 | release 之前的 load/store |
图示:
1 | 之前的访问 ──────> Store-Release |
因此:
1 | data = 42 |
STLR 保证的是 Producer 侧此前对 data 的访问排在对 flag 的 release store 之前:
1 | 写 data |
它只解决发布端的顺序,并不能单独约束 Consumer。Consumer 如果仍使用两个普通 LDR,对 data 的读取仍可能在观察 flag 之前就已被满足,因此 release 单独使用仍不能排除 Message Passing 的坏结果。在本文采用的 release/acquire 方案中,Consumer 还需要通过 LDAR 观察这个 flag,再读取 data。
但 release 是单向的:
1 | Store-Release |
后面的访问并不一定被 release 阻挡。
Acquire:挡住后面的操作
Acquire load 的含义是:
1 | Load-Acquire |
图示:
1 | 之前的访问可能越过 |
所以:
1 | LDAR flag |
不允许对 data 的读取越过 acquire,跑到读取 flag 之前。
Acquire/Release 提供单向排序语义,而不是 Full Barrier。可以把它们直观地理解为单向的 ordering primitive:Acquire 约束该访问之后的操作,Release 约束该访问之前的操作。
要建立跨 CPU 的发布关系,LDAR 必须观察到 STLR 发布的值
当 Consumer 的 LDAR 读取到 Producer 的 STLR 写入的 flag == 1 时,Store-Release 和 Load-Acquire 会将以下顺序连接起来:
1 | Producer 写 data |
因此,在这个 AArch64 Message Passing 测试中:
1 | flag == 1 && data == 0 |
的执行被禁止。
C/C++ 原子内存模型中也有对应的 release/acquire 发布模式,但那属于语言层内存模型,本文不展开。
为什么生产者和消费者两边都要有顺序?
只有 release:
1 | 生产者:data = 42; store_release(flag, 1); |
生产者的发布顺序正确,但消费者仍可能把 data 的 load 提前。
只有 acquire:
1 | 生产者:store_relaxed(data, 42); store_relaxed(flag, 1); |
消费者顺序正确,但生产者可能先让 flag 可见。
所以典型发布关系需要:
1 | 生产者 release |
总结
x86 TSO
可以把 x86 想成:
1 | 每个 CPU 都有一个 FIFO store buffer |
因此最主要的硬件弱点是:
1 | Store → Load |
ARM Weak Ordering
可以把 ARM 想成:
1 | 普通 LDR/STR 对独立跨地址访问提供的隐含顺序较少 |
ARM 并不是”随机执行”,它仍然具备:
- 单线程语义;
- 同地址 coherence;
- 原子操作;
- 依赖关系约束;
- acquire/release;
- barrier;
- 正式定义的 allowed/forbidden executions。
它只是比 x86 给硬件留出了更大的优化空间。
记忆图
1 | x86 TSO |
同地址访问仍受到单地址 coherence、读己之写和相关架构规则约束。
1 | Coherence: |
参考资料:
- Intel 64 and IA-32 Architectures Software Developer’s Manual,Volume 3A
- Arm Architecture Reference Manual for A-profile architecture
- How to generate litmus tests automatically with the diy7 tool
- DPDK Optimization on Arm