尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

【嵌入式linux学习】为什么原子操作开销通常比锁更小

【嵌入式linux学习】为什么原子操作开销通常比锁更小 【嵌入式linux学习】为什么原子操作开销通常比锁更小先总结一下原子操作是硬件 CPU 原生支持的单条指令锁自旋锁 / 互斥锁本质是用原子操作 循环 / 睡眠包装出来的高层逻辑天然多了很多额外工作。文章目录【嵌入式linux学习】为什么原子操作开销通常比锁更小1. 原子操作底层是什么2. 锁自旋锁为例底层是什么3. Cache-line缓存行影响原子操作场景自旋锁场景4. 开销对比总结5. 但是原子操作有巨大局限性6. 多种同步方法进行测试比较【拓】MESI协议状态流转通俗场景举例场景 1CPU0 第一次读 val场景 2CPU1 也读同一个 val场景 3CPU0 要写 valval场景 4现在 CPU1 想读 val总线嗅探snoop是什么回到之前的话题原子操作、锁为什么会触发 MESI 开销1. lock 前缀原子操作做了什么x862. 自旋锁为什么比单4纯原子操作更容易引发 MESI 风暴3. 伪共享false sharing4. MESI 的局限性总结7.总结1. 原子操作底层是什么原子操作atomic_t、atomic_inc、cmpxchg等最终编译成 CPU 硬件指令比如 x86 的lock add。lock前缀会锁住缓存行保证这一条内存操作不可被打断整个读 - 改 - 写一次性完成。 整个过程CPU 拿到对应变量所在缓存行执行带 lock 的单条指令完成修改释放缓存行独占权。只做一件事完成变量的读修改写。2. 锁自旋锁为例底层是什么自旋锁spinlock_t底层本身也要靠原子操作来抢锁。 伪代码看自旋锁spin_lock()// 抢锁用原子操作尝试把锁从0改成1 while( atomic_cmpxchg(lock, 0, 1) ! 0 ) { // 抢不到原地循环忙等自旋 cpu_relax(); }spin_unlock原子操作把锁置回 0。可以看到 自旋锁底层依赖原子指令但是多了循环重试逻辑抢不到锁就一直自旋锁变量本身的读写、缓存行竞争内存屏障保证指令顺序互斥锁mutex开销更大抢不到锁的时候进程会睡眠、上下文切换、放入等待队列、唤醒。上下文切换要保存 / 恢复寄存器、换页表这是非常重的开销。3. Cache-line缓存行影响CPU 缓存是以缓存行64 字节为单位工作的。 当多核 CPU 修改同一个缓存行里的数据会触发缓存一致性协议MESI核之间来回传递缓存行状态失效、共享、独占这是多核最大的性能杀手。原子操作场景只修改目标原子变量这一个缓存行。一次 lock 指令短暂独占缓存行修改完成立刻释放。操作对象只有业务变量本身。自旋锁场景有两个变量在参与竞争业务变量你真正要保护的数据锁变量本身锁变量是独立变量大概率也占用一个缓存行多核抢锁的时候锁变量所在缓存行会疯狂的在各个核之间来回失效、同步。 哪怕大家只是抢锁、还没操作业务数据MESI 协议就已经在不停抖动缓存行。锁带来了额外的缓存行竞争。这就是原文说原子操作对 cache-line 影响更小。举个例子多个核做计数器累加方案 Aatomic_inc(cnt)只争cnt这一个缓存行。方案 B自旋锁保护cnt多核抢锁的时候锁变量的缓存行持续乒乓失效额外引入大量 MESI 流量。4. 开销对比总结指令层面原子操作单条硬件原子指令。 锁原子操作 循环 / 等待 内存屏障更多指令。mutex 还会有进程睡眠、唤醒、上下文切换。缓存一致性开销原子操作只竞争业务变量的缓存行。 锁锁变量本身也会引发多核缓存行竞争额外增加 MESI 开销。阻塞特性原子操作永远不会睡眠不会发生进程上下文切换。 mutex抢不到锁进程休眠上下文切换开销巨大。 spinlock抢不到锁原地自旋空耗 CPU。5. 但是原子操作有巨大局限性原子操作只能保护单个整型变量的简单操作自增、加减、比较交换。 如果你的临界区是多条语句、多个变量原子操作无能为力必须使用锁。例子// 可以原子单变量自增 atomic_inc(ref); // 不能用原子一次性保护读取a、读取b、赋值c这是多条操作 c a b;原子操作保护的范围 单条硬件指令能完成的范围。临界区一旦变复杂只能上锁。6. 多种同步方法进行测试比较但是对于那些有高性能要求的代码对多种同步方法进行测试比较不失为一种明智的做法。原因多核高度竞争场景即使原子操作大量lock指令也会造成缓存行严重乒乓性能暴跌有些场景可以换无锁算法per-cpu 变量每个核单独计数最后汇总比原子操作性能更好少量并发场景有时候轻量锁的实际表现不一定比原子差理论开销只是理论高性能代码一定要实测。【拓】MESI协议前置背景多核 CPU每个核都有自己的 L1/L2 高速缓存缓存最小单位是缓存行cache line一般 64 字节。 多个 CPU 核心缓存里可能同时保存同一块内存的数据副本。如果不做管理A 核改了缓存里的数据B 核还拿着旧副本就会出错。 MESI 就是多核之间维护缓存一致性的协议用来保证所有 CPU 看到的同一份内存数据是一致的。MESI 是四个状态缩写每个缓存行在某个 CPU 的缓存里只能处于下面四种状态之一M Modified修改缓存里的数据已经被修改和主存不一致。这个缓存行是当前 CPU 独有的。必须在合适时机写回主存。E Exclusive独占缓存行有效和主存一致其他 CPU 缓存没有这份数据。当前核可以直接修改不需要通知别人。S Shared共享缓存行有效和主存一致多个 CPU 的缓存里都有这份副本。任何一个核想写数据必须先通知其他核把他们的副本置为无效。I Invalid无效这份缓存行数据作废不能使用。需要读内存重新加载。一句话记住M 修改、E 独占、S 共享、I 无效。状态保存在每个 CPU 缓存行的标记位。状态流转通俗场景举例假设系统有 CPU0、CPU1 两个核变量val放在内存初始值 0。场景 1CPU0 第一次读 valCPU0 去读内存把 val 所在缓存行加载到自己 L1 缓存。 此时 CPU1 缓存没有这份数据 → 状态变成E独占。CPU0 可以直接修改 val不需要通知 CPU1。场景 2CPU1 也读同一个 valCPU1 发起读请求总线嗅探snoop发现CPU0 缓存有这个缓存行E 状态。 CPU0 把缓存行状态改成S共享把数据发给 CPU1。 CPU1 拿到数据自己缓存行状态也设为S。现在 CPU0、CPU1 缓存都是 S 状态两份副本相同和内存一致。两个核都只能读谁想写必须广播消息。场景 3CPU0 要写 valvalCPU0 要修改 S 状态的缓存行会向总线发失效请求Invalidate。 CPU1 收到嗅探消息把自己缓存里这个缓存行标记为I无效。 CPU0 收到确认把自己缓存行改成M修改然后修改缓存里 val 的值。✅ 此时CPU0 缓存 MCPU1 缓存 I主存数据还是旧的。后续 CPU0 在合适时机把 M 的数据写回内存状态变回 E。场景 4现在 CPU1 想读 valCPU1 读嗅探发现 CPU0 缓存行是 M 状态。 CPU0 把缓存行数据发回主存同时发给 CPU1CPU0 状态变为 SCPU1 收到数据状态 S。 两个核又回到共享状态。总线嗅探snoop是什么所有 CPU 都监听系统总线上的缓存操作消息。当某个核发读 / 写、失效消息其他核都能看到检查自己缓存行对应的状态做出响应。MESI 依靠总线广播消息来同步状态。多核越多总线压力越大。回到之前的话题原子操作、锁为什么会触发 MESI 开销1.lock前缀原子操作做了什么x86lock add这种原子指令本质是在这条指令期间独占这个缓存行。CPU 拿到缓存行如果是 S 状态广播失效把其他核副本置 I自己独占缓存行完成读改写释放。 整个过程会触发 MESI 状态跳转、总线消息。开销来源就是缓存行状态来回切换俗称缓存行乒乓cache line bouncing。 多核并发修改同一个变量各个核不断抢缓存行反复 S↔M↔I 来回切换性能急剧下降。2. 自旋锁为什么比单4纯原子操作更容易引发 MESI 风暴自旋锁有两个变量锁变量 业务变量。 多个核抢锁的时候大家反复读写锁变量。锁变量本身在一个独立缓存行。 多核不断 CAS 争抢锁变量导致锁变量对应的缓存行持续在各个核之间来回失效、传递。 哪怕业务数据还没碰锁变量本身就产生大量 MESI 流量。原子操作对 cache-line 影响更小原因就在这里。对比原子计数器atomic_inc(cnt)只争 cnt 这一个缓存行。自旋锁保护 cnt锁变量 业务变量两个缓存行都发生竞争。3. 伪共享false sharingMESI 最经典坑两个独立变量放在同一个 64 字节缓存行里。 CPU0 修改变量 ACPU1 修改变量 B。A、B 无关但共享同一个缓存行。 CPU0 写 A → 广播失效消息CPU1 缓存行变 ICPU1 写 B又广播失效。 明明修改的是两个互不相关变量却疯狂触发 MESI 失效性能暴跌。这叫伪共享。 解决__attribute__((aligned(64)))做缓存行对齐把变量分隔开避免放到同一个缓存行。4. MESI 的局限性总线广播核越多总线嗅探消息越多扩展性差。状态切换有开销S/M/I 之间跳转、等待其他核应答都是延迟。MESI 保证缓存一致性cache coherence不等于内存一致性memory ordering。MESI 保证所有 CPU 看到缓存的数据一致但不保证指令执行顺序所以 CPU 会乱序执行需要内存屏障mfence/smb等来约束指令顺序。这个点面试经常问区分缓存一致性和内存序。atomic_inc 里面的 lock 前缀和自旋锁抢锁时的缓存行锁是一回事吗底层都是 MESI 缓存行独占机制但原子操作只锁这一条指令周期自旋锁会持续反复竞争锁变量的缓存行竞争持续时间更长缓存颠簸更严重。总结MESI 是多核 CPU 缓存一致性协议每个缓存行有 M/E/S/I 四种状态。依靠总线嗅探多核互相感知缓存行状态当一个核修改共享缓存行时会广播失效消息让其他核的副本失效。原子操作的 lock 指令会触发 MESI 状态切换自旋锁除业务变量外锁变量本身也会造成额外缓存行乒乓竞争。伪共享就是无关变量落在同一个缓存行引发不必要的 MESI 失效。MESI 解决缓存数据一致性但不解决 CPU 指令乱序需要内存屏障。7.总结原子操作依托 CPU 硬件提供的原子指令只完成单个变量的读 - 改 - 写操作只会引起目标变量缓存行的短暂竞争。 而锁自旋锁、互斥锁底层本身就要依赖原子操作还额外引入锁变量带来的缓存行竞争自旋锁会忙等互斥锁还会触发进程睡眠和上下文切换所以开销更大。但原子操作能力有限只能保护单个变量的简单操作多条语句、多个变量的临界区仍然需要锁。高性能场景不能只靠理论推断需要实测对比。
返回列表