内联是编译器最重要的优化之一,但它由一个“代价预算”决定,不是由函数行数决定。这篇用编译器自己的诊断看清:哪些写法能内联、代价怎么算、预算的边界在哪里、内联真正改变了什么——以及它没有改变什么。
add 可以内联,代价 4 abs 可以内联,代价 9 sum 可以内联,代价 16 forWithBreak 可以内联,代价 19 withClosure 可以内联,代价 17 withDefer 不能内联:unhandled op DEFER withRecover 不能内联:call to recover withGo 不能内联:unhandled op GO withSelect 可以内联,代价 14 withSwitch 可以内联,代价 16 callsMethod 可以内联,代价 6 callsInterface 可以内联,代价 61 callsFuncValue 可以内联,代价 21 recursive 可以内联,代价 71 panics 可以内联,代价 9 usesStd 可以内联,代价 29 big 可以内联,代价 62 caller 不能内联:function too complex: cost 96 exceeds budget 80 main 不能内联:function too complex: cost 310 exceeds budget 80 (方法 (*T).M:can inline 3)
can inline f with cost N 是说 f 能被内联,N 是它的代价;cannot inline f: function too complex: cost 96 exceeds budget 80 是代价超出;unhandled op DEFER 这类是遇到了编译器目前不支持内联的语句。-m=2 比 -m 多打印原因与代价。
caller 函数体(add、abs、callsMethod、sum、panics、withSwitch 六个调用)里被内联的调用:add, abs, callsMethod, sum, panics, withSwitch, (*T).M main.go:84:6: cannot inline caller: function too complex: cost 96 exceeds budget 80 main.go:89:6: cannot inline main: function too complex: cost 310 exceeds budget 80
每个节点(运算、语句、调用)按固定权重累加;内联进来的函数的代价也要计入调用者。所以 add、abs 这样的小函数一个个都能内联,但把它们放在同一个 caller 里之后,caller 自己就超出预算,不再能被别人内联。实验里连续的 x += i 每条约 3 的代价,最多 26 条。
最大的 k = 26:(True, 80);k = 27:(False, 83) k = 1:(True, 5) k = 10:(True, 32) k = 20:(True, 62)
这个版本里 for(含 break)、闭包、select、switch、方法、函数值调用、自递归都能内联;defer、recover、go 语句不行。别把“有循环就不能内联”之类旧结论当成现状,以你所用版本的诊断为准。
内联的 addInl(i, 1) 中位 2.33 ns/op(最小 2.32 / 最大 2.35) //go:noinline 的 addNoInl(i, 1) 中位 1.99 ns/op(最小 1.98 / 最大 2.22) 内联的 newPInl(i, 1).x(指针不逃逸) 中位 2.07 ns/op(最小 2.06 / 最大 2.37) //go:noinline 的 newPNoInl(i, 1).x(指针逃逸到堆) 中位 6.34 ns/op(最小 6.31 / 最大 6.69) AllocsPerRun:newPInl 0 次;newPNoInl 1 次 同一份代码加 -gcflags=-l(关闭内联): 内联的 addInl(i, 1) 中位 1.98 ns/op(最小 1.97 / 最大 2.04) //go:noinline 的 addNoInl(i, 1) 中位 1.99 ns/op(最小 1.97 / 最大 2.00) 内联的 newPInl(i, 1).x(指针不逃逸) 中位 6.52 ns/op(最小 6.28 / 最大 6.84) //go:noinline 的 newPNoInl(i, 1).x(指针逃逸到堆) 中位 6.57 ns/op(最小 6.39 / 最大 6.78) AllocsPerRun:newPInl 1 次;newPNoInl 1 次
最直观的收益是去掉调用开销,但在这个紧循环微基准里没有看到(内联的 addInl 反而略慢,差异我没有深究)。更实在的收益是内联之后逃逸分析能看到调用者的全貌:newPInl 返回的指针没有逃出调用者,于是 &P{} 留在栈上,0 次分配;关闭内联后同一段代码就多一次分配、慢 3 倍(见上一篇逃逸分析)。
inlining call to strings.Index inlining call to strings.HasPrefix inlining call to bytes.Equal inlining call to utf8.RuneLen inlining call to stringslite.HasPrefix
标准库里的小函数(strings.HasPrefix、bytes.Equal、utf8.RuneLen 等)也会被内联进你的代码,因为编译好的包里带着可内联函数的函数体。math.Abs 没有出现在诊断里,我没有查它是不是被当成内建指令处理了。