这个问题切中了移动端和服务器端底层设计的根本分歧。很多从x86转到ARM的开发者,第一反应就是:“为什么ARM没有lock cmpxchg?"。
答案很哲学:x86是复杂(CISC)的"霸道总裁”,而ARM是精简(RISC)的"优雅绅士"。前者直接给你原子指令,后者给你两条指令让你自己组合,以此换取极致的灵活性。
1. 为什么手机芯片(ARM)不直接用 CAS?
直接原因:CAS(Compare-And-Swap)是一个"读-改-写"三合一的巨无霸指令。
在RISC哲学里,一条指令只做一件事。如果强行在ARM里塞入cmpxchg,会:
- 增加晶体管开销:需要复杂的微码控制器。
- 破坏多核扩展性:未来的ARM可能几百个核,硬件的CAS逻辑会变成瓶颈。
- 功耗太高:手机对功耗极其敏感,
cmpxchg需要持续锁住总线等待结果,而ARM的LL/SC允许在失败时提前释放。
根本区别在于:
- x86的CAS:你告诉CPU “你要么成功,要么失败”,CPU内部硬扛所有冲突。
- ARM的LL/SC:CPU告诉你 “我给你做个标记,你试着改,如果我发现有人动过,我就让你重试”——把"判断冲突"的主动权交给软件。
2. 核心兵器拆解:LL(Load-Link)与 SC(Store-Conditional)
这是ARM体系(以及RISC-V、PowerPC)的原子操作基石。
指令原型(ARMv8 示例)
; 假设我们要实现原子自增: *ptr += 1
1. LL x0, [x1] ; Load-Link: 将 x1 地址的值读入 x0,并打上"独占标记"
2. ADD x0, x0, #1 ; 普通加法(此时还没写回)
3. SC x0, [x1] ; Store-Conditional: 尝试把 x0 写入 x1
; 如果成功,x0=0;如果失败(独占标记丢失),x0=1
4. CBNZ x0, retry ; 如果 x0 != 0,跳回重试
硬件执行流程(独占监视器机制)
[ 核心A ] [ 核心B ] [ 全局监视器 ]
| | |
|--- 1. LL (读取地址P) -------------->| ---------------------------> | 记录: 核心A 正在监视 地址P
| (核心A: 拿到数据10) | | (Exclusive Monitor 置位)
| | |
| (此时核心A做加法,但没写) |--- 对同一地址P 执行写操作 --> | 检测到冲突!
| | (核心B: 写入11) | 标记: 核心A 的监视失效
| | |
|--- 3. SC (尝试写回11) ------------->| ---------------------------> | 检查: 核心A 的监视还在吗?
| (返回失败: x0=1) | | 回复: 不在 (已失效)
| | |
|--- 4. 检测失败 -> 跳转重试 | |
3. 硬件独占监视器(Exclusive Monitor)的工作原理
这是LL/SC的灵魂。它不是锁,而是一个监听状态的触发器。分为两级:
本地监视器(Local Monitor)
- 位于每个CPU核心内部。
- 执行
LL时,记录下地址标签(通常是地址的哈希,而非完整地址)。 - 执行
SC时,检查本地是否还有这个标签。
全局监视器(Global Monitor / Snoop Unit)
- 位于**总线互联(Interconnect)**上。
- 当其他核心执行写操作时,总线会广播 “Invalidate” 消息。
- 全局监视器收到消息后,会清空对应核心的本地监视器标记。
关键特性(面试陷阱):
监视器不阻塞总线!它只是"贴标签"。其他核心写数据时,不需要等待监视器解锁,直接覆盖,只是在覆盖的同时撕掉你的标签。
4. LL/SC 的致命弱点(也是它的特点)
| 问题 | 原因 | 应对策略 |
|---|---|---|
| 伪失败(Spurious Failure) | 监视器可能因为上下文切换、缓存驱逐、中断处理而提前失效,即使没人改数据,SC 也会失败。 |
重试循环(所以ARM的CAS比x86稍慢) |
| 活锁(Live Lock) | 多线程同时LL/SC,互相撕标签,导致谁都成功不了。 | 引入指数退避(Exponential Backoff) 或 随机延迟 |
| 指令数量多 | 至少3条指令(LL, 计算, SC, 判断)。 | ARMv8.1 引入了原子指令扩展(LSE),硬件实现了原子CAS(后面讲) |
C++ 代码映射(ARM底层无 lock 前缀,而是生成 LL/SC 循环)
#include <atomic>
std::atomic<int> counter{0};
void arm_increment() {
int expected, desired;
do {
// 底层编译为:
// 1. LDXR (Load Exclusive, 即 LL)
// 2. ADD
// 3. STXR (Store Exclusive, 即 SC)
expected = counter.load(std::memory_order_relaxed);
desired = expected + 1;
} while (!counter.compare_exchange_weak(expected, desired,
std::memory_order_release, std::memory_order_relaxed));
}
反汇编(ARM64):
.Lretry:
LDXR w0, [x1] ; LL: 加载独占
ADD w0, w0, #1
STXR w2, w0, [x1] ; SC: 条件存储,w2 返回 0(成功) 或 1(失败)
CBNZ w2, .Lretry ; 如果失败,重试
5. 重大升级:ARMv8.1 的 LSE(Large System Extensions)
手机芯片越来越强(如苹果M系列、骁龙8系),软件开发者抱怨LL/SC太慢。ARM在v8.1版本直接硬加入了原生原子指令,抹平了与x86的差距。
新增指令(硬件实现CAS)
; 直接在硬件完成 Compare-And-Swap
CAS w0, w1, [x2] ; 比较 w0 和 [x2],如果相等,将 w1 写入 [x2]
; 这条指令和 x86 的 lock cmpxchg 行为完全一致!
C++ 行为变化:
- 如果你的手机CPU支持ARMv8.1(现代旗舰都支持),
std::atomic::compare_exchange不再编译为LL/SC循环,而是直接编译为单条CAS指令。 - 性能提升 30%~50%,且功耗更低。
6. 终极对比表:x86 CAS vs ARM LL/SC
| 维度 | x86 (lock cmpxchg) | ARMv8.0 (LL/SC) | ARMv8.1+ (LSE CAS) |
|---|---|---|---|
| 指令数量 | 1条(微码实现) | 3~4条(循环) | 1条(硬连线) |
| 总线占用 | 直到完成(可能阻塞) | 仅写回时检查(不阻塞) | 同x86 |
| 失败处理 | 硬件自动重试内部微码 | 软件显式重试(暴露给C++) | 硬件自动重试 |
| 功耗 | 较高 | 低(失败无需回滚) | 中等 |
| ABA问题 | 存在 | 存在(但监视器会部分缓解) | 存在 |
| C++语义 | compare_exchange_strong |
compare_exchange_weak(推荐) |
同x86 |
7. 给你的"高性能底层心法"
- 跨平台写C++时:永远用
compare_exchange_weak(而非strong),因为它在ARM上映射为LL/SC的一次尝试,失败后让C++循环重试;而strong在ARM上会编译成LL/SC+内部自旋,性能反而差。 - 在x86上:
weak和strong没区别(底层都是cmpxchg)。 - 检测CPU特性:
// 运行时检测 ARM LSE 是否支持 (Linux) if (getauxval(AT_HWCAP) & HWCAP_ATOMICS) { // 使用原生 CAS,性能起飞 }