You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

关于Ticket-taking自旋锁内存序强度及缓存行布局的技术问询

Ticket自旋锁:内存序优化与缓存行设计分析

一、初始实现的内存序是否过强?

初始代码中,加锁函数对ticket的自增使用__ATOMIC_ACQ_REL确实过强,原因如下:

  • ticket的原子自增仅用于分配唯一票号,不需要与临界区的内存操作建立同步关系:
    • 原子操作本身已经保证了票号不会重复分配;
    • 后续自旋等待serving时使用的__ATOMIC_ACQUIRE,会与解锁操作的__ATOMIC_RELEASE形成完整的同步链——当线程看到serving等于自己的票号时,Acquire语义会确保之前解锁线程的临界区操作全部可见,同时禁止临界区的读写重排到该加载操作之前。
  • 因此,ticket的自增只需要__ATOMIC_RELAXED内存序即可,Acquire/Release语义在这里是多余的,会带来不必要的内存屏障开销(尤其在弱内存模型的arm64架构上)。

而自旋加载serving使用__ATOMIC_ACQUIRE是完全正确的,这是保证临界区内存可见性的关键。

二、ticket与serving是否需要分隔到不同缓存行?

针对arm64和amd64架构,分场景讨论:

  • 低竞争场景:不需要拆分。两个字段总大小仅16字节(u64)或8字节(u32),会被放在同一个64字节缓存行内。此时伪共享的影响可以忽略,紧凑的结构反而更利于缓存命中。
  • 高竞争场景:建议拆分到不同缓存行,避免伪共享问题:
    • amd64和arm64的缓存一致性协议(MESI/ARMv8的MOESI)下,若两个字段在同一缓存行,ticket的多线程写操作会频繁使缓存行失效,导致自旋读serving的线程需要反复从内存重新加载,大幅降低性能。
    • 拆分方式可以通过填充字节实现,比如:
      typedef struct {
          u32 ticket;
          u8 pad[60]; // 填充至64字节缓存行
          u32 serving;
          u8 pad2[60];
      } ticket_mutex __attribute__((aligned(64)));
      

三、优化后的实现分析

针对低竞争场景的更新实现做了几个关键优化:

  1. 缩小字段类型:将u64改为u32,减少内存占用,低竞争场景下票号不会溢出;
  2. 内存序精简:ticket的自增用__ATOMIC_RELAXED,符合票号分配的语义需求;
  3. x86架构优化:自旋循环中加入pause指令,减少自旋时的CPU功耗,避免内存总线过度占用,同时规避推测执行带来的性能损耗;
  4. 解锁操作调整:先用__ATOMIC_RELAXED加载serving再加1,最后用__ATOMIC_RELEASE存储——由于只有持有锁的线程会执行解锁,不存在竞争,这种写法和原子自增的效果一致,但更直观。

内容的提问来源于stack exchange,提问作者Harris M Snyder

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 06:12:09