3. 多核同步与低功耗的冲突:同步等待导致的功耗浪费、锁竞争与功耗峰值、缓存一致性协议(MESI)的功耗影响
多核系统里,同步和低功耗就像一对冤家。你想想看,核多了,干活快了,但协调它们却要付出代价。这个代价,很多时候就是功耗。
我刚开始做多核项目时,天真地以为只要把任务分给几个核,功耗自然就降下来了。结果一测,好家伙,功耗比单核还高。问题出在哪?就出在同步上。
3.1 同步等待导致的功耗浪费
最简单的同步方式是什么?自旋锁。一个核在等另一个核释放资源,它就在那空转。空转意味着什么?CPU 还在跑,指令还在取,流水线还在动,但没干任何有用的活。
这种功耗浪费,我称之为“空转功耗”。
关键数据:一个 Cortex-A53 核在自旋等待时,功耗大约是满负荷运行的 60%-70%。如果系统里有 4 个核都在等,那浪费的功耗相当可观。
为什么会这样?因为自旋锁的实现通常是一个循环,不断读取共享内存中的标志位。这个循环本身就在消耗动态功耗。更糟糕的是,它还会导致缓存行 bouncing,进一步增加功耗。
我曾经在一个项目中,发现一个核在等另一个核处理完数据,等了整整 2 毫秒。2 毫秒在嵌入式系统里可不短。那个等待的核,功耗一直维持在 80mW 左右。如果换成睡眠等待,功耗能降到 1mW 以下。差距有多大?80 倍。
我的建议:能用睡眠等待,就别用自旋等待。实在要用自旋,也要加个超时机制,别让核在那死等。
3.2 锁竞争与功耗峰值
锁竞争带来的问题更隐蔽。它不光是性能问题,还是功耗问题。
当多个核同时竞争一把锁时,会发生什么?
- 每个核都在尝试获取锁
- 获取失败的核会重试
- 重试过程中,缓存一致性协议在疯狂工作
- 总线带宽被占满
结果就是:功耗峰值瞬间飙升。我见过一个极端案例,8 个核同时竞争一个全局锁,系统瞬时功耗达到了平均功耗的 3 倍。这个峰值如果持续时间长,电源管理单元可能会误判,导致电压调节器输出不稳定。
| 锁竞争场景 | 平均功耗 | 峰值功耗 | 持续时间 |
|---|---|---|---|
| 无竞争 | 100mW | 120mW | — |
| 2 核竞争 | 150mW | 220mW | ~10μs |
| 4 核竞争 | 210mW | 350mW | ~20μs |
| 8 核竞争 | 320mW | 580mW | ~50μs |
你看这个表,核数越多,竞争越激烈,功耗峰值越高。而且这个峰值不是一次性的,是反复出现的。每次锁竞争,都是一次功耗冲击。
避坑指南:我曾经在一个项目中,锁竞争导致的功耗峰值触发了芯片的过流保护,系统直接复位了。排查了三天才发现是锁竞争的问题。从那以后,我设计系统时一定会做锁竞争分析,尤其是对功耗敏感的场合。
3.3 缓存一致性协议(MESI)的功耗影响
MESI 协议,说白了就是让多个核的缓存保持一致的一套规则。每个缓存行有四种状态:Modified、Exclusive、Shared、Invalid。
状态切换是要花代价的。每次状态变化,都意味着总线事务。总线事务意味着什么?功耗。
我画了一张图,帮你理解 MESI 状态切换与功耗的关系:
从这张图你能看到,MESI 状态切换不是免费的。每次状态变化,都伴随着总线事务。总线事务越多,功耗越高。
这里我要特别提一下伪共享。伪共享是什么?两个核访问不同的变量,但这两个变量恰好在同一个缓存行里。结果就是,明明没有真正的数据竞争,但缓存行却在两个核之间来回 bouncing。
我遇到过最夸张的一个案例:一个数据结构里有两个计数器,分别被两个核频繁更新。两个计数器在同一个缓存行里。结果呢?缓存行每纳秒都在 bouncing,总线带宽被占满,功耗比预期高了 40%。
我的建议:对于频繁更新的共享数据,用缓存行对齐。把不同核访问的数据放到不同的缓存行里。C11 标准里有 alignas 关键字,可以用起来。
3.4 如何平衡同步与低功耗
说了这么多问题,那怎么解决?我总结了几条经验:
- 减少同步粒度:能用局部锁,就别用全局锁。锁的粒度越小,竞争越少,功耗越低。
- 使用无锁数据结构:在合适的场景下,用原子操作代替锁。原子操作不会导致缓存行 bouncing,功耗更低。
- 合理选择同步机制:短等待用自旋锁,长等待用信号量。自旋锁的等待时间不要超过 10μs。
- 缓存行对齐:避免伪共享,把不同核访问的数据放到不同缓存行。
- 使用硬件同步原语:有些芯片有硬件同步单元,比软件锁效率高得多。
经验之谈:我做过一个项目,把全局锁拆成 4 个局部锁,功耗直接降了 25%。有时候,简单的设计改动就能带来巨大的收益。
嗯,同步和低功耗的冲突,说白了就是“协调成本”的问题。协调得越好,成本越低。但完全消除协调成本是不可能的,我们只能尽量优化。