菜单

优化

相关源文件

本页面描述了 Go 编译器执行的优化过程。它解释了编译器如何分析和转换代码以提高性能,同时保持语义不变。有关函数内联等具体优化的信息,请参阅 函数内联。有关 SSA 优化的详细信息,请参阅 SSA 优化

编译器优化概述

Go 编译器包含一套全面的优化,这些优化在编译的不同阶段运行。大多数优化在类型检查之后、机器码生成之前运行,将代码转换为更高效的形式,同时保持原始语义。这些优化主要在静态单赋值(SSA)框架中实现。

可以使用 -N 标志(禁用所有优化)来禁用优化,或使用更具体的标志进行调整。可以使用 -m 标志监控优化决策的详细程度。

来源: src/cmd/compile/internal/inline/inl.go1-50 src/cmd/compile/internal/ssa/prove.go1-50 src/cmd/compile/internal/ssa/rewrite.go1-50 src/cmd/compile/internal/base/flag.go56-81

优化控制

优化通过几个编译器标志进行控制。其中最重要的标志是

标志描述
-N禁用优化
-l控制内联(级别0禁用)
-m打印优化决策(值越高,细节越多)
-d=ssa/prove/debug=1为值证明启用调试输出
-d=checkptr启用指针检查(影响优化)

默认优化级别启用大多数优化,这等同于设置 -N=0-l=1

来源: src/cmd/compile/internal/base/flag.go56-76 src/cmd/compile/internal/inline/inl.go10-25

函数内联

函数内联是最重要的优化之一。它用被调用函数的代码替换函数调用,消除了调用开销,并允许对组合代码进行进一步优化。

内联由预算系统控制。每个函数都有一个基于其复杂度的成本,只有当其成本低于内联预算阈值时才能将其内联。

编译器使用复杂的启发式方法来决定内联哪些函数

  1. 非常小的函数几乎总是会被内联
  2. 递归函数不会被内联
  3. 标记为 //go:noinline 的函数永远不会被内联
  4. 具有某些属性的函数(例如,使用 runtime.getcallerpc 的函数)无法内联

剖析引导优化 (PGO) 可以影响内联决策,允许在“热”调用点进行更积极的内联。

来源: src/cmd/compile/internal/inline/inl.go48-187 src/cmd/compile/internal/inline/inl.go210-360 src/cmd/compile/internal/inline/inl.go901-970

值证明与分支消除

最有力的优化之一是 SSA 包中的值证明系统。该系统分析值之间的关系,并可以证明如下条件:

  • 一个值是非负的
  • 切片索引在边界内
  • 两个值相等或不相等
  • 一个值在特定范围内

当编译器可以保证它们永远不会失败时,这些证明用于消除不必要的跳转、边界检查和其他运行时安全检查。

证明系统通过部分排序集跟踪变量之间的关系,并通过 SSA 图传播已知的界限和约束。它可以消除

  1. 冗余边界检查(a[i] 后面紧跟着 a[i]
  2. 显式长度比较后的边界检查(if i < len(a) { a[i] = ... }
  3. 不必要的 nil 检查
  4. 死代码分支(当条件可被证明总是真或假时)

来源: src/cmd/compile/internal/ssa/prove.go118-230 src/cmd/compile/internal/ssa/prove.go380-480 test/prove.go15-40

通用重写规则

Go 编译器使用声明式规则系统来简化常见的代码模式。这些规则定义在 _gen/generic.rules 中,并自动应用以将代码转换为更高效的形式。

通用重写规则的例子包括

  • 常量折叠: 1 + 23
  • 代数简化: x * 1xx * 2x << 1
  • 死代码消除: true || xtrue
  • 内存优化: *(&x)x

通用规则与架构无关,并在特定于架构的优化之前应用。这些规则使用 SSA 表示的模式匹配来实现。

来源: src/cmd/compile/internal/ssa/_gen/generic.rules1-60 src/cmd/compile/internal/ssa/_gen/generic.rules187-207 src/cmd/compile/internal/ssa/rewritegeneric.go1-50

特定于架构的优化

在通用优化之后,将应用特定于架构的重写。这些会将操作转换为目标架构最有效的指令。

特定于架构的优化的例子

  1. 使用专用指令(例如,SIMD/矢量指令)
  2. 寄存器分配优化
  3. 寻址模式选择
  4. 指令调度

例如,Power9/Power10 机器具有用于整数乘法加法等操作的专用指令,可用于组合操作。

来源: src/cmd/compile/internal/ssa/rewritePPC64.go1-30 src/cmd/compile/internal/ssa/_gen/PPC64.rules1-40 src/cmd/compile/internal/ppc64/ssa.go1-20

死代码消除

死代码消除会移除不影响程序可观察行为的代码。这包括

  1. 不可达代码(在无条件返回或 panic 之后)
  2. 未使用的变量及其计算
  3. 无操作(例如,x = x
  4. 具有未使用的结果且没有副作用的函数调用

基于 SSA 的优化器可以有效地识别和移除这些结构。死代码消除与其他优化交织在一起,因为每次转换都可能为消除创造新的机会。

来源:src/cmd/compile/internal/ssa/rewrite.go82-93

剖析驱动优化

Go 1.20 引入了剖析驱动优化 (PGO),它使用运行时剖析数据来做出更好的优化决策。启用后,PGO 会影响

  1. 内联决策(对热调用点进行更积极的内联)
  2. 函数布局(将热代码放在一起以提高缓存局部性)
  3. 分支预测提示

PGO 在内联方面尤其有效,因为它可以为频繁调用的函数超出正常的内联预算。

来源:src/cmd/compile/internal/inline/inl.go104-140 src/cmd/compile/internal/inline/inl.go901-965

调试优化

编译器提供了多种方法来调试和理解优化

  • -m 标志显示优化决策
  • -d=ssa/prove/debug=1 显示值证明的详细信息
  • -N 禁用优化
  • -l=0 禁用内联

特别是对于内联,您可以使用

  • -m 显示哪些函数被内联
  • -m=2 显示内联决策的详细原因

通过这些调试标志,开发人员可以了解为什么对他们的代码应用了某些优化或未应用。

来源:src/cmd/compile/internal/base/debug.go19-83 src/cmd/compile/internal/inline/inl.go290-300

优化演进

Go 编译器的优化随着每个版本的发布而不断发展。一些值得注意的发展包括

  1. 改进的逃逸分析以减少堆分配
  2. 更好的内联启发式
  3. 更复杂的边界检查消除
  4. 剖析驱动优化
  5. 更积极的泛型重写规则

优化框架旨在具有可扩展性,允许在不干扰现有优化的前提下添加新优化。

来源:src/cmd/compile/internal/base/debug.go19-50 src/cmd/compile/internal/inline/inl.go48-60