「这是我参与2022首次更文挑战的第21天,活动详情查看:2022首次更文挑战」。
前言
本文主要介绍defer如何在栈中存储。
栈分配
栈分配的过程包括deferprocStack,deferreturn
deferprocStack源码如下:
func deferprocStack(d *_defer) {
gp := getg()
if gp.m.curg != gp {
throw("defer on system stack")
}
if goexperiment.RegabiDefer && d.siz != 0 {
throw("defer with non-empty frame")
}
d.started = false
d.heap = false
d.openDefer = false
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()
}
底下那部分冗长且有指针的操作其实只是对defer结构体的赋值,并把defer挂在当前协程上,这么写的好处是没有写屏障,至于什么是写屏障留到后面垃圾回收算法细说。
相较于堆分配需要在这步在堆内存中创建一个defer结构体并为其复制,栈内存更加迅捷一点。
内联优化
相较于栈分配,是否可以进一步优化呢?
最便捷的方式就是像函数一样直接调用
举个例子
func f(){
deferproc a()
deferproc b()
c()
deferreturn()
}
可以近似变成
func f(){
c()
b()
a()
}
我们考虑deferproc a()来记录所需参数,但是依旧有一个问题就是deferreturn的时候不确定这个defer是否需要执行。因为可能会出现defer b()被包裹在if里。在编译时无法确定是否执行这个defer的情况下将defer放入链表中显然是不健康的。
在go语言中,Go语言编辑器采取了一种巧妙的方式,以位图的方式来进行判断。
func f(){
defer a()
if isTrue {
defer b()
}
c()
}
变为
func f(){
deferBits |= 1<<0
tmpF1 = a
if isTrue{
deferBits |= 1<<1
tmpF2 = b
}
c()
//deferreturn 区
if deferBIts & 1<<1 != 0 {
tmpF2()
}
if deferBIts & 1<<0 != 0 {
tmpF1()
}
}
通过位图的方式将if判断条件与是否执行defer相解耦,以最小的代价满足了大部分情况下的需求。