1-10. 内联优化

1 什么是内联?

  • 内联是将较小的函数合并到它们各自的调用者中的行为。其在不同的计算历史时期的做法不一样,如下:

    • 早期:这种优化通常是由手工完成的。

    • 现在:内联是在编译过程中自动进行的一类基本优化之一。

2 为什么内联很重要?

  • 内联是很重要的,每一门语言都必然会有。具体的原因如下:

    • 它消除了函数调用本身的开销。

    • 它允许编译器更有效地应用其他优化策略。

  • 核心来讲,就是性能更好了。

3 函数调用的开销

3.1 a. 基本知识

  • 在任何语言中调用一个函数都是有代价的。将参数编入寄存器或堆栈(取决于ABI),并在返回时反转这一过程,这些都是开销。

  • 调用一个函数需要将程序计数器从指令流中的一个点跳到另一个点,这可能会导致流水线停滞。

  • 一旦进入函数,通常需要一些前言来为函数的执行准备一个新的堆栈框架,在返回调用者之前,还需要一个类似的尾声来退掉这个框架。

3.2 b. Go 中的开销和优化

  • 在 Go 中,一个函数的调用需要额外的成本来支持动态堆栈的增长。在进入时,goroutine 可用的堆栈空间的数量与函数所需的数量进行比较。

  • 如果可用的堆栈空间不足,前言就会跳转到运行时逻辑,通过将堆栈复制到一个新的、更大的位置来增加堆栈。

  • 消除这些开销的解决方案必须是消除函数调用本身,Go 编译器在某些条件下通过用函数的内容替换对函数的调用来做到这一点,这被称为内联。因为它使函数的主体与它的调用者保持一致。

4 进行内联优化

4.1 a. 不允许内联

  • 内联的效果可以通过这个小例子来证明:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "testing"

//go:noinline
func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

var Result int

func BenchmarkMax(b *testing.B) {
    var r int
    for i := 0; i < b.N; i++ {
        r = max(-1, i)
    }
    Result = r
}
  • 运行这个基准可以得到以下结果:
1
2
% go test -bench=. 
BenchmarkMax-4   530687617         2.24 ns/op
  • 从执行结果来看,max(-1, i)的成本大约是 2.24ns,感觉性能不错。

4.2 b. 允许内联

  • 现在让我们去掉 //go:noinline pragma 的语句,再看看不允许内联的情况下,性能是否会改变。

  • 如下结果:

1
2
% go test -bench=. 
BenchmarkMax-4   1000000000         0.514 ns/op
  • 两个结果对比一看,2.24ns 和 0.51ns。差距至少一倍以上。

  • 另外根据 benchstat 的建议,在内联情况下,性能提高了 78%。

  • 如下结果:

1
2
3
4
5
% benchstat {old,new}.txt

name   old time/op  new time/op  delta

Max-4  2.21ns ± 1%  0.49ns ± 6%  -77.96%  (p=0.000 n=18+19)
Buy me a coffee~
Fred 支付宝支付宝
Fred 微信微信
0%