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 main
func doNoDefer(t *int) {
func() {
*t++
}()
}
func doDefer(t *int) {
defer func() {
*t++
}()
}

Let’s benchmark these —

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)
}
}

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.