原子访问、READ_ONCE与普通变量访问的性能差别
一、每CPU变量的访问速度、原子变量的访问速度、普通变量的访问速度,在Arm64 Armv8.5上有多大区别?
ARM64 ARMv8.5 上三种变量访问速度对比
1. 指令级别对比
1.1 普通变量读写
// 读 ldr w0, [x1] // 1 条指令,1 cycle(L1 命中时) // 写 str w0, [x1] // 1 条指令,1 cycle
开销:1 条 load/store 指令,无内存屏障,无额外操作。
1.2 Per-CPU 变量读写
this_cpu_read 在 ARM64 上展开为:
// __kern_my_cpu_offset 读 TPIDR_EL1 系统寄存器获取 per-cpu 偏移 mrs x2, tpidr_el1 // 1 条指令,通常 1 cycle(系统寄存器读) // 加上 per-cpu 变量的基地址偏移 add x3, x2, #offset // 1 条指令 // 实际读取 ldr w0, [x3] // 1 条 load
但 this_cpu_read() 还需要包裹 preempt_disable/enable:
preempt_disable_notrace(); // 内核中通常是 preempt_count++ (一次 RMW 或简单 store) __retval = READ_ONCE(*raw_cpu_ptr(&pcp)); preempt_enable_notrace(); // preempt_count-- + 可能的 reschedule 检查
总开销:
----------------------------------------------------------------------------------------------- 变体 指令数 说明 ----------------------------------------------------------------------------------------------- raw_cpu_read(无抢占保护) ~3 条 mrs + 地址计算 + ldr this_cpu_read(有抢占保护) ~7-10 条 preempt_disable + mrs + ldr + preempt_enable __this_cpu_read(已禁抢占) ~3 条 等同于 raw_cpu_read -----------------------------------------------------------------------------------------------
1.3 原子变量操作
ARM64 有两种原子指令实现方式,ARMv8.5 必定支持 LSE (Large System Extensions):
(1) LSE 原子指令(ARMv8.1+ 引入,ARMv8.5 必有):
// atomic_add (fire-and-forget,不返回旧值) stadd w0, [x1] // 1 条指令,但需要独占缓存行 // atomic_add_return (需要返回旧值) ldadd w0, w2, [x1] // 1 条指令,但有隐式内存序 // atomic_add_return_acquire ldadda w0, w2, [x1] // acquire 语义 // atomic_add_return_release ldaddl w0, w2, [x1] // release 语义 // atomic_add_return (full barrier) ldaddal w0, w2, [x1] // acquire + release
(2) LL/SC 原子指令(旧路径,ARMv8.5 不走这条路):
prfm pstl1strm, [x1] // prefetch for store 1: ldxr w0, [x1] // load exclusive add w0, w0, w2 // 操作 stxr w3, w0, [x1] // store exclusive cbnz w3, 1b // 失败重试
2. 延迟对比(典型 Cortex-A76/A78/X1 级别核心,L1 命中)
2.1 对比表格
-------------------------------------------------------------------------------------------------------------------- 操作 指令 延迟 (cycles) 说明 -------------------------------------------------------------------------------------------------------------------- 普通变量读 ldr 1 纯 L1 load,无额外开销 普通变量写 str 1 store buffer 吸收 READ_ONCE / WRITE_ONCE ldr / str + compiler barrier 1 只阻止编译器优化,无硬件开销 per-cpu 读 (raw_cpu_read) mrs tpidr_el1 + ldr 2-4 mrs 读系统寄存器 ~1-3 cycle per-cpu 读 (this_cpu_read) 上述 + preempt_disable/enable 7-12 preempt_count 操作是本CPU缓存行,L1命中 atomic_read ldr (普通 load) 1 仅是 READ_ONCE,无原子指令 atomic_set str (普通 store) 1 仅是 WRITE_ONCE atomic_add (LSE, fire-and-forget) stadd ~4-6 需要获取缓存行独占态(E/M) atomic_add_return (LSE, relaxed) ldadd ~4-6 需独占 + 返回旧值 atomic_add_return (LSE, full) ldaddal ~12-20+ 独占 + acquire + release 屏障 atomic_cmpxchg (LSE) cas / casal ~4-20 取决于内存序 --------------------------------------------------------------------------------------------------------------------
2.2 单核无竞争时
----------------------------------------------------------- 读 写/RMW ----------------------------------------------------------- 普通变量 1 cycle 1 cycle Per-cpu (raw) 2-4 cycles 2-4 cycles Per-cpu (this_cpu) 7-12 cycles 7-12 cycles atomic_read/set 1 cycle 1 cycle atomic_add (LSE) — 4-6 cycles -----------------------------------------------------------
2.3 多核有竞争时
------------------------------------------------------------------------------------------ 有竞争时额外开销 ------------------------------------------------------------------------------------------ 普通变量 无保护,数据竞态(UB) Per-cpu 变量 无竞争(每个 CPU 独立副本,不存在跨核竞争) atomic (LSE, relaxed) +10-100 cycles(缓存行 bouncing,MESI 协议跨核通信) atomic (LSE, full barrier) +50-200+ cycles(屏障 + 缓存一致性流量) ------------------------------------------------------------------------------------------
3. 速度排序
速度排序(快→慢): 单核无竞争: 普通变量 ≈ atomic_read/set > raw_cpu_read > this_cpu_read > atomic_add(LSE) 多核有竞争(实际系统中最重要的场景): per-cpu 变量 >>> 普通变量(需外部锁保护) >> atomic_add(relaxed) > atomic_add(full) ↑ 每CPU独立无竞争 ↑ 锁本身是原子操作 ↑ 缓存行 bouncing
二、per-cpu 变量的读/写,和做一次乘法+除法+赋值,哪个耗时长?
1. Per-CPU 变量读取(this_cpu_read)
// preempt_disable (preempt_count++) ldr w0, [x18, #offset] // 读 preempt_count(x18 常被用作 current_task 指针) add w0, w0, #1 str w0, [x18, #offset] // 写 preempt_count // 实际读取 mrs x1, tpidr_el1 // 取 per-cpu offset (~1-3 cycles) ldr w2, [x1, #var_offset] // 实际 load // preempt_enable (preempt_count--) ldr w0, [x18, #offset] sub w0, w0, #1 str w0, [x18, #offset] // 可能还有 reschedule 检查: // cbnz w0, skip // bl preempt_schedule
约 7-10 条指令,6-12 cycles(全部 L1 命中,无分支误判)
注: 在特定的上下文下,有可能不需要关/开抢占。
2. 乘法 + 除法 + 赋值
mul x0, x1, x2 // 乘法:3-5 cycles (Cortex-A76/A78 整数乘) udiv x0, x0, x3 // 除法:7-12 cycles (整数除法最慢) str x0, [x4] // 赋值:1 cycle
3 条指令,11-18 cycles
3. 两者对比
-------------------------------------------------------------------------- 操作 指令数 延迟 (cycles) -------------------------------------------------------------------------- this_cpu_read() 7-10 6-12 raw_cpu_read()(已禁抢占) 2-3 2-4 乘法 + 除法 + 赋值 3 11-18 --------------------------------------------------------------------------
4. 结论
乘法+除法+赋值更耗时,主要因为整数除法 udiv 在 ARM64 上是 7-12 cycles(具体取决于操作数位宽和核心微架构)。即使 this_cpu_read 指令数更多,但每条都是 1 cycle 的简单 load/store/add。
如果是 raw_cpu_read(在已经禁抢占的上下文中,比如持有 spinlock 时),那 per-cpu 读只需 2-4 cycles,远快于一次除法。
不过需要注意:如果除法可以被编译器优化为移位(除以 2 的幂次)或乘以逆元(除以常数),那耗时会大幅降低到 1-3 cycles,此时反而比 this_cpu_read 快。
三、READ_ONCE/WRITE_ONCE 和 atomic_read/atomic_set 的性能真的和普通变量的读写是一致的吗
回答:是的,硬件执行层面完全一致。
1. 从源码可以直接确认:
// include/asm-generic/rwonce.h #define __READ_ONCE(x) (*(const volatile __unqual_scalar_typeof(x) *)&(x)) #define __WRITE_ONCE(x, val) (*(volatile typeof(x) *)&(x) = (val)) // arch/arm64/include/asm/atomic.h #define arch_atomic_read(v) __READ_ONCE((v)->counter) #define arch_atomic_set(v, i) __WRITE_ONCE(((v)->counter), (i))
atomic_read 就是 READ_ONCE,atomic_set 就是 WRITE_ONCE。 三者生成完全相同的机器码。
2. 生成的汇编对比
int a; atomic_t b; // 以下三者生成 **完全相同** 的 ldr/str 指令: x = a; → ldr w0, [x1] x = READ_ONCE(a); → ldr w0, [x1] x = atomic_read(&b); → ldr w0, [x1]
在 ARM64 上,对齐的 32-bit/64-bit load/store 本身就是原子的(架构保证),不需要任何额外指令。
3. volatile 影响了什么
EAD_ONCE/WRITE_ONCE 的 volatile 转换影响的是编译器行为,不是 CPU 执行:
---------------------------------------------------------------------------------------------------- 影响 普通变量 READ_ONCE / atomic_read ---------------------------------------------------------------------------------------------------- 编译器可以缓存到寄存器不重新读 ✓ ✗ 每次必须从内存读 编译器可以合并多次读为一次 ✓ ✗ 禁止合并 编译器可以把写移到循环外 ✓ ✗ 禁止移动 编译器可以删除"无用"的读 ✓ ✗ 禁止删除 编译器可以跨语句重排 ✓ ✗ 同一变量的 volatile 访问保序 生成的单条 ldr/str 指令 相同 相同 CPU 执行延迟 相同 相同 ----------------------------------------------------------------------------------------------------
4. 什么时候会有真正的性能差异
volatile 导致的"间接"性能损失:
(1) 场景 1:循环中反复读
// 普通变量 — 编译器可以把读提到循环外,只读一次 for (int i = 0; i < 1000; i++) { if (flag) // 编译器:flag 不变,提升为循环不变量 do_something(); } // READ_ONCE — 编译器每次循环迭代都必须从内存重新 load for (int i = 0; i < 1000; i++) { if (READ_ONCE(flag)) // 每次都生成 ldr,1000 次 load do_something(); }
(2) 场景 2:连续多次读同一变量
a = x + x + x; // 普通变量:编译器优化为 a = 3 * x(只读一次) // READ_ONCE(x) + READ_ONCE(x) + READ_ONCE(x):3 次 ldr(如果确实需要3次独立读的语义)
这些差异来自编译器优化被抑制,不是硬件层面的差异。单次访问的硬件开销完全相同。
5. 总结
---------------------------------------------------------------------------------------------------- 硬件指令 单次延迟 编译器优化 ---------------------------------------------------------------------------------------------------- 普通变量读写 ldr/str 1 cycle 可优化(缓存寄存器、合并、消除、重排) READ_ONCE/WRITE_ONCE ldr/str 1 cycle 禁止优化(每次必须真正访存) atomic_read/atomic_set ldr/str 1 cycle 禁止优化(等同于 READ/WRITE_ONCE) ----------------------------------------------------------------------------------------------------
单次访问:三者硬件开销完全一致,都是一条 ldr 或 str,1 cycle(L1 命中时)。差异只在于编译器能否把多次访问优化掉。若每次判断只读一次,用 READ_ONCE 和直接读普通变量在最终生成的代码上没有区别 —— 但 READ_ONCE 在语义上更正确,告诉编译器"这个值可能被其他 CPU 修改,不要假设它不变"。
四、READ_ONCE/WRITE_ONCE 和 atomic_read/atomic_set 的性能真的和普通变量的读写是一致的吗
1. "原子读写"和"原子 RMW"
---------------------------------------------------------------------- 操作 实际含义 指令 ---------------------------------------------------------------------- atomic_read 就是 READ_ONCE ldr atomic_set 就是 WRITE_ONCE str atomic_add 原子 read-modify-write stadd(LSE) atomic_add_return 原子 RMW + 返回值 ldadd atomic_cmpxchg 原子比较交换 cas ----------------------------------------------------------------------
前两个和普通变量无差别。后三个即使 L1 命中也有额外开销,原子 RMW 即使 L1 命中也更慢的原因:
普通 store: CPU pipeline --> store buffer --> L1 cache ~1 cycle 原子 stadd/ldadd (LSE, 数据已在本核 L1, Exclusive/Modified 态): CPU pipeline --> 锁定 cache line --> read --> modify --> write --> 释放 ~4-6 cycles
原因:
(1) Cache line 锁定:原子 RMW 需要在 L1 cache 控制器层面保证读-改-写三步操作不可中断。硬件必须在操作期间锁定该 cache line,阻止任何其他核的 snoop/invalidate 请求被响应。
(2) 流水线停顿:普通 str 可以直接丢进 store buffer,CPU 继续执行后续指令(store buffer 是异步写回)。原子 RMW 不能使用 store buffer 的异步模型——必须保证 R-M-W 是不可分割的,导致 pipeline 等待操作完成。
(3) 内存序约束(如果带 acquire/release):ldaddal(full ordered)还需要刷 store buffer 并阻止后续 load 的推测执行,这会额外增加 6-15+ cycles。
2. L1 命中时的实测延迟对比
------------------------------------------------------------------------------------------------------- 操作 指令 L1 命中延迟 原因 ------------------------------------------------------------------------------------------------------- 普通变量读 ldr 1 cycle 纯 load,pipeline 不停 普通变量写 str 1 cycle 丢进 store buffer 即返回 READ_ONCE ldr 1 cycle 同普通 load WRITE_ONCE str 1 cycle 同普通 store atomic_read ldr 1 cycle 就是 READ_ONCE atomic_set str 1 cycle 就是 WRITE_ONCE atomic_add (relaxed) stadd 4-6 cycles cache line 锁定 + RMW atomic_add_return (relaxed) ldadd 4-6 cycles 同上 + 返回旧值 atomic_add_return_acquire ldadda 6-10 cycles + acquire 屏障 atomic_add_return (full) ldaddal 12-20 cycles + 刷 store buffer + 阻止推测 atomic_cmpxchg (relaxed) cas 4-8 cycles 比较 + 条件写 atomic_cmpxchg (full) casal 12-20 cycles + 全屏障 -------------------------------------------------------------------------------------------------------
3. 多核场景下更大的差异
即使 L1 命中也分两种情况:
------------------------------------------------------------------------------------------------------------ Cache line 状态 含义 原子 RMW 额外开销 ------------------------------------------------------------------------------------------------------------ Exclusive/Modified(独占) 只有本核持有 4-6 cycles(最好情况) Shared(共享) 多核都有副本 +30-100 cycles(必须先 invalidate 其他核的副本,获得独占权) ------------------------------------------------------------------------------------------------------------
atomic_add 必须先拿到 cache line 的 Exclusive 态才能写入。如果该 cache line 同时被其他核持有(Shared 态),需要通过 interconnect 发 invalidate 请求,等所有其他核 ack 后才能继续。
而普通 str/WRITE_ONCE 也需要独占态,但区别是它可以把请求丢进 store buffer 后立即返回(由硬件异步完成 invalidate),CPU 不会阻塞。原子 RMW 不行,因为它必须同步等待独占权确认。
4. 结论
------------------------------------------------------------------------------------------------------------------------- 操作类型 L1命中 + 独占态 L1命中 + 共享态 ------------------------------------------------------------------------------------------------------------------------- ldr / READ_ONCE / atomic_read 1 cycle 1 cycle(读不需要独占) str / WRITE_ONCE / atomic_set 1 cycle(store buffer) 1 cycle(store buffer 异步处理) atomic_add (LSE relaxed) 4-6 cycles 30-100+ cycles atomic_add_return (full ordered) 12-20 cycles 50-200+ cycles -------------------------------------------------------------------------------------------------------------------------
纯读/纯写(atomic_read/atomic_set/READ_ONCE/WRITE_ONCE) = 普通变量,无任何差别。原子 RMW 操作(atomic_add/atomic_inc/cmpxchg)即使 L1 命中也比普通操作慢 4-20 倍,因为硬件需要保证 read-modify-write 的原子性。
五、内存屏障相关对性能影响
1. 不同 barrier 方式的对比
-------------------------------------------------------------------------------------- 方式 ARM64 生成指令 额外开销 -------------------------------------------------------------------------------------- 普通读 *ptr ldr 0 READ_ONCE(*ptr) ldr(+ 编译器 barrier) 0(只防编译器优化) smp_load_acquire(ptr) ldar ~0-1 cycle 读取 + smp_rmb() ldr + dmb ishld ~10-40 cycles(显式 barrier) 读取 + smp_mb() ldr + dmb ish ~20-60 cycles(full barrier) --------------------------------------------------------------------------------------
2. 结论
-------------------------------------------------------------------------------------------------- 场景 选择 理由 -------------------------------------------------------------------------------------------------- 单核内防编译器重排 READ_ONCE 零硬件开销 多核间需要 acquire 语义 smp_load_acquire LDAR 开销几乎为零,语义正确 多核间需要完整屏障 smp_mb() 有真实开销,仅在必要时使用 --------------------------------------------------------------------------------------------------
smp_load_acquire 在 ARM64 上的性能代价极小(接近零),不需要为了性能而避免使用它。 真正有显著开销的是 dmb ish(full memory barrier)和 dsb(data synchronization barrier),那些是几十个周期级别的代价。
posted on 2026-08-13 14:52 Hello-World3 阅读(5) 评论(0) 收藏 举报
浙公网安备 33010602011771号