先看标准库中的 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)
从基准测试结果中可以看到,After函数从老版本的1.63ms直接下降到了0.11ms,提升相当恐怖。
我个人对于此次优化的粗浅理解是:
在1.14之前,标准库会在在创建Timer时懒创建goroutinue,全局最多创建64个,这些goroutinue创建好后永远不会退出,每个goroutinue中有一个最小堆,管理着一部分定时器,这些goroutinue会给Go的线程调度模型带来非常大的开销。
而1.14则直接在P上管理Timer,这样调度器在每次调度循环的间隙都可以检查是否有超时的timer需要执行,并且可以通过netpoll进行唤醒,有点类似于epoll的事件驱动模型中的定时器管理。
接着讲解 Go1.14 针对 defer 做的优化。
在 Go1.14 之前,Go 中的每一个 defer 函数,会在编译期在 defer 位置生成一个 runtime.deferproc 调用,并且在包含 defer 的函数退出时生成一个 runtime.deferreturn 调用。
如下代码:
func run() {
defer foo()
defer bar()
fmt.Println("hello")
}
编译器会生成类似如下的代码:
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 已经非常接近了,性能确实有大幅提升。