原子访问、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)    收藏  举报

导航