Runtime overhead of using defer in go
Golang has a pretty nifty keyword named defer
. As explained here, a defer statement pushes a function call onto a list. The list of saved calls is executed after the surrounding function returns. Defer is commonly used to simplify functions that perform various clean-up actions.
Using defer, however, is not free. Using go’s benchmarking support, we can try to quantify this overheard.
The following two functions do the same work, but one calls a function in a defer
statement while the other doesn’t
package mainfunc doNoDefer(t *int) {
func() {
*t++
}()
}func doDefer(t *int) {
defer func() {
*t++
}()
}
Let’s benchmark these —
package mainimport (
"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)
}
}
Running this with go -bench
on an 8 core google cloud VM gives us
⇒ 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
As expected, both these functions don’t allocate any memory. But doDefer
is roughly 16 times more expensive than doNoDefer
. To understand why defer
is this expensive, let’s look at the disassembled code.
The disassembly for the actual functions called inside doDefer
and doNoDefer
is the same
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
The doNoDefer
sets up the necessary registers and then calls 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)
The doDefer
function also sets up registers, but there are additional function calls — the first one to runtime.deferproc
which sets up the deferred function to be called. The second one is to runtime.deferreturn
— which in turn calls itself for every defer
statement encountered in the function.
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)
deferproc
and deferreturn
are both non-trivial functions and they do a bunch of accounting and setup at entry and exit. In short, don’t use defer
in hot code paths. The overhead is non-trivial and not obvious.