使用 defer 的运行时开销

saberuster · 2018-05-27 21:57:21 · 1514 次点击 · 预计阅读时间 3 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2018-05-27 21:57:21 的文章,其中的信息可能已经有所发展或是发生改变。

在 Go 语言中有一个特殊的关键字 defer。对于它更多的介绍请看这里defer 语句会把一个函数追加到函数调用列表。这个列表会在函数返回的时候依次调用。defer 常用来进行各种清理操作。

但是 defer 本身是有开销的。使用 Go 的基准测试工具我们可以量化这种开销。

下面两个函数做同样的工作。一个使用 defer 语句而另一个不使用:

package main
func doNoDefer(t *int) {
    func() {
        *t++
    }()
}
func doDefer(t *int) {
    defer func() {
        *t++
    }()
}

基准测试代码:

package main
import (
    "testing"
)
func BenchmarkDeferYes(b *testing.B) {
    t := 0
    for i := 0; i < b.N; i++ {
        doDefer(&t)
    }
}
func BenchmarkDeferNo(b *testing.B) {
    t := 0
    for i := 0; i < b.N; i++ {
        doNoDefer(&t)
    }
}

在一个 8 核的谷歌云主机上运行基准测试:

⇒ go test -v -bench BenchmarkDefer -benchmem
goos: linux
goarch: amd64
pkg: cmd
BenchmarkDeferYes-8  20000000   62.4 ns/op  0 B/op  0 allocs/op
BenchmarkDeferNo-8   500000000  3.70 ns/op  0 B/op  0 allocs/op

和预想的一样,这些函数都没有额外分配任何内存。但是 doDefer 的开销要比 doNoDefer 高 16 倍之多。我们需要借助反汇编代码来了解为什么 defer 的开销如此之大。

反汇编代码的函数调用部分 doDeferdoNoDefer 是相同的。

main.go:10   MOVQ 0x8(SP), AX
main.go:11   MOVQ 0(AX), CX
main.go:11   INCQ CX
main.go:11   MOVQ CX, 0(AX)
main.go:12   RET

doNoDefer 先初始化必要的注册工作然后调用 main.doNoDefer.func1

TEXT main.doNoDefer(SB) main.go
main.go:3  MOVQ FS:0xfffffff8, CX
main.go:3  CMPQ 0x10(CX), SP
main.go:3  JBE 0x450b65
main.go:3  SUBQ $0x10, SP
main.go:3  MOVQ BP, 0x8(SP)
main.go:3  LEAQ 0x8(SP), BP
main.go:3  MOVQ 0x18(SP), AX
main.go:6  MOVQ AX, 0(SP)
main.go:6  CALL main.doNoDefer.func1(SB)
main.go:7  MOVQ 0x8(SP), BP
main.go:7  ADDQ $0x10, SP
main.go:7  RET
main.go:3  CALL runtime.morestack_noctxt(SB)
main.go:3  JMP main.doNoDefer(SB)

doDefer 也会先进行必要的注册工作,但是它会额外调用几个函数:第一个是 runtime.deferproc,它用来设置需要调用的延迟函数。第二个是 runtime.deferreturn,它会自动调用每个 defer 语句。

TEXT main.doDefer(SB) main.go
main.go:9    MOVQ FS:0xfffffff8, CX
main.go:9    CMPQ 0x10(CX), SP
main.go:9    JBE 0x450bd3
main.go:9    SUBQ $0x20, SP
main.go:9    MOVQ BP, 0x18(SP)
main.go:9    LEAQ 0x18(SP), BP
main.go:9    MOVQ 0x28(SP), AX
main.go:12   MOVQ AX, 0x10(SP)
main.go:10   MOVL $0x8, 0(SP)
main.go:10   LEAQ 0x218e3(IP), AX
main.go:10   MOVQ AX, 0x8(SP)
main.go:10   CALL runtime.deferproc(SB)
main.go:10   TESTL AX, AX
main.go:10   JNE 0x450bc3
main.go:13   NOPL
main.go:13   CALL runtime.deferreturn(SB)
main.go:13   MOVQ 0x18(SP), BP
main.go:13   ADDQ $0x20, SP
main.go:13   RET
main.go:10   NOPL
main.go:10   CALL runtime.deferreturn(SB)
main.go:10   MOVQ 0x18(SP), BP
main.go:10   ADDQ $0x20, SP
main.go:10   RET
main.go:9    CALL runtime.morestack_noctxt(SB)
main.go:9    JMP main.doDefer(SB)

deferprocdeferreturn 都是比较复杂的函数,它们会在进入和退出函数时进行一系列的配置和计算。所以,不要在热代码中使用 defer 关键字,因为它的开销很大的而且很难被侦测到。


via: https://medium.com/i0exception/runtime-overhead-of-using-defer-in-go-7140d5c40e32

作者:Aniruddha  译者:saberuster  校对:polaris1119

本文由 GCTT 原创编译,Go语言中文网 荣誉推出


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1514 次点击  
加入收藏 微博
被以下专栏收入,发现更多相似内容
2 回复  |  直到 2018-06-09 12:09:50
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传