[译] Go语言inline内联的策略与限制

1、什么是内联

内联,就是将一个函数调用原地展开,替换成这个函数的实现。尽管这样做会增加编译后二进制文件的大小,但是却可以提高程序的性能。

在 Go 中,函数调用有固定的开销;栈和抢占检查

硬件分支预测器改善了其中的一些功能,但就功能大小和时钟周期而言,这仍然是一个成本。

内联是避免这些成本的经典优化方法。

内联只对叶子函数有效,叶子函数是不调用其他函数的。这样做的理由是:

  • 如果你的函数做了很多工作,那么前序开销可以忽略不计。
  • 另一方面,小函数为相对较少的有用工作付出固定的开销。这些是内联目标的功能,因为它们最受益。

2、规则

编写两个go文件,分别是main.go和op.go,作用就是对一组数字进行加和减

//main.go
import "fmt"

func main() {
    n := []float32{120.4, -46.7, 32.50, 34.65, -67.45}
    fmt.Printf("The total is %.02f\n",sum(n))
}

func sum(s []float32) float32 {
   var t float32
   for _,v := range s {
      if t < 0 {
         t = add(t,v)
      } else {
         t = sub(t,v)
      }
   }
   return t
}

//op.go
func add(a,b float32) float32 {
   return a + b
}

func sub(a,b float32) float32{
   return a - b
}

使用以下命令来build

go build -gcflags=-m main.go op.go

运行结果如下:

# command-line-arguments
.\op.go:3:6: can inline add
.\op.go:7:6: can inline sub
.\main.go:16:11: inlining call to sub
.\main.go:14:11: inlining call to add
.\main.go:7:13: inlining call to fmt.Printf
.\main.go:10:10: s does not escape
.\main.go:6:17: []float32 literal does not escape
.\main.go:7:40: sum(n) escapes to heap
.\main.go:7:13: []interface {} literal does not escape
<autogenerated>:1: .this does not escape
<autogenerated>:1: .this does not escape

可以看到方法add、sub被内联了,但是sum方法没有被内联,但是sum方法逃逸到heap 上了

可以通过以下命令来查看,sum方法为何没有被内联

go build -gcflags="-m -m" main.go op.go

运行结果如下:

# command-line-arguments
.\op.go:3:6: can inline add with cost 4 as: func(float32, float32) float32 { return a + b }
.\op.go:7:6: can inline sub with cost 4 as: func(float32, float32) float32 { return a - b }
.\main.go:10:6: cannot inline sum: unhandled op RANGE
.\main.go:16:11: inlining call to sub func(float32, float32) float32 { return a - b }
.\main.go:14:11: inlining call to add func(float32, float32) float32 { return a + b }
.\main.go:5:6: cannot inline main: function too complex: cost 148 exceeds budget 80
.\main.go:7:13: inlining call to fmt.Printf func(string, ...interface {}) (int, error) { var fmt..autotmp_4 int; fmt..autotmp_4 = <N>; var fmt..autotmp_5 error; fm
t..autotmp_5 = <N>; fmt..autotmp_4, fmt..autotmp_5 = fmt.Fprintf(io.Writer(os.Stdout), fmt.format, fmt.a...); return fmt..autotmp_4, fmt..autotmp_5 }
.\main.go:10:10: s does not escape
.\main.go:7:40: sum(n) escapes to heap:
.\main.go:7:40:   flow: ~arg1 = &{storage for sum(n)}:
.\main.go:7:40:     from sum(n) (spill) at .\main.go:7:40
.\main.go:7:40:     from fmt.format, ~arg1 = <N> (assign-pair) at .\main.go:7:13
.\main.go:7:40:   flow: {storage for []interface {} literal} = ~arg1:
.\main.go:7:40:     from []interface {} literal (slice-literal-element) at .\main.go:7:13
.\main.go:7:40:   flow: fmt.a = &{storage for []interface {} literal}:
.\main.go:7:40:     from []interface {} literal (spill) at .\main.go:7:13
.\main.go:7:40:     from fmt.a = []interface {} literal (assign) at .\main.go:7:13
.\main.go:7:40:   flow: {heap} = *fmt.a:
.\main.go:7:40:     from fmt.Fprintf(io.Writer(os.Stdout), fmt.format, fmt.a...) (call parameter) at .\main.go:7:13
.\main.go:6:17: []float32 literal does not escape
.\main.go:7:40: sum(n) escapes to heap
.\main.go:7:13: []interface {} literal does not escape
<autogenerated>:1: .this does not escape
<autogenerated>:1: .this does not escape

