蛮荆

Golang 编译速度为什么这么快?

2023-10-28

概述

开始接触 GO 语言的时候,笔者已经拥有 C/C++, Java 三种静态语言和 PHP, Javascript 两种动态语言的基础。正因为如此,在入门 Go 语言的过程中, 第一次编译一个几百行的 demo 文件时,着实被其编译速度惊到了,当时心中感慨: “Go 语言号称静态语言的执行速度,动态语言的编译速度,果然名不虚传”。 事实上,Google 发明 Go 语言的核心诉求就是解决其内部的大型项目代码每次构建时都需要花费很长时间的问题,抛开技术的产生历史背景, 本文着重介绍一下 Go 语言编译速度背后的主要优化方案。

极简关键字

Go 语言只有 25 个关键字,这有助于缩短编译时间。

符号表

符号表是编译器中的一种数据结构,用于存储程序中的标志符 (变量、函数、结构体/对象) 等,主要作用是提供了一个管理程序中的各种标志符的快捷方式, 以便在程序编译的不同阶段进行引用、解析、优化等工作。

Go 语言中并没有直接 (显式) 的符号表,编译器会在编译过程中创建一些内部数据结构来管理标志符和类型信息,虽然这些内部数据结构直接暴露给开发者使用的符号表, 但是可以看做是编译器的 “内部符号表”。此外,Go 语言提供了反射 (Reflection) 机制,可以在运行时获取类型、结构体字段、调用方法/堆栈 等, 也可以获取和修改对象的字段,开发者可以通过这种间接的方式获取类似符号表的信息。

依赖分析

这是 Go 语言编译速度性能提升的主要原因

消除循环依赖

循环依赖示例

Go 语言出现循环依赖会在编译时报错,在开发者解决了循环依赖问题之后,编译过程只需要递归到依赖关系树的底部即可,本质上就是一个 DFS (深度优先搜索) 过程。 此外在没有循环依赖的前提下,每个包可以独立并行编译。

依赖简化

Go 语言代码文件中只需要包含直接在代码中使用到的包名,例如当我们需要使用 Cookie 对象时, 只需要引入 cookiejar 包即可,不需要引入 http 包。

package main

import "net/http/cookiejar"

func main() {
	var c cookiejar.Jar
}

与此同时,Go 语言中引入未使用的包也会产生编译错误:

imported and not used:  ...

这个编译前置约束可以保证不相关的包对编译时间造成影响。

最后,Go 语言规定源文件中所有使用到的包必须在文件开始处 (package 语句之后) 全部列出,因此编译器无须读完整个文件内容就可以确定依赖关系。 已经编译完成的 Go 包的目标文件不仅记录了包文件的导出信息,还记录了其本身依赖的包导出信息,编译时每次导入一个目标文件即可,不需要查看其他信息。

不支持重载/重写

Go 语言不支持方法重写和方法重载,也就是说,所有的方法都可以视为静态类型的,就像 Java 里面的 static 方法。

class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!"); 
    }
}

不支持模板

Go 语言同样不支持模板方法,这可以避免模板实例化带来的性能开销。

无须虚拟机

Go 语言和 Java 一样,也是自带 GC 的语言,但是不同的地方在于,Java 是在虚拟机中编译的,因此 Java 代码在通过编译器之前必须先编译为字节码, 但是 Go 不依赖虚拟机编译代码,而是直接从源代码编译为二进制文件。

Reference

转载申请

本作品采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,商业转载请联系作者获得授权。