1.14版本defer性能大幅度提升,内部实现了开放编码优化

Python011

1.14版本defer性能大幅度提升,内部实现了开放编码优化,第1张

GO中的defer会在当前函数返回前执行传入的函数,常用于关闭文件描述符,关闭链接及解锁等操作。

Go语言中使用defer时会遇到两个常见问题:

接下来我们来详细处理这两个问题。

官方有段对defer的解释:

这里我们先来一道经典的面试题

你觉得这个会打印什么?

输出结果:

这里是遵循先入后出的原则,同时保留当前变量的值。

把这道题简化一下:

输出结果

上述代码输出似乎不符合预期,这个现象出现的原因是什么呢?经过分析,我们发现调用defer关键字会立即拷贝函数中引用的外部参数,所以fmt.Println(i)的这个i是在调用defer的时候就已经赋值了,所以会直接打印1。

想要解决这个问题也很简单,只需要向defer关键字传入匿名函数

这里把一些垃圾回收使用的字段忽略了。

中间代码生成阶段cmd/compile/internal/gc/ssa.go会处理程序中的defer,该函数会根据条件不同,使用三种机制来处理该关键字

开放编码、堆分配和栈分配是defer关键字的三种方法,而Go1.14加入的开放编码,使得关键字开销可以忽略不计。

call方法会为所有函数和方法调用生成中间代码,工作内容:

defer关键字在运行时会调用deferproc,这个函数实现在src/runtime/panic.go里,接受两个参数:参数的大小和闭包所在的地址。

编译器不仅将defer关键字转成deferproc函数,还会通过以下三种方式为所有调用defer的函数末尾插入deferreturn的函数调用

1、在cmd/compile/internal/gc/walk.go的walkstmt函数中,在遇到ODEFFER节点时会执行Curfn.Func.SetHasDefer(true),设置当前函数的hasdefer属性

2、在ssa.go的buildssa会执行s.hasdefer = fn.Func.HasDefer()更新hasdefer

3、在exit中会根据hasdefer在函数返回前插入deferreturn的函数调用

runtime.deferproc为defer创建了一个runtime._defer结构体、设置它的函数指针fn、程序计数器pc和栈指针sp并将相关参数拷贝到相邻的内存空间中

最后调用的return0是唯一一个不会触发延迟调用的函数,可以避免deferreturn的递归调用。

newdefer的分配方式是从pool缓存池中获取:

这三种方式取到的结构体_defer,都会被添加到链表的队头,这也是为什么defer按照后进先出的顺序执行。

deferreturn就是从链表的队头取出并调用jmpdefer传入需要执行的函数和参数。

该函数只有在所有延迟函数都执行后才会返回。

如果我们能够将部分结构体分配到栈上就可以节约内存分配带来的额外开销。

在call函数中有在栈上分配

在运行期间deferprocStack只需要设置一些未在编译期间初始化的字段,就可以将栈上的_defer追加到函数的链表上。

除了分配的位置和堆的不同,其他的大致相同。

Go语言在1.14中通过开放编码实现defer关键字,使用代码内联优化defer关键的额外开销并引入函数数据funcdata管理panic的调用,该优化可以将 defer 的调用开销从 1.13 版本的 ~35ns 降低至 ~6ns 左右。

然而开放编码作为一种优化 defer 关键字的方法,它不是在所有的场景下都会开启的,开放编码只会在满足以下的条件时启用:

如果函数中defer关键字的数量多于8个或者defer处于循环中,那么就会禁用开放编码优化。

可以看到这里,判断编译参数不用-N,返回语句的数量和defer数量的乘积小于15,会启用开放编码优化。

延迟比特deferBitsTemp和延迟记录是使用开放编码实现defer的两个最重要的结构,一旦使用开放编码,buildssa会在栈上初始化大小为8个比特的deferBits

延迟比特中的每一个比特位都表示该位对应的defer关键字是否需要被执行。延迟比特的作用就是标记哪些defer关键字在函数中被执行,这样就能在函数返回时根据对应的deferBits确定要执行的函数。

而deferBits的大小为8比特,所以该优化的条件就是defer的数量小于8.

而执行延迟调用的时候仍在deferreturn

这里做了特殊的优化,在runOpenDeferFrame执行开放编码延迟函数

1、从结构体_defer读取deferBits,执行函数等信息

2、在循环中依次读取执行函数的地址和参数信息,并通过deferBits判断是否要执行

3、调用reflectcallSave执行函数

1、新加入的defer放入队头,执行defer时是从队头取函数调用,所以是后进先出

2、通过判断defer关键字、return数量来判断是否开启开放编码优化

3、调用deferproc函数创建新的延迟调用函数时,会立即拷贝函数的参数,函数的参数不会等到真正执行时计算

Go语言由Google公司开发,并于2009年开源,相比Java/Python/C等语言,Go尤其擅长并发编程,性能堪比C语言,开发效率肩比Python,被誉为“21世纪的C语言”。

Go语言在云计算、大数据、微服务、高并发领域应用应用非常广泛。BAT大厂正在把Go作为新项目开发的首选语言。

Go语言能干什么?

1、服务端开发:以前你使用C或者C++做的那些事情,用Go来做很合适,例如日志处理、文件系统、监控系统等

2、DevOps:运维生态中的Docker、K8s、prometheus、grafana、open-falcon等都是使用Go语言开发

3、网络编程:大量优秀的Web框架如Echo、Gin、Iris、beego等,而且Go内置的 net/http包十分的优秀

4、Paas云平台领域:Kubernetes和Docker Swarm等

5、分布式存储领域:etcd、Groupcache、TiDB、Cockroachdb、Influxdb等

6、区块链领域:区块链里面有两个明星项目以太坊和fabric都使用Go语言

7、容器虚拟化:大名鼎鼎的Docker就是使用Go语言实现的

8、爬虫及大数据:Go语言天生支持并发,所以十分适合编写分布式爬虫及大数据处理。

1、简单易学。

Go语言的作者本身就很懂C语言,所以同样Go语言也会有C语言的基因,所以对于程序员来说,Go语言天生就会让人很熟悉,容易上手。

2、并发性好。

Go语言天生支持并发,可以充分利用多核,轻松地使用并发。 这是Go语言最大的特点。

描述

Go的语法接近C语言,但对于变量的声明有所不同。Go支持垃圾回收功能。Go的并行模型是以东尼·霍尔的通信顺序进程(CSP)为基础,采取类似模型的其他语言包括Occam和Limbo,但它也具有Pi运算的特征,比如通道传输。

在1.8版本中开放插件(Plugin)的支持,这意味着现在能从Go中动态加载部分函数。

与C++相比,Go并不包括如枚举、异常处理、继承、泛型、断言、虚函数等功能,但增加了 切片(Slice) 型、并发、管道、垃圾回收、接口(Interface)等特性的语言级支持。