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

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

大家好,我是煎鱼。

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

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

背景

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

图片

其个人对于 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]》。大家有兴趣的可以认真看下。下面提及主要优化部分。

图片

核心优化点

核心的观点在于:扩展互斥锁的 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

责任编辑:武晓燕 来源: 脑子进煎鱼了
相关推荐

2025-01-06 09:18:04

2025-02-12 08:50:22

2024-12-30 11:05:28

泛型Go类型别名

2024-12-23 08:44:49

2025-02-07 09:18:05

机制Go函数

2022-05-05 11:20:08

KubernetesDocker云计算

2024-09-03 08:49:01

2025-02-08 11:00:33

2022-12-09 08:52:51

Go匿名接口

2013-05-15 09:37:00

GoGo1.1性能测试

2022-05-02 09:21:25

微信微信支付

2021-01-14 10:38:41

Java枚举工具

2021-09-01 07:21:46

堆栈Gopanic

2022-09-30 14:00:50

JavaScrip新特性代码

2021-09-05 18:25:30

Go命令仓库

2021-09-10 17:26:14

Windows 11Windows微软

2024-08-07 09:59:56

2021-02-02 09:10:12

Go语言二进制

2024-01-22 00:30:00

Go编程Go 1.22

2021-04-07 20:01:23

Go变量常量
点赞
收藏

51CTO技术栈公众号