1。14版本defer性能大幅度提升,内部实现了开放编码优化
你的留言,是我坚持分享的动力。如果你有任何疑问,可以点击关注,点赞、留言提问。
GO中的defer会在当前函数返回前执行传入的函数,常用于关闭文件描述符,关闭链接及解锁等操作。
Go语言中使用defer时会遇到两个常见问题:defer的调用时机和执行顺序defer调用函数使用传值的方法会执行函数,导致不符预期的结果
接下来我们来详细处理这两个问题。一、defer
官方有段对defer的解释:
每次defer语句执行时,会把函数"压栈",函数的参数会被拷贝下来,当外层函数退出时,defer函数按照定义的逆序执行,如果defer执行的函数为nil,那么会在最终调用函数产生panic。
这里我们先来一道经典的面试题func main() { a,b := 1,2 defer cal("1",a,cal("10",a,b)) a = 0 defer cal("2",a,cal("20",a,b)) } func cal(index string, a, b int) int { ret := a + b fmt.Println(index,a,b,ret) return ret }
你觉得这个会打印什么?
输出结果:10 1 2 3 20 0 2 2 2 0 2 2 1 1 3 4
这里是遵循先入后出的原则,同时保留当前变量的值。
把这道题简化一下:func main() { var i = 1 defer fmt.Println(i) i = 4 }
输出结果1
上述代码输出似乎不符合预期,这个现象出现的原因是什么呢?经过分析,我们发现调用defer关键字会立即拷贝函数中引用的外部参数,所以fmt.Println(i)的这个i是在调用defer的时候就已经赋值了,所以会直接打印1。
想要解决这个问题也很简单,只需要向defer关键字传入匿名函数func main() { var i = 1 defer func() { fmt.Println(i) }() i = 4 }二、数据结构// 占用48字节的内存,link把所有的_defer串成一个链表。 // _defer 只是一个header,结构紧跟的是延迟函数的参数和返回值的空间,大小由 siz 决定 type _defer struct { // 参数和返回值的内存大小 siz int32 started bool // 区分该结构是在栈上分配还是在堆上分配 heap bool openDefer bool // sp 计数器值,栈指针 sp uintptr // sp at time of defer // pc 计数器值,程序计数器 pc uintptr // pc at time of defer // defer 传入的函数地址,也就是延后执行的函数 fn *funcval // can be nil for open-coded defers // 触发延迟调用的结构体 _panic *_panic // 下一个要被执行的延迟函数 link *_defer }
这里把一些垃圾回收使用的字段忽略了。2.1、执行机制
中间代码生成阶段cmd/compile/internal/gc/ssa.go会处理程序中的defer,该函数会根据条件不同,使用三种机制来处理该关键字func (s *state) stmt(n *Node) { ... switch n.Op { case ODEFER: if Debug_defer > 0 { var defertype string if s.hasOpenDefers { defertype = "open-coded" } else if n.Esc == EscNever { defertype = "stack-allocated" } else { defertype = "heap-allocated" } Warnl(n.Pos, "%s defer", defertype) } if s.hasOpenDefers { // 开放编码 s.openDeferRecord(n.Left) } else { // 堆分配 d := callDefer if n.Esc == EscNever { // 栈分配 d = callDeferStack } s.call(n.Left, d) } } }
开放编码、堆分配和栈分配是defer关键字的三种方法,而Go1.14加入的开放编码,使得关键字开销可以忽略不计。2.2、堆分配
call方法会为所有函数和方法调用生成中间代码,工作内容:获取需要执行的函数名、闭包指针、代码指针和函数调用的接收方获取栈地址并将函数或方法写入栈中调用newValue1A及相关函数生成函数调用的中间代码如果当前函数的调用函数是defer,那么会单独生成相关的结束代码块获取函数的返回值并结束当前调用func (s *state) call(n *Node, k callKind) *ssa.Value { var call *ssa.Value if k == callDeferStack { // 在栈上初始化defer结构体 ... } else { ... switch { case k == callDefer: call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, deferproc, s.mem()) ... } call.AuxInt = stksize } s.vars[&memVar] = 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的函数调用func (s *state) exit() *ssa.Block { if s.hasdefer { if s.hasOpenDefers { ... } else { s.rtcall(Deferreturn, true, nil) } } ... }2.2.1、创建延迟调用
runtime.deferproc为defer创建了一个runtime._defer结构体、设置它的函数指针fn、程序计数器pc和栈指针sp并将相关参数拷贝到相邻的内存空间中func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn gp := getg() if gp.m.curg != gp { // go code on the system stack can"t defer throw("defer on system stack") } // 获取caller的sp寄存器和pc寄存器值 sp := getcallersp() argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn) callerpc := getcallerpc() // 分配 _defer 结构 d := newdefer(siz) if d._panic != nil { throw("deferproc: d.panic != nil after newdefer") } // _defer 结构初始化 d.link = gp._defer gp._defer = d d.fn = fn d.pc = callerpc d.sp = sp switch siz { case 0: // Do nothing. case sys.PtrSize: *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp)) default: memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz)) } return0() }
最后调用的return0是唯一一个不会触发延迟调用的函数,可以避免deferreturn的递归调用。
newdefer的分配方式是从pool缓存池中获取:从调度器的deferpool中取出结构体并将该结构体加入到当前goroutine的缓存池中从goroutine的deferpool中取出结构体通过mallocgc从堆上创建一个新的结构体func newdefer(siz int32) *_defer { var d *_defer sc := deferclass(uintptr(siz)) gp := getg() // 获取当前的goroutine // 从pool缓存池中分配,如果没有则mallocgc从堆上分配内存 if sc < uintptr(len(p{}.deferpool)) { pp := gp.m.p.ptr() if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil { for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil { d := sched.deferpool[sc] sched.deferpool[sc] = d.link d.link = nil pp.deferpool[sc] = append(pp.deferpool[sc], d) } } if n := len(pp.deferpool[sc]); n > 0 { d = pp.deferpool[sc][n-1] pp.deferpool[sc][n-1] = nil pp.deferpool[sc] = pp.deferpool[sc][:n-1] } } if d == nil { total := roundupsize(totaldefersize(uintptr(siz))) d = (*_defer)(mallocgc(total, deferType, true)) } d.siz = siz d.heap = true return d }
这三种方式取到的结构体_defer,都会被添加到链表的队头,这也是为什么defer按照后进先出的顺序执行。2.2.2、执行延迟调用func deferreturn(arg0 uintptr) { // 获取当前的goroutine gp := getg() // 拷贝延迟函数到变量d上 d := gp._defer // 如果延迟函数不存在则返回 if d == nil { return } // 获取caller函数的sp寄存器 sp := getcallersp() if d.sp != sp { // 说明这个_defer 不是在该caller函数注册的 return } switch d.siz { case 0: // Do nothing. case sys.PtrSize: *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d)) default: memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz)) } // 获取要调用的函数 fn := d.fn // 重置函数 d.fn = nil // 把下一个_defer结构体依附到goroutine上 gp._defer = d.link // 释放_defer(主要是堆回收,因为栈上的随函数执行完会自动回收) freedefer(d) _ = fn.fn // 调用该函数 jmpdefer(fn, uintptr(unsafe.Pointer(&arg0))) }
deferreturn就是从链表的队头取出并调用jmpdefer传入需要执行的函数和参数。
该函数只有在所有延迟函数都执行后才会返回。2.3、栈分配
如果我们能够将部分结构体分配到栈上就可以节约内存分配带来的额外开销。
在call函数中有在栈上分配func (s *state) call(n *Node, k callKind) *ssa.Value { var call *ssa.Value if k == callDeferStack { t := deferstruct(stksize) ... call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, deferprocStack, s.mem()) if stksize < int64(Widthptr) { stksize = int64(Widthptr) } call.AuxInt = stksize } ... }
在运行期间deferprocStack只需要设置一些未在编译期间初始化的字段,就可以将栈上的_defer追加到函数的链表上。func deferprocStack(d *_defer) { gp := getg() if gp.m.curg != gp { // go code on the system stack can"t defer throw("defer on system stack") } // siz 和 fn在进入到这个函数之前已经赋值 d.started = false // 标明是栈的内存 d.heap = false d.openDefer = false // 获取到caller函数的sp寄存器值和pc寄存器 d.sp = getcallersp() d.pc = getcallerpc() d.framepc = 0 d.varp = 0 *(*uintptr)(unsafe.Pointer(&d._panic)) = 0 *(*uintptr)(unsafe.Pointer(&d.fd)) = 0 *(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer)) *(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d)) return0() }
除了分配的位置和堆的不同,其他的大致相同。2.4、开放编码
Go语言在1.14中通过开放编码实现defer关键字,使用代码内联优化defer关键的额外开销并引入函数数据funcdata管理panic的调用,该优化可以将 defer 的调用开销从 1.13 版本的 ~35ns 降低至 ~6ns 左右。
然而开放编码作为一种优化 defer 关键字的方法,它不是在所有的场景下都会开启的,开放编码只会在满足以下的条件时启用:函数的 defer 数量少于或者等于 8 个;函数的 defer 关键字不能在循环中执行;函数的 return 语句与 defer 语句的乘积小于或者等于 15 个;2.4.1、启动优化const maxOpenDefers = 8 func walkstmt(n *Node) *Node { ... switch n.Op { case ODEFER: Curfn.Func.SetHasDefer(true) Curfn.Func.numDefers++ if Curfn.Func.numDefers > maxOpenDefers { Curfn.Func.SetOpenCodedDeferDisallowed(true) } if n.Esc != EscNever { Curfn.Func.SetOpenCodedDeferDisallowed(true) } fallthrough } }
如果函数中defer关键字的数量多于8个或者defer处于循环中,那么就会禁用开放编码优化。func buildssa(fn *Node, worker int) *ssa.Func { ... s.hasOpenDefers = Debug["N"] == 0 && s.hasdefer && !s.curfn.Func.OpenCodedDeferDisallowed() if s.hasOpenDefers && s.curfn.Func.numReturns*s.curfn.Func.numDefers > 15 { s.hasOpenDefers = false } ... }
可以看到这里,判断编译参数不用-N,返回语句的数量和defer数量的乘积小于15,会启用开放编码优化。2.4.2、延迟记录
延迟比特deferBitsTemp和延迟记录是使用开放编码实现defer的两个最重要的结构,一旦使用开放编码,buildssa会在栈上初始化大小为8个比特的deferBitsfunc buildssa(fn *Node, worker int) *ssa.Func { if s.hasOpenDefers { deferBitsTemp := tempAt(src.NoXPos, s.curfn, types.Types[TUINT8]) s.deferBitsTemp = deferBitsTemp startDeferBits := s.entryNewValue0(ssa.OpConst8, types.Types[TUINT8]) s.vars[&deferBitsVar] = startDeferBits s.deferBitsAddr = s.addr(deferBitsTemp, false) s.store(types.Types[TUINT8], s.deferBitsAddr, startDeferBits) s.vars[&memVar] = s.newValue1Apos(ssa.OpVarLive, types.TypeMem, deferBitsTemp, s.mem(), false) } }
延迟比特中的每一个比特位都表示该位对应的defer关键字是否需要被执行。延迟比特的作用就是标记哪些defer关键字在函数中被执行,这样就能在函数返回时根据对应的deferBits确定要执行的函数。
而deferBits的大小为8比特,所以该优化的条件就是defer的数量小于8.
而执行延迟调用的时候仍在deferreturnfunc deferreturn(arg0 uintptr) { if d.openDefer { done := runOpenDeferFrame(gp, d) if !done { throw("unfinished open-coded defers in deferreturn") } gp._defer = d.link freedefer(d) return } }
这里做了特殊的优化,在runOpenDeferFrame执行开放编码延迟函数
1、从结构体_defer读取deferBits,执行函数等信息
2、在循环中依次读取执行函数的地址和参数信息,并通过deferBits判断是否要执行
3、调用reflectcallSave执行函数func runOpenDeferFrame(gp *g, d *_defer) bool { done := true fd := d.fd deferBitsOffset, fd := readvarintUnsafe(fd) nDefers, fd := readvarintUnsafe(fd) deferBits := *(*uint8)(unsafe.Pointer(d.varp - uintptr(deferBitsOffset))) for i := int(nDefers) - 1; i >= 0; i-- { // 读取函数funcdata地址和参数信息 var argWidth, closureOffset, nArgs uint32 argWidth, fd = readvarintUnsafe(fd) closureOffset, fd = readvarintUnsafe(fd) nArgs, fd = readvarintUnsafe(fd) if deferBits&(1<
洗碗机洗碗和人工工洗碗,哪个更省心?1。水电成本洗碗机洗碗和人工洗碗差不多。2。人工费像大型餐饮,星级酒店等洗碗工都得好几个,而且还不好招工,如果用洗碗机,用人数量减少一半,人工成本会节省很多。3。餐具损耗人工洗碗时
好物推荐南卡A2ANC降噪耳机百元性价比之选本次产品为厂家送测,感谢南卡的大力支持!提到南卡,我们首先想到的应该是南卡家的骨传导蓝牙耳机,不过这次,南卡也是推出了新产品南卡A2,这是南卡首款主动降噪的产品,而能上我的好物推荐
好物推荐漫步者W820NB头戴式耳机没有人比我更懂降噪漫步者这个品牌,近些年可以说是蓝牙耳机领域的一匹黑马了,但是却很少有人知道漫步者已经做了20多年的声学领域了,甚至还是一个上市公司,而他们是做音箱起家的,之后我们有机会也可以去介绍
南卡T2蓝牙耳机评测开箱评测南卡T2蓝牙耳机双动力好音质南卡今年出了很多新品,有不用耳朵听音乐的骨传导CC,有圆滑的半入耳式LitePro,有带主动降噪ANC的A1,可以说每一款耳机都有着自己的特点和
开箱评测SanagA7S气传导耳机新技术新方向本产品为厂家送测,感谢Sanag的大力支持!Sanag虽然是英国品牌,但如今在国内市场也是频频发力,甚至请了花样滑冰世界冠军于小雨做形象大使,可见其品牌对国内市场的重视程度,而一向
自动洗碗机常见功能及优势随着生活条件的提高,洗碗机逐渐被家庭餐饮行业接受和使用,越来越多的家用洗碗机和商用用洗碗机都进入了成千上万的家庭后厨,解决用餐后洗碗的问题。商用洗碗机,通常是商业目的,用于餐厅,食
洗碗机带烘干功能的优势对于一些需要洗很多餐具的洗碗间来说,要操作方便,节省成本,而且还确保餐具卫生安全,还有一个关键的配置不能少,这是干燥系统!聪明的用户可以在购买洗碗机选择干式的功能,或洗碗机烘干系统
如何看待全红婵对游戏防沉迷认证感到为难,称每天只能打1小时?尊重少年人天性,别让鲁迅的后悔重演。风筝北京的冬季,地上还有积雪,灰黑色的秃树枝丫叉于晴朗的天空中,而远处有一二风筝浮动,在我是一种惊异和悲哀。故乡的风筝时节,是春二月,倘听到沙沙
自动洗碗机的功能及优点随着生活条件的改善,洗碗机逐渐被家庭和餐饮业所接受和使用。越来越多的家用洗碗机和商用洗碗机进入千家万户和厨房,解决饭后洗碗的问题。商用洗碗机,通常用于商业用途,用于餐馆食堂宾馆等餐
商用自动洗碗机的功能介绍洗碗机是一种自动清洗碗筷盘子碟子刀叉等餐具的装置。市场上的全自动洗碗机可分为家用和商用两种。家用全自动洗碗机只适合家庭使用,主要有柜式台式水槽一体式和一体式。商用洗碗机按结构可分为
比亚迪汉DM到店实拍,推荐购买四驱性能版豪华型比亚迪今年的重磅车型应该要说汉系列。车型有两种动力总成,分别是插电混动(汉DM)和纯电版(汉EV)。今天我们实拍是刚刚到店的汉DM车型,实拍为四驱性能版豪华型21。98万元,另外还