首先,我们通过一些实际的例子来看 defer 的特性。
package main
import (
"fmt"
"log"
)
func main() {
defer log.Println("defer sim lou")
fmt.Println("ending")
}
输出结果:
ending
2019/10/22 22:50:32 defer sim lou
可以看到 defer 定义的函数是在 main 函数逻辑执行完了之后才执行的,也就是延迟调用。
package main
import (
"log"
"strconv"
)
func main() {
for i := 1; i <= 6; i++ {
defer log.Println("defer sim lou-" + strconv.Itoa(i) + ".")
}
log.Println("ending.")
}
输出结果:
2019/10/22 22:56:37 ending.
2019/10/22 22:56:37 defer sim lou-6.
2019/10/22 22:56:37 defer sim lou-5.
2019/10/22 22:56:37 defer sim lou-4.
2019/10/22 22:56:37 defer sim lou-3.
2019/10/22 22:56:37 defer sim lou-2.
2019/10/22 22:56:37 defer sim lou-1.
根据输出可以看到,defer 函数的执行顺序是 LIFO,也就是后进先出的。
package main
import (
"log"
)
func main() {
func() {
defer log.Println("defer sim lou.")
}()
log.Println("main.ending.")
}
输出结果是:
2019/10/22 22:58:59 defer sim lou.
2019/10/22 22:58:59 main.ending.
可以看到 defer 的作用于是函数级别的, 也就是defer 生效在 defer 定义所在的函数栈。
package main
import (
"log"
)
func main() {
defer func() {
if e := recover(); e != nil {
log.Println("defer sim lou.")
}
}()
panic("throw panic")
}
输出结果是:
2019/10/23 23:45:28 defer sim lou.
defer 不是免费的午餐,任何一次的 defer 都存在性能问题,这样一个简单的性能对比:
基于go 1.13:
package main
import (
"sync"
"testing"
)
var lock sync.Mutex
func NoDefer() {
lock.Lock()
lock.Unlock()
}
func Defer() {
lock.Lock()
defer lock.Unlock()
}
func BenchmarkNoDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
NoDefer()
}
}
func BenchmarkDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
Defer()
}
}
benchmark结果是:
goos: darwin
goarch: amd64
BenchmarkNoDefer-12 108125792 11.0 ns/op
BenchmarkDefer-12 35371854 32.2 ns/op
PASS
ok command-line-arguments 3.299s
下面将深入源码探究 defer 存在如此之高性能损耗的原因。
我们看这一段源码会被翻译成汇编代码:
func main() {
defer log.Println("defer sim lou")
fmt.Println("ending")
}
go tool compile -S defer_test1.go >> defer_test1.s
"".main STEXT size=314 args=0x0 locals=0xb0
........
0x0097 00151 (defer_test1.go:9) CALL runtime.deferprocStack(SB)
........
0x00ff 00255 ($GOROOT/src/fmt/print.go:274) CALL fmt.Fprintln(SB)
0x0104 00260 (defer_test1.go:11) XCHGL AX, AX
0x0105 00261 (defer_test1.go:11) CALL runtime.deferreturn(SB)
0x010a 00266 (defer_test1.go:11) MOVQ 168(SP), BP
0x0112 00274 (defer_test1.go:11) ADDQ $176, SP
0x0119 00281 (defer_test1.go:11) RET
通过汇编我们可以知道: defer 的调用被编译为了 runtime.deferprocStack 的调用(这个是go1.13的优化,1.13之前是runtime.deferproc调用)。返回前还被插入了 runtime.deferreturn 调用。
runtime.deferprocStack
runtime.deferreturn
下面我们看看这两个调用具体发生了什么事情。
要先介绍一下 defer 的基础单元 _defer 结构体,如下:
type _defer struct {
siz int32 // includes both arguments and results
started bool
heap bool
sp uintptr // sp at time of defer
pc uintptr
fn *funcval
_panic *_panic // panic that is running defer
link *_defer
}
type funcval struct {
fn uintptr
// variable-size, fn-specific data here
}
siz:所有传入参数的总大小
started:该 defer 是否已经执行过
heap: 表明该defer是否存储在heap上
sp:函数栈指针寄存器,一般指向当前函数栈的栈顶
pc:程序计数器,有时称为指令指针(IP),线程利用它来跟踪下一个要执行的指令。在大多数处理器中,PC指向的是下一条指令,而不是当前指令
fn:指向传入的函数地址和参数
_panic:指向 _panic 链表
link:指向 _defer 链表
所以一个defer对象持有一个defer列表的队头。根据SDK的注释:一些的defer被分配在函数栈上(go1.13的优化),一些被分配在堆上。
func deferprocStack(d *_defer) {
......
d.started = false
d.heap = false
d.sp = getcallersp()
d.pc = getcallerpc()
// The lines below implement:
// d.panic = nil
// d.link = gp._defer
// gp._defer = d
*(*uintptr)(unsafe.Pointer(&d._panic)) = 0
*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))
return0()
// No code can go here - the C return register has
// been set and must not be clobbered.
}
由于defer是在栈上创建的,所以直接以参数传递给 deferprocStack
函数。
我们看看下面代码:
func main() {
for i:=1; i<=2; i++ {
defer log.Println("defer sim lou, ", strconv.Itoa(i))
}
fmt.Println("ending")
}
编译之后的代码:
........
0x00d5 00213 (defer_test1_1.go:11) CALL runtime.deferproc(SB)
.......
0x00e4 00228 (defer_test1_1.go:11) CALL runtime.deferreturn(SB)
........
我们可以看到代码这次defer翻译成的代码是 deferproc
函数。
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
......
sp := getcallersp()
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc()
d := newdefer(siz)
......
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()
// No code can go here - the C return register has
// been set and must not be clobbered.
}
上面会调用 newdefer
函数来在堆上创建新的 defer 对象。
func newdefer(siz int32) *_defer {
var d *_defer
sc := deferclass(uintptr(siz))
gp := getg()
if sc < uintptr(len(p{}.deferpool)) {
pp := gp.m.p.ptr()
if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
// Take the slow path on the system stack so
// we don't grow newdefer's stack.
systemstack(func() {
lock(&sched.deferlock)
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)
}
unlock(&sched.deferlock)
})
}
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 {
// Allocate new defer+args.
systemstack(func() {
total := roundupsize(totaldefersize(uintptr(siz)))
d = (*_defer)(mallocgc(total, deferType, true))
})
if debugCachedWork {
// Duplicate the tail below so if there's a
// crash in checkPut we can tell if d was just
// allocated or came from the pool.
d.siz = siz
d.link = gp._defer
gp._defer = d
return d
}
}
d.siz = siz
d.heap = true
d.link = gp._defer
gp._defer = d
return d
}
申请defer对象时候,会使用 Per-P 里面的defer 对象池。释放defer是通过调用freedefer实现。
通过这个方法我们可以注意到两点,如下:
这个函数主要承担了获取新的 _defer 的作用,它有可能是从 deferpool 中获取的,也有可能是重新申请的。
eferreturn 是由编译器插入到函数末尾的, 它自身只是简单的将需要被 defer 的入口地址取出,然后跳转并执行:
// 如果存在,则运行 defer 函数
// 编译器会将这个调用插入到任何包含 defer 的函数的末尾。
// 如果存在一个被 defer 的函数,此调用会调用 runtime.jmpdefer
// 这将跳转到被延迟的函数,使得它看起来像是在调用 deferreturn 之前由 deferreturn 的调用者调用。
// 产生的结果就是反复地调用 deferreturn,直到没有更多的 defer 函数为止。
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
if d == nil {
return
}
sp := getcallersp()
if d.sp != sp {
return
}
// Moving arguments around.
//
// Everything called after this point must be recursively
// nosplit because the garbage collector won't know the form
// of the arguments until the jmpdefer can flip the PC over to
// fn.
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
gp._defer = d.link
freedefer(d)
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
该方法中主要做了如下事项:
在这段代码中,跳转方法 jmpdefer 格外重要。因为它显式的控制了流转,代码如下:
// func jmpdefer(fv *funcval, argp uintptr)
// argp is a caller SP.
// called from deferreturn.
// 1. pop the caller
// 2. sub 5 bytes from the callers return
// 3. jmp to the argument
TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
MOVQ fv+0(FP), DX // fn
MOVQ argp+8(FP), BX // caller sp
LEAQ -8(BX), SP // caller sp after CALL
MOVQ -8(SP), BP // restore BP as if deferreturn returned (harmless if framepointers not in use)
SUBQ $5, (SP) // return to CALL again
MOVQ 0(DX), BX
JMP BX // but first run the deferred function
这个 jmpdefer 巧妙的地方在于,它通过调用方 sp 来推算了 deferreturn 的入口地址, 从而在完成某个 defer 调用后,会再次回到 deferreturn 的初始位置,继续反复调用,造成尾递归的假象。尾递归可以避免创建新的函数栈从而提升性能。
通过源码的分析,我们发现它做了两个很 “奇怪” 又很重要的事,如下:
你可能会问,为什么是 5?好吧。翻了半天最后看了一下汇编代码…嗯,相减的确是 5 没毛病,如下:
0x0105 00261 (defer_test1.go:11) CALL runtime.deferreturn(SB)
0x010a 00266 (defer_test1.go:11) MOVQ 168(SP), BP
照上述逻辑的话,那 deferreturn 就是一个 “递归” 了哦。每次都会重新回到 deferreturn 函数,那它在什么时候才会结束呢,如下:
unc deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
if d == nil {
return
}
...
}
也就是会不断地进入 deferreturn 函数,判断链表中是否还存着 _defer。若已经不存在了,则返回,结束掉它。简单来讲,就是处理完全部 defer 才允许你真的离开它。
我们先总结以下 defer 的实现思路:
从这两个过程来看,一次 defer 的成本来源于 _defer 对象的分配与回收、被 defer 函数的参数的拷贝。
go 1.13的release note中显示 defer 性能提升30%。根据前面的分析,编译器会根据应用场景去选择使用 deferproc 还是 deferprocStack 方法,他们分别是针对分配在堆上和栈上的使用场景。
官方说明的 Go1.13 defer 性能提高 30%,主要来源于其延迟对象的堆栈分配规则的改变,措施是由编译器通过对 defer 的 for-loop 迭代深度进行分析,如果 loopdepth 为 1,则设置逃逸分析的结果,将分配到栈上,否则分配到堆上。
参考:
尾调用优化: http://www.ruanyifeng.com/blog/2015/04/tail-call.html
尾递归:https://www.cnblogs.com/zhanggui/p/7722541.html