version change in golang
Go1.14
本次更新大多数变化在于工具链的实现,runtime和libraries
defer性能提升
在Go1.14之前,Go中的每一个defer函数,会在编译期在defer位置生成一个runtime.deferproc调用,并且在包含defer函数退出时生成一个runtime.deferreturn调用
go build main.go && objdump -S main
这使得使用defer时增加了go runtime函数掉哦那个开销。另外值得一提的是,Go runtime中使用了先进后出的栈管理着一个函数中多个defer调用,这也意味着defer越多,开销越大
这使得部分Go程序员在高性能编程场景下,舍弃了defer的使用。但是不使用defer,容易导致代码可读性下降,资源忘记释放问题。
性能测试
优化原理:在Go1.14,编译器会在某些场景下尝试在函数返回处直接调用被defer的函数,从而使得使用defer的开销就像一个常规函数调用一样
goroutine支持异步抢占
Go语言调度其的性能随着版本越来越优异,我们来了解一下调度器使用的G-M-P模型,
- G: goroutine,由关键字go创建
- M: 在Go中称为工作线程,由内核调度
- P: 处理器P是线程M和Goroutine之间的中间层,主要是为了处理内核进行系统调用
Go语言调度器的工作原理就是处理器P从本地队列依次选择G放到M上调度执行,任务偷窃
在Go1.1版本中,调度器还不支持抢占式调度,只能依靠goroutine主动让出CPU资源,存在非常严重的调度问题
- 单独的goroutine可以一直占用线程运行,不会切换到其它的goroutine,造成饥饿问题
- 垃圾回收需要暂停整个程序,如果没有抢占可能需要等待几分钟的时间,导致整个程序无法工作 在Go1.12中编译器在特定时机插入函数,通过函数调用作为入口触发抢占,实现了协作式的抢占调度,但是这种需要函数调用主动配合的调度方式存在一些边缘情况,比如:协作式的调度不会使一个没有主动放弃执行权,且不参与任何函数调用的goroutine被抢占
Go1.14实现了基于信号的真抢占式调度,runtime.sighandler注册信号SIGURG函数runtime.doSigPreempt,在出发垃圾回收的栈扫描时,调用函数挂起goroutine并向M发送信号,M收到信号后,会让当前goroutine陷入休眠继续执行其它的G
preempt时机
一方面,Go进程启动时,会开启一个后台线程sysmon,监控执行时间过长的goroutine,另一方面,STW时会让所有的goroutine停止,两者都会调用preemptone()
preemptone() -> preemptM -> signalM -> sighanlder
#doSigPreempt() if ok, newpc := isAsyncSafePoint(gp, ctxt.sigpc(), ctxc.sigsp(), ctxt.siglr()); ok { //Adjust the PC inject a call to asyncPreempt ctxt.pushCall(funcPC(asyncPreempt), newpc) }
isAsyncSafePoint()返回当前goroutine能否被抢占,以及从哪一条指定开始抢占,返回的newpc表示安全的抢占地址
当执行完sighandler,执行流再次回到线程,由于sighandler插入了一个asyncPreempt函数调用,原来的函数流程就暂停了
asyncPreempt -> asyncPreempt2 -> mcall(gopreempt_m) -> goschedImp1 -> schedule
mcall(fn)的作用是切换到g0栈去执行函数fn,fn永不返回;gopreempt_m直接调用goschedImp1
func goschedImp1(gp *g) { casgstatus(gp, _Grunning, _Grunnable) dropg() //解绑m, g lock(&sched.lock) golabrunqput(gp) //将goroutine丢到全局队列 unlock(&sched.lock) shcedule() } func dropg() { _g_ := getg() setMNOWB(&_g_.m.curg.m, nil) setGNOWB(&_g_.m.curg, nil) }
timer定时器性能得到巨幅提升
Go1.14做到了直接在每个P上维护自己的timer堆,像维护本地队列runq一样
Go1.15
小整数缓存
xxx/go1.17.5/go/src/runtime/iface.go:522(convT64)
原理: staticuint64s预分配优化避免了小整数转换为interface{}的内存分配
linker
先说下golang编译过程.(/usr/local/go/src/cmd/compile/internal/gc/main.go:132 Main())
词法分析
调用parseFile()解析,获取抽象语法树
类型检查
静态类型检查,动态类型检查
- 常量、类型和函数名及类型(重写OMAKE节点)
- 变量的复制和初始化
- 函数和闭包的主体
- 决定如何捕获变量
- 检查内联函数的类型
- 进行逃逸分析
- 将闭包主体转换为引用的捕获变量
- 编译顶层函数
- 检查外部依赖声明
中间代码生成
SSA配置初始化
- 常数传播
- 值域传播
- 稀疏有条件的常数传播
- 消除无用的程式码
- 全域数值编号
- 消除部分冗余
- 寄存器分配
遍历和替换,堆AST中节点的一些元素进行替换(walk系列函数对关键词进行遍历和改写,转换成函数调用,compileSSA函数将AST转换为中间代码)
最终机器码生成
编译器将一些值重写成目标CPU架构的特定值,将SSA中间代码降级,汇编器将这些指令转换为机器码
go1.15完全重写了linker
新链接器变化
- 移动许多工作从编译期到链接器,这个是为了使其paralleization,充分利用多核
- 优化关键数据结构,主要是string,当前linker使用一个很大的符号表索引来替代symbo-number编码
- 避免马上加载input object files,这将使少量的内存就能编译large program
Go1.16
metrics
字段意义: https://pkg.go.dev/runtime/metrics#example-Read-ReadingAllMetrics
释放内存给OS
释放内存给OS,MADV_DONTNEED(man madvise)
native embedding of static files
Go1.17
调用约定更改
程序性能提升about 5%,二进制大小减少2%
影响unsafe.Pointer
包含闭包的函数可以被内联
这个变化影响使一个闭包可能产生不同的闭包函数指针
Resource
- Go1.16 Release Notes: https://go.dev/doc/go1.16