关键在于这句输出:

.\main.go:10:6: cannot inline sum: unhandled op RANGE

Go不会内联包含循环的方法。实际上,包含以下内容的方法都不会被内联:

闭包调用、select、for、defer、go关键字创建的协程

并且除了这些,还有其他的限制。

当解析AST时,Go申请了80个节点作为内联的预算,每个节点都会消耗一个预算。

比如:a = a +1 这行代码包含了5个节点:

AS,NAME,ADD,NAME,LITERAL,以下是对应的SSA dump:

当一个函数的开销超过了预算,就无法内联,例如:

.\main.go:5:6: cannot inline main: function too complex: cost 148 exceeds budget 80

但是严重的内联会使得堆栈信息更加难以追踪。

3、挑战

当发生panic时,开发人员需要知道panic的准确堆栈信息,获取源码文件以及行号。那么问题来了,

被内联的函数是否还有正确的堆栈信息吗?

下面是一个包含panic的内联方法:

func add(a,b float32) float32 {
   if b < 0 {
      panic(`Do not add negative number`)
   }
   return a + b
}

运行这个程序,可以看到panic显示了正确的源码行号,尽管它被内联了:

 

这是因为,Go在内部维持了一份内联函数的映射关系,首先它会生成一个内联树,

可以通过以下命令查看

go build -gcflags="-d pctab=pctoinline" main.go op.go

输出的部分结果如下:

-- inlining tree for "".sum:
0 | -1 | "".add (E:\goproject\awesomeProject\interview\inlining\main.go:14:11) pc=83
1 | -1 | "".sub (E:\goproject\awesomeProject\interview\inlining\main.go:16:11) pc=89
--
funcpctab "".add [valfunc=pctoinline]
     0     -1 00000 (E:\goproject\awesomeProject\interview\inlining\op.go:3)    TEXT    "".add(SB), ABIInternal, $24-16
     0        00000 (E:\goproject\awesomeProject\interview\inlining\op.go:3)    TEXT    "".add(SB), ABIInternal, $24-16
     0     -1 00000 (E:\goproject\awesomeProject\interview\inlining\op.go:3)    MOVQ    TLS, CX
     9        00009 (E:\goproject\awesomeProject\interview\inlining\op.go:3)    PCDATA  $0, $-2
     9        00009 (E:\goproject\awesomeProject\interview\inlining\op.go:3)    MOVQ    (CX)(TLS*2), CX
    10        00016 (E:\goproject\awesomeProject\interview\inlining\op.go:3)    PCDATA  $0, $-1
    10        00016 (E:\goproject\awesomeProject\interview\inlining\op.go:3)    CMPQ    SP, 16(CX)
    14        00020 (E:\goproject\awesomeProject\interview\inlining\op.go:3)    PCDATA  $0, $-2
    14        00020 (E:\goproject\awesomeProject\interview\inlining\op.go:3)    JLS     105
    16        00022 (E:\goproject\awesomeProject\interview\inlining\op.go:3)    PCDATA  $0, $-1
    16        00022 (E:\goproject\awesomeProject\interview\inlining\op.go:3)    SUBQ    $24, SP
    1a        00026 (E:\goproject\awesomeProject\interview\inlining\op.go:3)    MOVQ    BP, 16(SP)
    1f        00031 (E:\goproject\awesomeProject\interview\inlining\op.go:3)    LEAQ    16(SP), BP
    24        00036 (E:\goproject\awesomeProject\interview\inlining\op.go:3)    FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    24        00036 (E:\goproject\awesomeProject\interview\inlining\op.go:3)    FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    24        00036 (E:\goproject\awesomeProject\interview\inlining\op.go:4)    XORPS   X0, X0
    27        00039 (E:\goproject\awesomeProject\interview\inlining\op.go:4)    MOVSS   "".b+36(SP), X1
    2d        00045 (E:\goproject\awesomeProject\interview\inlining\op.go:4)    UCOMISS X1, X0
    30        00048 (E:\goproject\awesomeProject\interview\inlining\op.go:4)    JHI     76
    32        00050 (E:\goproject\awesomeProject\interview\inlining\op.go:7)    MOVSS   "".a+32(SP), X0
    38        00056 (E:\goproject\awesomeProject\interview\inlining\op.go:7)    ADDSS   X1, X0
    3c        00060 (E:\goproject\awesomeProject\interview\inlining\op.go:7)    MOVSS   X0, "".~r2+40(SP)
    42        00066 (E:\goproject\awesomeProject\interview\inlining\op.go:7)    MOVQ    16(SP), BP
    47        00071 (E:\goproject\awesomeProject\interview\inlining\op.go:7)    ADDQ    $24, SP
    4b        00075 (E:\goproject\awesomeProject\interview\inlining\op.go:7)    RET
    4c        00076 (E:\goproject\awesomeProject\interview\inlining\op.go:5)    LEAQ    type.string(SB), AX
    53        00083 (E:\goproject\awesomeProject\interview\inlining\op.go:5)    MOVQ    AX, (SP)
    57        00087 (E:\goproject\awesomeProject\interview\inlining\op.go:5)    LEAQ    ""..stmp_1(SB), AX
    5e        00094 (E:\goproject\awesomeProject\interview\inlining\op.go:5)    MOVQ    AX, 8(SP)
    63        00099 (E:\goproject\awesomeProject\interview\inlining\op.go:5)    PCDATA  $1, $0
    63        00099 (E:\goproject\awesomeProject\interview\inlining\op.go:5)    CALL    runtime.gopanic(SB)
    68        00104 (E:\goproject\awesomeProject\interview\inlining\op.go:5)    XCHGL   AX, AX
    69        00105 (E:\goproject\awesomeProject\interview\inlining\op.go:5)    NOP
    69        00105 (E:\goproject\awesomeProject\interview\inlining\op.go:3)    PCDATA  $1, $-1
    69        00105 (E:\goproject\awesomeProject\interview\inlining\op.go:3)    PCDATA  $0, $-2
    69        00105 (E:\goproject\awesomeProject\interview\inlining\op.go:3)    CALL    runtime.morestack_noctxt(SB)
    6e        00110 (E:\goproject\awesomeProject\interview\inlining\op.go:3)    PCDATA  $0, $-1
    6e        00110 (E:\goproject\awesomeProject\interview\inlining\op.go:3)    JMP     0
    70 done
