Go1.14 巨大的性能是怎么实现的?主要关注 time.Timer 和 内联 defer

下面这部分是关于 Go1.14为time.Timer定时器带来巨幅性能提升,原文:https://www.pengrl.com/p/20021/

先看标准库中的 time package benchmark 的指标变化:

Changes in the time package benchmarks:

name                      old time/op  new time/op  delta
AfterFunc-12              1.57ms ± 1%  0.07ms ± 1%  -95.42%  (p=0.000 n=10+8)
After-12                  1.63ms ± 3%  0.11ms ± 1%  -93.54%  (p=0.000 n=9+10)
Stop-12                   78.3µs ± 3%  73.6µs ± 3%   -6.01%  (p=0.000 n=9+10)
SimultaneousAfterFunc-12   138µs ± 1%   111µs ± 1%  -19.57%  (p=0.000 n=10+9)
StartStop-12              28.7µs ± 1%  31.5µs ± 5%   +9.64%  (p=0.000 n=10+7)
Reset-12                  6.78µs ± 1%  4.24µs ± 7%  -37.45%  (p=0.000 n=9+10)
Sleep-12                   183µs ± 1%   125µs ± 1%  -31.67%  (p=0.000 n=10+9)
Ticker-12                 5.40ms ± 2%  0.03ms ± 1%  -99.43%  (p=0.000 n=10+10)
Sub-12                     114ns ± 1%   113ns ± 3%     ~     (p=0.069 n=9+10)
Now-12                    37.2ns ± 1%  36.8ns ± 3%     ~     (p=0.287 n=8+8)
NowUnixNano-12            38.1ns ± 2%  37.4ns ± 3%   -1.87%  (p=0.020 n=10+9)
Format-12                  252ns ± 2%   195ns ± 3%  -22.61%  (p=0.000 n=9+10)
FormatNow-12               234ns ± 1%   177ns ± 2%  -24.34%  (p=0.000 n=10+10)
MarshalJSON-12             320ns ± 2%   250ns ± 0%  -21.94%  (p=0.000 n=8+8)
MarshalText-12             320ns ± 2%   245ns ± 2%  -23.30%  (p=0.000 n=9+10)
Parse-12                   206ns ± 2%   208ns ± 4%     ~     (p=0.084 n=10+10)
ParseDuration-12          89.1ns ± 1%  86.6ns ± 3%   -2.78%  (p=0.000 n=10+10)
Hour-12                   4.43ns ± 2%  4.46ns ± 1%     ~     (p=0.324 n=10+8)
Second-12                 4.47ns ± 1%  4.40ns ± 3%     ~     (p=0.145 n=9+10)
Year-12                   14.6ns ± 1%  14.7ns ± 2%     ~     (p=0.112 n=9+9)
Day-12                    20.1ns ± 3%  20.2ns ± 1%     ~     (p=0.404 n=10+9)

数据来源:https://github.com/golang/go/commit/6becb033341602f2df9d7c55cc23e64b925bbee2

从基准测试结果中可以看到,After函数从老版本的1.63ms直接下降到了0.11ms,提升相当恐怖。

我个人对于此次优化的粗浅理解是:

在1.14之前,标准库会在在创建Timer时懒创建goroutinue,全局最多创建64个,这些goroutinue创建好后永远不会退出,每个goroutinue中有一个最小堆,管理着一部分定时器,这些goroutinue会给Go的线程调度模型带来非常大的开销。

而1.14则直接在P上管理Timer,这样调度器在每次调度循环的间隙都可以检查是否有超时的timer需要执行,并且可以通过netpoll进行唤醒,有点类似于epoll的事件驱动模型中的定时器管理。

具体的分析可以看欧长坤大佬对于Go 1.14定时器优化的分析:

  • Go夜读:第 74 期 time.Timer 源码分析 (Go 1.14) #541 https://github.com/developer-learning/night-reading-go/issues/541

  • 相关的文章:17.2 time 的计时器 Timer | Go Under The Hood https://changkun.de/golang/zh-cn/part4lib/ch17other/time/

  • Go夜读在b站的视频:#74 Go time.Timer 源码分析(Go1.14)https://www.bilibili.com/video/av81849820

接着讲解 Go1.14 针对 defer 做的优化。

在 Go1.14 之前,Go 中的每一个 defer 函数,会在编译期在 defer 位置生成一个 runtime.deferproc 调用,并且在包含 defer 的函数退出时生成一个 runtime.deferreturn 调用。

如下代码:

0: func run() {
1:    defer foo()
2:    defer bar()
3:
4:    fmt.Println("hello")
5: }

编译器会生成类似如下的代码:

runtime.deferproc(foo) // generated for line 1
runtime.deferproc(bar) // generated for line 2

fmt.Println("hello")

runtime.deferreturn() // generated for line 5

这使得使用 defer 时增加了 Go runtime 函数调用开销。

另外值得一提的是,Go runtime 中使用了先进后出的栈管理着一个函数中的多个 defer 调用,这也意味着 defer 越多,开销越大。

拿我们常见的互斥锁场景举例:

var mu sync.Mutex
mu.Lock()

defer mu.Unlock()

defer 和非 defer 版本的 benchmark 如下:

BenchmarkMutexNotDeferred-8  125341258  9.55 ns/op          0 B/op          0 allocs/op
BenchmarkMutexDeferred-8     45980846   26.6 ns/op         0 B/op          0 allocs/op

尽管耗时都是纳秒级别,但是 defer 版本是非 defer 版本的 2.7 倍,换句话说,在一些简单的锁场景,defer 的开销甚至超过了锁自身的开销。如果在性能热点路径上,这部分开销还是挺可观的。

这使得部分 Go 程序员在高性能编程场景下,舍弃了 defer 的使用。但是不使用 defer,容易导致代码可读性下降,资源忘记释放的问题。

于是在 Go1.14,编译器会在某些场景下尝试在函数返回处直接调用被 defer 的函数,从而使得使用 defer 的开销就像一个常规函数调用一样。

还拿上面那个例子举例,编译器将生成如下的代码:

fmt.Println("hello")

bar() // generated for line 5
foo() // generated for line 5

但是 defer 并不是所有场景都能内联。比如如果是在一个循环次数可变的循环中使用 defer 就没法内联。但是在函数起始处,或者不包含在循环内部的条件分支中的 defer 都是可以内联的。老实说,我们大部分时候也就是在这些简单场景使用 defer。

在 Go1.14beta 再次执行上面 mutex 的基准测试:

BenchmarkMutexNotDeferred-8  123710856  9.64 ns/op          0 B/op          0 allocs/op
BenchmarkMutexDeferred-8     104815354  11.5 ns/op         0 B/op          0 allocs/op

可以看到 defer 版本从 26.6 ns/op 下降到了 11.5 ,与非 defer 版本的 9.64 已经非常接近了,性能确实有大幅提升。

本文内容来自一篇英文文章,但是省去了原文中关于 defer 的基础功能介绍,只对 defer 内联部分的内容进行意译,没有逐字翻译,感兴趣的可以观看原文:https://rakyll.org/inlined-defers/

Go 内联 defe 的设计、提案:https://github.com/golang/proposal/blob/master/design/34481-opencoded-defers.md