defer
defer
defer
在go的日常开发中是一个出现频率非常高的关键字,它会以先进后出的方式来执行defer
关联的函数,很多时候我们利用这种机制来进行一些资源的释放操作,比如文件关闭之类的操作。
fd, err := os.Open("/dev/stdin")
if err != nil{
return err
}
defer fd.Close()
...
如此高频出现的关键字,使得我们有必要去了解一下它背后的结构。
结构
defer
关键字对应runtime._defer
结构体,它的结构并不复杂
type _defer struct {
started bool
heap bool
openDefer bool
sp uintptr // sp at time of defer
pc uintptr // pc at time of defer
fn func() // can be nil for open-coded defers
_panic *_panic // panic that is running defer
link *_defer // next defer on G; can point to either heap or stack!
fd unsafe.Pointer // funcdata for the function associated with the frame
varp uintptr // value of varp for the stack frame
framepc uintptr
}
其中的fn
字段是defer
关键字对应的函数,link
表示下一个链接的defer
,sp
和pc
记录了调用方的函数信息,用于判断defer
属于哪一个函数。defer在运行时以链表的形式存在,链表的头部就在协程G上,所以defer
实际上是与协程直接关联的。
type g struct {
...
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
...
}
当协程执行函数时,就会按照顺序将函数中的defer
从链表的头部加入
defer fn1()
defer fn2()
defer fn3()
上面那段代码就对应这幅图
除了协程之外,P也跟defer
有一定的关联,在P的结构体中,有一个deferpool
字段,如所示。
type p struct {
...
deferpool []*_defer // pool of available defer structs (see panic.go)
deferpoolbuf [32]*_defer
...
}
deferpool
中存放着预分配好的defer
结构,用于给与P关联的协程G分配新的defer
结构,可以减少开销。
分配
在语法上对defer
关键字的使用,编译器会将其转为为对runtime.deferproc
函数的调用。比如go代码是这样写的
defer fn1(x, y)
而编译后实际上的代码是这样的
deferproc(func(){
fn1(x, y)
})
所以实际上defer
传入的函数是没有参数也没有返回值的,deferproc
函数代码如下所示
func deferproc(fn func()) {
gp := getg()
d := newdefer()
d.link = gp._defer
gp._defer = d
d.fn = fn
d.pc = getcallerpc()
d.sp = getcallersp()
return0()
}
该函数负责创建defer
结构并将其加入协程G链表的头部,其中的runtime.newdefer
函数就会尝试从P中的deferpool
来获取预分配的defer
结构。
if len(pp.deferpool) == 0 && sched.deferpool != nil {
lock(&sched.deferlock)
for len(pp.deferpool) < cap(pp.deferpool)/2 && sched.deferpool != nil {
d := sched.deferpool
sched.deferpool = d.link
d.link = nil
pp.deferpool = append(pp.deferpool, d)
}
unlock(&sched.deferlock)
}
它首先会从全局的sched.deferpool
向局部的deferpool
装填一半的defer
结构,然后再从P中的deferpool
尝试去获取
if n := len(pp.deferpool); n > 0 {
d = pp.deferpool[n-1]
pp.deferpool[n-1] = nil
pp.deferpool = pp.deferpool[:n-1]
}
if d == nil {
// Allocate new defer.
d = new(_defer)
}
d.heap = true
最后实在找不到才会使用手动分配的方式。最后可以看到有这么一段代码
d.heap = true
这表示defer
在堆上分配,相应的当其为false
时,就会在栈上分配,栈上分配的内存会在返回时自动回收,其内存管理效率要比在堆上更高,而决定是否在栈上分配的因素就是循环层数,这部分逻辑可以追溯到cmd/compile/ssagen
中的escape.goDeferStmt
方法的这一小段,如下所示
func (e *escape) goDeferStmt(n *ir.GoDeferStmt) {
...
if n.Op() == ir.ODEFER && e.loopDepth == 1 {
k = e.later(e.discardHole())
n.SetEsc(ir.EscNever)
}
...
}
e.loopDepth
表示的就是当前语句的循环层数,如果当前defer
语句不在循环中,就会将其分配到栈上。
case ir.ODEFER:
n := n.(*ir.GoDeferStmt)
if s.hasOpenDefers {
s.openDeferRecord(n.Call.(*ir.CallExpr))
} else {
d := callDefer
if n.Esc() == ir.EscNever {
d = callDeferStack
}
s.callResult(n.Call.(*ir.CallExpr), d)
}
如果是在栈上分配的话,就会直接在栈上创建defer
结构体,最终会由runtime.deferprocStack
函数来完成defer
结构的创建。
if k == callDeferStack {
// Make a defer struct d on the stack.
if stksize != 0 {
s.Fatalf("deferprocStack with non-zero stack size %d: %v", stksize, n)
}
t := deferstruct()
...
// Call runtime.deferprocStack with pointer to _defer record.
ACArgs = append(ACArgs, types.Types[types.TUINTPTR])
aux := ssa.StaticAuxCall(ir.Syms.DeferprocStack, s.f.ABIDefault.ABIAnalyzeTypes(nil, ACArgs, ACResults))
callArgs = append(callArgs, addr, s.mem())
call = s.newValue0A(ssa.OpStaticLECall, aux.LateExpansionResultType(), aux)
call.AddArgs(callArgs...)
call.AuxInt = int64(types.PtrSize) // deferprocStack takes a *_defer arg
deferprocStack
函数的签名如下
func deferprocStack(d *_defer)
其具体的创建逻辑与deferproc
并无太大区别,主要的区别在于,在栈上分配时是defer
结构的来源是直接创建的结构体,在堆上分配的defer
来源是new
函数。
执行
当函数将要返回或者发生panic
时,便会进入runtime.deferreturn
函数,它负责从协程的链表中取出defer
并执行。
func deferreturn() {
gp := getg()
for {
d := gp._defer
sp := getcallersp()
if d.sp != sp {
return
}
fn := d.fn
d.fn = nil
gp._defer = d.link
freedefer(d)
fn()
}
}
首先会通过getcallersp()
获取当前函数的栈帧并与defer
结构中的sp
做比较来判断defer
是否属于当前函数,然后将defer
结构从链表头部取出,并使用gp._defer = d.link
执行下一个defer
,再通过runtuime.freedefer
函数将defer
结构释放回池中,最后再调用fn
执行,就这样一直循环到执行完属于当前函数的所有defer
结束为止。
开放编码
defer
的使用并非毫无成本,虽然它在语法上给我们提供了便利,但毕竟它不是直接进行函数调用,中间会进行经过一系列的过程,所以还是会造成性能损耗,所以后来go官方设计了一种优化方——开放编码,它是一种对defer
的优化方式,其原英文名叫open-coded,国内基本上都给翻译成了开放编码,这里的open指的是展开的意思,就是将defer
函数的代码展开到当前函数代码中,就像函数内联一样。这种优化方式有以下几个限制条件
- 函数中的
defer
数量不能超过8个 defer
与return
两者数量的乘积不能超过15defer
不能出现在循环中- 未禁用编译优化
- 没有手动调用
os.Exit()
- 不需要从堆上复制参数
这部分判断逻辑可以追溯到cmd/compile/ssagen.buildssa
函数的下面这部分代码
s.hasOpenDefers = base.Flag.N == 0 && s.hasdefer && !s.curfn.OpenCodedDeferDisallowed()
if s.hasOpenDefers && len(s.curfn.Exit) > 0 {
s.hasOpenDefers = false
}
if s.hasOpenDefers {
for _, f := range s.curfn.Type().Results().FieldSlice() {
if !f.Nname.(*ir.Name).OnStack() {
s.hasOpenDefers = false
break
}
}
}
if s.hasOpenDefers && s.curfn.NumReturns*s.curfn.NumDefers > 15 {
s.hasOpenDefers = false
}
然后go会在当前函数创建一个8位整数变量deferBits
来当作bitmap用于标记defer
,每一位标记一个,8位整数uint8
最多表示8个,如果对应位为1,那么对应的开放编码优化后的defer
就会在函数要返回时执行。