wrote 3 bytes to 0xc000437e68
 00 70 00
funcpctab "".sub [valfunc=pctoinline]
     0     -1 00000 (E:\goproject\awesomeProject\interview\inlining\op.go:10)   TEXT    "".sub(SB), NOSPLIT|ABIInternal, $0-16
     0        00000 (E:\goproject\awesomeProject\interview\inlining\op.go:10)   TEXT    "".sub(SB), NOSPLIT|ABIInternal, $0-16
     0        00000 (E:\goproject\awesomeProject\interview\inlining\op.go:10)   FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
     0        00000 (E:\goproject\awesomeProject\interview\inlining\op.go:10)   FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
     0     -1 00000 (E:\goproject\awesomeProject\interview\inlining\op.go:11)   MOVSS   "".a+8(SP), X0
     6        00006 (E:\goproject\awesomeProject\interview\inlining\op.go:11)   MOVSS   "".b+12(SP), X1
     c        00012 (E:\goproject\awesomeProject\interview\inlining\op.go:11)   SUBSS   X1, X0
    10        00016 (E:\goproject\awesomeProject\interview\inlining\op.go:11)   MOVSS   X0, "".~r2+16(SP)
    16        00022 (E:\goproject\awesomeProject\interview\inlining\op.go:11)   RET
    17 done
wrote 3 bytes to 0xc000084c68
 00 17 00

 

Go 在生成的代码中映射了内联函数,并且,也映射了行号,可以通过-d pctab=pctoline(可查看源文件行号) 或者是 -gcflags="-d pctab=pctofile"参数查看

此内联函数的映射关系会形成一张映射表,并嵌入到了二进制文件中,所以在运行时可以得到准确的堆栈信息。得到的映射表,可能以以下形式:

 

4、调整内联级别

调整内联级别

使用-gcflags=-l标识调整内联级别。有些令人困惑的是,传递一个-l将禁用内联,两个或两个以上将在更激进的设置中启用内联。

  • -gcflags=-l,禁用内联。
  • 什么都不做,常规的内联
  • -gcflags='-l -l' 内联级别2,更积极,可能更快,可能会制作更大的二进制文件。
  • -gcflags='-l -l -l' 内联级别3,再次更加激进,二进制文件肯定更大,也许更快,但也许会有 bug。
  • -gcflags=-l=4 (4个 -l) 在 Go 1.11 中将支持实验性的 中间栈内联优化。

总之,内联是高性能编程的一种重要手段,每个函数调用都有开销:创建栈帧、读写寄存器,这些开销可以通过内联来避免。但是,对函数踢进行拷贝也会增加大二进制文件的大小。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DreamCatcher

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值