Go1.24 新特性:自旋互斥 lock2 优化,性能有一定提高!

大家好,我是煎鱼。

除了上次跟大家提到的 map 使用 Swiss Table 来替换 Hashmap 的原始实现以外。本次 Go1.24 新版本还带来了更多的有效优化。

今天这篇文章将继续和大家一起学习自旋互斥 lock2 优化。

背景

提案作者 @Rhys Hiltner 在 2024 年提出了改进互斥锁的性能优化诉求:

83b2a27fb272b14e14eb65974765006d.png

其个人对于 runtime.mutex 值的部分经验是:整个进程会因为对单个 mutex 的需求使得整个程序缓慢运行

我不认为这一点会让人感到意外,尽管速度减慢的程度超出了我的预期。主要的惊喜在于,程序一旦跌落性能悬崖,就很难再恢复过来。

性能测试

在基准测试 ChanContended 中,作者发现随着 GOMAXPROCS 的增加,mutex 的性能明显下降。

  • Intel i7-13700H (linux/amd64):

    • 当允许使用 4 个线程时,整个进程的吞吐量是单线程时的一半。

    • 当允许使用 8 个线程时,吞吐量再次减半。

    • 当允许使用 12 个线程时,吞吐量再次减半。

    • 在 GOMAXPROCS=20 时,200 次通道操作平均耗时 44 微秒,平均每 220 纳秒调用一次 unlock2,每次都有机会唤醒一个睡眠线程。

  • M1 MacBook Air (darwin/arm64):

    • 当允许使用 5 个线程时,吞吐量不到单线程时的一半。

另一个角度是考虑进程的 CPU 占用时间。

下面的数据显示,在 1.78 秒的挂钟时间内,进程的 20 个线程在 lock2 调用中总共有 27.74 秒处于 CPU 上。

如下测试报告:

$ go test runtime -test.run='^$' -test.bench=ChanContended -test.cpu=20 -test.count=1 -test.cpuprofile=/tmp/p
goos: linux
goarch: amd64
pkg: runtime
cpu: 13th Gen Intel(R) Core(TM) i7-13700H
BenchmarkChanContended-20        26667      44404 ns/op
PASS
ok   runtime 1.785s

$ go tool pprof -peek runtime.lock2 /tmp/p
File: runtime.test
Type: cpu
Time: Jul 24, 2024 at 8:45pm (UTC)
Duration: 1.78s, Total samples = 31.32s (1759.32%)
Showing nodes accounting for 31.32s, 100% of 31.32s total
----------------------------------------------------------+-------------
      flat  flat%   sum%        cum   cum%   calls calls% + context
----------------------------------------------------------+-------------
                                            27.74s   100% |   runtime.lockWithRank
     4.57s 14.59% 14.59%     27.74s 88.57%                | runtime.lock2
                                            19.50s 70.30% |   runtime.procyield
                                             2.74s  9.88% |   runtime.futexsleep
                                             0.84s  3.03% |   runtime.osyield
                                             0.07s  0.25% |   runtime.(*lockTimer).begin
                                             0.02s 0.072% |   runtime.(*lockTimer).end
----------------------------------------------------------+-------------

关键问题之一:这些 lock2 相关的线程并没有休眠,而是一直在自旋!

新提案:增加 spinning 状态

发现问题

通过上述的分析,原作者发现当前的 lock2 实现虽然理论上允许线程睡眠,但实际上导致所有线程都在自旋,自旋的线程至少与(并且可能也导致)更慢的锁传递有关,带来了不少的性能损耗。

@Rhys Hiltner 进而提出了新的设计方案《Proposal: Improve scalability of runtime.lock2[1]》。大家有兴趣的可以认真看下。下面提及主要优化部分。

c46b6ea3cd643174acb21d196317b8f3.png

核心优化点

核心的观点在于:扩展互斥锁的 mutex 状态字,加入一个新的标志位,称为 “spinning”(旋转)

使用这个 “spinning” 位来表示是否有一个等待的线程处于 “醒着并循环尝试获取锁” 的状态。线程之间会互相排除进入 “spinning” 状态,但它们不会因为尝试获取这个标志位而阻塞。

只有持有 “spinning” 位的线程可以循环重新加载 mutex 状态字。这个线程在进入休眠之前会释放 “spinning” 位。其他等待线程则会直接进入休眠,而不会尝试争夺 “spinning” 位。

当某个线程解锁互斥锁时,如果发现已经有线程处于 “醒着并旋转” 的状态,就可以避免唤醒其他线程。在 Go 运行时的背景下,这种设计被称为 “spinbit”(旋转位)。

简单来说,这个设计的核心目的是:通过让一个线程负责 “旋转尝试获取锁”,避免所有线程都同时竞争资源,从而减少争用和不必要的线程切换

兼容性和多平台

本次对于兼容性有保障,导出 API 没有变化。所以我们只需要升级到新版本 Go1.24 就可以白嫖这个优化点了!

目前该优化支持 futex 和 Xchg8 系统调用两个类型。futex 专门用于 GOOS=linux 平台。futex 是主要实现,整体综合表现会好一些。

在已支持的平台上会默认打开 GOEXPERIMENT=spinbitmutex 以此应用该实验性规则。如果大家不需要可以进行关闭。

推荐阅读

参考资料

[1]

Proposal: Improve scalability of runtime.lock2: https://github.com/golang/proposal/blob/master/design/68578-mutex-spinbit.md

关注和加煎鱼微信,

一手消息和知识,拉你进技术交流群👇

38a484f436081352b025e7a57297be6e.jpeg

6187e68809fb41818c28475a3303df23.png

你好,我是煎鱼,出版过 Go 畅销书《Go 语言编程之旅》,再到获得 GOP(Go 领域最有观点专家)荣誉,点击蓝字查看我的出书之路

日常分享高质量文章,输出 Go 面试、工作经验、架构设计,加微信拉读者交流群,和大家交流!

原创不易 点赞支持

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值