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

Go1.17

  • 调用约定更改

    程序性能提升about 5%,二进制大小减少2%

    影响unsafe.Pointer

  • 包含闭包的函数可以被内联

    这个变化影响使一个闭包可能产生不同的闭包函数指针

Resource