Go官方团队将在今年2月份发布1.14版本。相比较于之前的版本升级,Go1.14在性能提升上做了较大改动,还加入了很多新特性,我们一起来看一下Go1.14都给我们带来了哪些惊喜吧!
性能提升1. defer性能提升
package main
import (
"testing"
)
type channel chan int
func NoDefer() {
ch1 := make(channel, 10)
close(ch1)
}
func Defer() {
ch2 := make(channel, 10)
defer close(ch2)
}
func BenchmarkNoDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
NoDefer()
}
}
func BenchmarkDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
Defer()
}
}
我们分别使用Go1.13版本和Go1.14版本进行测试,关于Go多个版本的管理切换,推荐大家使用 gvm ,非常的方便。首先使用Go1.13版本,只需要命令: gvm use go1.13 ;之后运行命令: go test -bench=. -v ,结果如下:
goos: darwin
goarch: amd64
pkg: github.com/GuoZhaoran/myWebSites/data/goProject/defer
BenchmarkNoDefer-4 15759076 74.5 ns/op
BenchmarkDefer-4 11046517 102 ns/op
PASS
ok github.com/GuoZhaoran/myWebSites/data/goProject/defer 3.526s
可以看到,Go1.13版本调用defer关闭channel的性能开销还是蛮大的,op几乎差了30ns。切换到go1.14: gvm use go1.14 ;再次运行命令: go test -bench=. -v ,下面的结果一定会亮瞎了小伙伴的双眼:
goos: darwin
goarch: amd64
pkg: github.com/GuoZhaoran/myWebSites/data/goProject/defer
BenchmarkNoDefer
BenchmarkNoDefer-4 13094874 80.3 ns/op
BenchmarkDefer
BenchmarkDefer-4 13227424 80.4 ns/op
PASS
ok github.com/GuoZhaoran/myWebSites/data/goProject/defer 2.328s
Go1.14版本使用defer关闭channel几乎0开销!关于这一改进,官方给出的回应是:
Go1.14提高了defer的大多数用法的性能,几乎0开销!defer已经可以用于对性能要求很高的场景了。
goroutine支持异步抢占
Go语言调度器的性能随着版本迭代表现的越来越优异,我们来了解一下调度器使用的G-M-P模型。先是一些概念:
- G(Goroutine): goroutine,由关键字go创建
- M(Machine): 在Go中称为Machine,可以理解为工作线程
- P(Processor) : 处理器 P 是线程 M 和 Goroutine 之间的中间层(并不是CPU)
M必须持有P才能执行G中的代码,P有自己本地的一个运行队列runq,由可运行的G组成,Go语言调度器的工作原理就是处理器P的队列中选择队列头的goroutine 放到线程 M 上执行,下图展示了 线程 M、处理器 P 和 goroutine 的关系。
每个P维护的G可能是不均衡的,调度器还维护了一个全局G队列,当P执行完本地的G任务后,会尝试从全局队列中获取G任务运行( 需要加锁 ),当P本地队列和全局队列都没有可运行的任务时,会尝试偷取其他P中的G到本地队列运行( 任务窃取 )。
在Go1.1版本中,调度器还不支持抢占式调度,只能依靠 goroutine 主动让出 CPU 资源,存在非常严重的调度问题:
单独的 goroutine 可以一直占用线程运行,不会切换到其他的 goroutine,造成饥饿问题
垃圾回收需要暂停整个程序(Stop-the-world,STW),如果没有抢占可能需要等待几分钟的时间,导致整个程序无法工作
Go1.12中编译器在特定时机插入函数,通过函数调用作为入口触发抢占,实现了协作式的抢占式调度。但是这种需要函数调用主动配合的调度方式存在一些边缘情况,就比如说下面的例子:
package main
import (
"runtime"
"time"
)
func main() {
runtime.GOMAXPROCS(1)
go func() {
for {
}
}()
time.Sleep(time.Millisecond)
println("OK")
}
其中创建一个goroutine并挂起, main goroutine 优先调用了 休眠,此时唯一的 P 会转去执行 for 循环所创建的 goroutine,进而 main goroutine 永远不会再被调度。换一句话说在Go1.14之前,上边的代码永远不会输出OK。这是因为Go1.12实现的协作式的抢占式调度是不会使一个没有主动放弃执行权、且不参与任何函数调用的goroutine被抢占。
Go1.14 通过实现了基于信号的真抢占式调度解决了上述问题,这是一个非常大的改动,Go团队对已有的逻辑进行重构并为 goroutine 增加新的状态和字段来支持抢占。这一改动使得Go语言调度器更加健壮,调度性能更加优越,但是还有一些潜在的问题没有被发现,预计将来会在 STW 和栈扫描之外加入更多的抢占点。