“defer 很慢”是一个过时的说法。Go 1.14 引入了 open-coded defer,把大多数 defer 编译成函数出口处的直接调用。这篇测出它到底快多少,什么时候会退化,并用编译器自己的诊断与源码里的限制条件对照。
noDefer:直接调用 inc() 中位 1.97 ns/op(最小 1.96 / 最大 2.01) oneDefer:defer inc() 中位 1.98 ns/op(最小 1.96 / 最大 2.10) lockDirect:Lock + 直接 Unlock 中位 7.64 ns/op(最小 7.59 / 最大 7.75) lockDefer:Lock + defer Unlock 中位 11.79 ns/op(最小 11.74 / 最大 11.84) eightDefers:8 个 defer(最多可以内联展开的数量) 中位 15.27 ns/op(最小 15.10 / 最大 16.82) nineDefers:9 个 defer 中位 58.47 ns/op(最小 58.19 / 最大 60.25) loopDefer:循环里的 defer(循环 1 次) 中位 17.95 ns/op(最小 17.85 / 最大 18.13) manyReturns:4 个 defer × 4 个 return 中位 29.63 ns/op(最小 29.52 / 最大 31.80) withRecover:正常返回(defer 里有 recover) 中位 3.32 ns/op(最小 3.30 / 最大 3.33) withPanic:panic 后被 recover 接住 中位 144.53 ns/op(最小 142.54 / 最大 147.44) AllocsPerRun:oneDefer 0、eightDefers 0、nineDefers 0、loopDefer 0、manyReturns 0、withPanic 0
open-coded defer 把 defer f() 在函数的每个出口直接展开成对 f() 的调用(再加一个栈上的位图记录哪些 defer 要执行),不需要运行时在堆或栈上登记。所以 oneDefer 与直接调用没有可测的差别。加锁加 defer 解锁比直接解锁多约 4 ns,这是被 defer 的 Unlock 无法内联造成的,不是 defer 本身的成本(这一点是我的解释,没有进一步验证)。
oneDefer open-coded defer × 1
lockDefer open-coded defer × 1
eightDefers open-coded defer × 8
nineDefers stack-allocated defer × 9
loopDefer heap-allocated defer × 1
manyReturns stack-allocated defer × 4
withRecover open-coded defer × 1
withPanic open-coded defer × 1
各函数汇编里的 defer 相关调用(去重):
noDefer (没有调用 runtime 的 defer 函数)
oneDefer runtime.deferreturn
lockDefer runtime.deferreturn
eightDefers runtime.deferreturn
nineDefers runtime.deferprocStack, runtime.deferreturn
loopDefer runtime.deferproc, runtime.deferreturn
manyReturns runtime.deferprocStack, runtime.deferreturn
withPanic runtime.deferreturn-d=defer=1 把每个 defer 分成 open-coded、stack-allocated、heap-allocated 三类。第 9 个 defer 起或出口太多就变成 stack-allocated(调用 deferprocStack,在栈上建记录),在循环里的 defer 才是 heap-allocated(调用 deferproc,需要运行时管理记录)。AllocsPerRun 在三种情形下都是 0——说明运行时有记录的复用,我没有去读它的实现。
默认:
noDefer 中位 1.98 ns/op(最小 1.98 / 最大 2.07)
oneDefer 中位 1.99 ns/op(最小 1.95 / 最大 2.04)
lockDefer 中位 12.30 ns/op(最小 12.18 / 最大 16.23)
eightDefers 中位 15.63 ns/op(最小 15.55 / 最大 16.09)
loopDefer 中位 18.11 ns/op(最小 18.01 / 最大 19.04)
-gcflags=-d=noopendefer=1(关闭 open-coded defer):
noDefer 中位 1.98 ns/op(最小 1.96 / 最大 1.99)
oneDefer 中位 13.23 ns/op(最小 13.15 / 最大 13.30)
lockDefer 中位 19.90 ns/op(最小 19.78 / 最大 20.40)
eightDefers 中位 54.05 ns/op(最小 53.57 / 最大 57.76)
loopDefer 中位 18.17 ns/op(最小 17.98 / 最大 19.13)用 -d=noopendefer=1 关掉这项优化,同一份代码里每个 defer 多花约 11 ns,而循环里的 defer 不变,证明它本来就走另一条路径。9 个 defer 与 8 个 defer 之间那个 3.8 倍的跳变(15.27 → 58.47 ns)就是跨过 open-coded 限制的代价。
walk/stmt.go:109-119
if ir.CurFunc.NumDefers > maxOpenDefers || n.DeferAt != nil {
// Don't allow open-coded defers if there are more than
// 8 defers in the function, since we use a single
// byte to record active defers.
// Also don't allow if we need to use deferprocat.
ir.CurFunc.SetOpenCodedDeferDisallowed(true)
}
if n.Esc() != ir.EscNever {
// If n.Esc is not EscNever, then this defer occurs in a loop,
// so open-coded defers cannot be used in this function.
ir.CurFunc.SetOpenCodedDeferDisallowed(true)
ssagen/ssa.go:424-432
if s.hasOpenDefers &&
s.curfn.NumReturns*s.curfn.NumDefers > 15 {
// Since we are generating defer calls at every exit for
// open-coded defers, skip doing open-coded defers if there are
// too many returns (especially if there are multiple defers).
// Open-coded defers are most important for improving performance
// for smaller functions (which don't have many returns).
s.hasOpenDefers = false
}编译器里这几条限制写得很清楚:defer 数用一个字节的位图记录,所以最多 8 个;循环里的 defer 次数运行时才知道,不能展开;出口数乘 defer 数超过 15 时展开的代码太多就放弃。实用的结论是:函数里少放 defer、不要在热循环里 defer;但没必要为了“省 defer”而去掉一个 defer mu.Unlock()。