现在的位置: 首页 > 编程语言 > 正文

Go1.14语言巨大的性能是怎么实现的

2020年02月19日 编程语言 ⁄ 共 2784字 ⁄ 字号 评论关闭

  先看标准库中的 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 已经非常接近了,性能确实有大幅提升。

抱歉!评论已关闭.