菜单

后端与代码生成

相关源文件

目的与范围

本文档描述了 Go 编译器的后端,它负责将编译器中间表示 (IR) 转换为特定于体系结构的机器码。后端处理指令选择、寄存器分配、指令调度和其他低级优化。有关前端(解析和类型检查)的信息,请参阅 前端和类型检查,有关 IR 和 SSA 的信息,请参阅 统一 IR 和 SSA

后端架构概览

Go 编译器的后端设计为模块化并支持多种目标体系结构。在编译器的前端生成 SSA(静态单赋值)形式后,后端会将这种与体系结构无关的表示转换为目标平台的机器码。

来源: src/cmd/compile/internal/ssa/opGen.go1-10 src/cmd/compile/internal/ssa/rewriteLOONG64.go1-15 src/cmd/compile/internal/ssa/_gen/LOONG64.rules1-50 src/cmd/compile/internal/ssa/_gen/RISCV64.rules1-50

SSA 到机器码转换

后端首先将 SSA 操作重写为特定于体系结构的操作。每个支持的体系结构都有自己的一组重写规则,将通用操作转换为特定于体系结构的操作。

来源: src/cmd/compile/internal/ssa/rewriteLOONG64.go5-40 src/cmd/compile/internal/ssa/rewriteRISCV64.go5-40 src/cmd/compile/internal/ssa/_gen/LOONG64.rules1-50 src/cmd/compile/internal/ssa/_gen/RISCV64.rules1-50

重写规则

每个支持的体系结构都有一组重写规则,用于定义通用 SSA 操作如何转换为特定于体系结构的操作。这些规则在 src/cmd/compile/internal/ssa/_gen/ 目录下的 .rules 文件中定义,并用于生成特定于体系结构的代码。

例如,LOONG64 的基本算术运算是如何重写的

(Add(Ptr|64|32|16|8) ...) => (ADDV ...)
(Add(32|64)F ...) => (ADD(F|D) ...)

(Sub(Ptr|64|32|16|8) ...) => (SUBV ...)
(Sub(32|64)F ...) => (SUB(F|D) ...)

(Mul(64|32|16|8) ...) => (MULV ...)
(Mul(32|64)F ...) => (MUL(F|D) ...)

RISCV64 的情况也是如此

(Add(Ptr|64|32|16|8) ...) => (ADD ...)
(Add(64|32)F ...) => (FADD(D|S) ...)

(Sub(Ptr|64|32|16|8) ...) => (SUB ...)
(Sub(64|32)F ...) => (FSUB(D|S) ...)

这些规则展示了每个体系结构如何使用不同的指令命名约定和可能的指令语义。

来源: src/cmd/compile/internal/ssa/_gen/LOONG64.rules5-12 src/cmd/compile/internal/ssa/_gen/RISCV64.rules5-10

指令选择

在 SSA 操作被重写为特定于体系结构的后,后端会执行指令选择,为每个操作选择最佳的机器指令。

特定于体系结构的指令表

每个体系结构都有一个指令表,该表定义了可用的指令及其编码。例如,对于 LOONG64,这在 src/cmd/internal/obj/loong64/asm.go 文件中的 optab 数组中定义。

每个体系结构的指令表将操作映射到其机器码表示,包括操作数的编码,并指定每条指令的大小和其他特性。

来源: src/cmd/internal/obj/loong64/asm.go55-427 src/cmd/internal/obj/loong64/a.out.go10-73

寄存器分配

指令选择之后,后端执行寄存器分配,将物理寄存器分配给程序使用的值。这个过程很复杂,因为大多数体系结构只有有限数量的寄存器,而正确的分配对于性能至关重要。

Go 编译器使用线性扫描寄存器分配器,它平衡了编译速度和代码质量。它分析值的活跃性(何时创建和最后使用),以确定何时可以重用寄存器。

寄存器分配器处理不同的寄存器类(通用、浮点、向量),并确保指令获得正确的寄存器类型。

来源: src/cmd/internal/obj/loong64/a.out.go22-54 src/cmd/compile/internal/loong64/ssa.go22-24

体系结构支持与差异

Go 支持多种体系结构,每种都有其自己的后端实现。体系结构之间的主要区别在于指令集、寄存器集和调用约定。

支持的体系结构表

架构寄存器数量 (GP)寄存器数量 (FP)特殊功能
AMD641616SSE/AVX 向量指令
ARM643132NEON 向量指令
LOONG643232LSX/LASX 向量扩展
RISCV643232RVV 向量扩展 (可选)
s390x1616向量设备
PPC643232向量设备

来源: src/cmd/internal/obj/loong64/a.out.go10-20 src/cmd/compile/internal/ssa/opGen.go20-450

LOONG64 特性

LOONG64 是 Go 支持的较新的体系结构之一。它有

  • 32 个通用寄存器 (R0-R31)
  • 32 个浮点寄存器 (F0-F31)
  • 32 个 LSX 向量寄存器 (V0-V31),用于 128 位 SIMD
  • 32 个 LASX 向量寄存器 (X0-X31),用于 256 位 SIMD

操作码格式和指令编码在 asm.go 文件中定义,包含算术、逻辑、内存和控制流指令的操作。

来源: src/cmd/internal/obj/loong64/a.out.go22-187 src/cmd/internal/obj/loong64/asm.go30-90

指令编码与发射

寄存器分配后,后端根据体系结构的指令编码规则将指令编码为二进制表示。

例如,在 LOONG64 中,这由 asm.go 文件中的 asmout 函数处理,该函数使用 optab 表来确定如何编码每条指令。

特殊情况与优化

后端包含对各种情况的特殊处理:

  1. 大常量:当指令无法直接编码大常量时,后端可能会将其展开为多个指令。
  2. 寻址模式:不同的体系结构支持不同的内存操作寻址模式。
  3. 跳转指令:后端会特别处理跳转指令,以确保正确的跳转目标对齐和编码。

来源: src/cmd/internal/obj/loong64/asm.go485-698

内在函数处理

Go 编译器通过“内在函数”对某些函数进行特殊处理——这些函数在编译过程中会被替换为专门的、特定于体系结构的代码。这允许更有效地实现诸如位计数、原子操作和加密原语等函数。

内在函数的示例包括 math/bits 包中的位操作函数,在可用时它们会被映射到特定的硬件指令。

来源: src/cmd/compile/internal/ssagen/intrinsics.go1-44 test/codegen/mathbits.go10-83

测试与验证

Go 编译器包含测试,以验证每个体系结构的代码生成是否正确。

  1. 端到端测试:测试从源代码到可执行文件的整个编译过程。
  2. 指令编码测试:验证指令是否被正确编码。
  3. 代码生成测试:验证特定的 Go 构造是否生成预期的机器码。

例如,test/codegen/mathbits.go 文件测试位操作函数是否生成预期的特定于体系结构的指令。

来源: src/cmd/asm/internal/asm/endtoend_test.go1-24 test/codegen/mathbits.go15-83

结论

Go 编译器的后端是一个复杂的系统,它将高级 Go 代码转换为高效的机器码,支持多种目标体系结构。它通过模块化设计来管理不同的指令集和寄存器模型的复杂性,将与体系结构无关的转换与特定于体系结构的代码生成分离开来。

SSA 形式充当了面向语言的前端和面向硬件的后端之间的桥梁,允许在两个层面进行高效有效的优化。每个支持的体系结构都有其自己的重写规则、指令表和编码逻辑集,使编译器能够为各种平台生成高质量的代码。