百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

Go 语言编译过程概述

toyiye 2024-05-19 19:35 14 浏览 0 评论

Golang 是一门需要编译才能运行的编程语言,也就说代码在运行之前需要通过编译器生成二进制机器码,随后二进制文件才能在目标机器上运行,如果我们想要了解 Go 语言的实现原理,理解它的编译过程就是一个没有办法绕过的事情。

这一节会先对 Go 语言编译的过程进行概述,从顶层介绍编译器执行的几个步骤,随后的章节会分别剖析各个步骤完成的工作和实现原理,同时也会对一些需要预先掌握的知识进行介绍和准备,确保后面的章节能够被更好的理解。

预备知识

想要深入了解 Go 语言的编译过程,需要提前了解一下编译过程中涉及的一些术语和专业知识。这些知识其实在我们的日常工作和学习中比较难用到,但是对于理解编译的过程和原理还是非常重要的。这一小节会简单挑选几个常见并且重要的概念提前进行介绍,减少后面章节的理解压力。

抽象语法树

抽象语法树(AST) 是源代码语法的结构的一种抽象表示,它用树状的方式表示编程语言的语法结构。抽象语法树中的每一个节点都表示源代码中的一个元素,每一颗子树都表示一个语法元素,例如一个 if else 语句,我们可以从 2 * 3 + 7 这一表达式中解析出下图所示的抽象语法树。

作为编译器常用的数据结构,抽象语法树抹去了源代码中不重要的一些字符 - 空格、分号或者括号等等。编译器在执行完语法分析之后会输出一个抽象语法树,这棵树会辅助编译器进行语义分析,我们可以用它来确定结构正确的程序是否存在一些类型不匹配或不一致的问题。

静态单赋值

静态单赋值(SSA)是中间代码的一个特性,如果一个中间代码具有静态单赋值的特性,那么每个变量就只会被赋值一次,在实践中我们通常会用添加下标的方式实现每个变量只能被赋值一次的特性,这里以下面的代码举一个简单的例子:

x := 1
x := 2
y := x

根据分析,我们其实能够发现上述的代码其实并不需要第一个将 1 赋值给 x 的表达式,也就是这一表达式在整个代码片段中是没有作用的:

x1 := 1
x2 := 2
y1 := x2

从使用 SSA 的『中间代码』我们就可以非常清晰地看出变量 y1 的值和 x1 是完全没有任何关系的,所以在机器码生成时其实就可以省略第一步,这样就能减少需要执行的指令来优化这一段代码。

根据 Wikipedia 对 SSA 的介绍来看,在中间代码中使用 SSA 的特性能够为整个程序实现以下的优化:

  1. 常数传播(constant propagation)
  2. 值域传播(value range propagation)
  3. 稀疏有条件的常数传播(sparse conditional constant propagation)
  4. 消除无用的程式码(dead code elimination)
  5. 全域数值编号(global value numbering)
  6. 消除部分的冗余(partial redundancy elimination)
  7. 强度折减(strength reduction)
  8. 寄存器分配(register allocation)

从 SSA 的作用我们就能看出,因为它的主要作用就是代码的优化,所以是编译器后端(主要负责目标代码的优化和生成)的一部分;当然,除了 SSA 之外代码编译领域还有非常多的中间代码优化方法,优化编译器生成的代码是一个非常古老并且复杂的领域,这里就不会展开介绍了。

指令集架构

最后要介绍的一个预备知识就是指令集的架构了,很多开发者都会遇到在生产环境运行的结果和本地不同的问题,导致这种情况的原因其实非常复杂,不同机器使用不同的指令也是可能的原因之一。

我们大多数开发者都会使用 x86_64 的 Macbook 作为工作上主要使用的硬件,在命令行中输入 uname -m 就能够获得当前机器上硬件的信息:

$ uname -m
x86_64

x86_64 是目前比较常见的指令集架构之一,除了 x86_64 之外,还包含其他类型的指令集架构,例如 amd64、arm64 以及 mips 等等,不同的处理器使用了大不相同的机器语言,所以很多编程语言为了在不同的机器上运行需要将源代码根据架构翻译成不同的机器代码。

复杂指令集计算机(CISC)和精简指令集计算机(RISC)是目前的两种 CPU 区别,它们的在设计理念上会有一些不同,从名字我们就能看出来这两种不同的设计有什么区别,复杂指令集通过增加指令的数量减少需要执行的质量数,而精简指令集能使用更少的指令完成目标的计算任务;早期的 CPU 为了减少机器语言指令的数量使用复杂指令集完成计算任务,这两者之前的区别其实就是设计上的权衡,我们会在后面的章节机器码生成 中详细介绍指令集架构,当然各位读者也可以自行搜索和学习。

编译原理

Go 语言编译器的源代码在 cmd/compile 目录中,目录下的文件共同构成了 Go 语言的编译器,学过编译原理的人可能听说过编译器的前端和后端,编译器的前端一般承担着词法分析、语法分析、类型检查和中间代码生成几部分工作,而编译器后端主要负责目标代码的生成和优化,也就是将中间代码翻译成目标『机器』能够运行的机器码。

Go 的编译器在逻辑上可以被分成四个阶段:词法与语法分析、类型检查和 AST 转换、通用 SSA 生成和最后的机器代码生成,在这一节我们会使用比较少的篇幅分别介绍这四个阶段做的工作,后面的章节会具体介绍每一个阶段的具体内容。

词法与语法分析

所有的编译过程其实都是从解析代码的源文件开始的,词法分析的作用就是解析源代码文件,它将文件中的字符串序列转换成 Token 序列,方便后面的处理和解析,我们一般会把执行词法分析的程序称为词法解析器(lexer)。

而语法分析的输入就是词法分析器输出的 Token 序列,这些序列会按照顺序被语法分析器进行解析,语法的解析过程就是将词法分析生成的 Token 按照语言定义好的文法(Grammar)自下而上或者自上而下的进行规约,每一个 Go 的源代码文件最终会被归纳成一个 SourceFile 结构:

SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" } .

标准的 Golang 语法解析器使用的就是 LALR(1) 的文法,语法解析的结果其实就是上面介绍过的抽象语法树(AST),每一个 AST 都对应着一个单独的 Go 语言文件,这个抽象语法树中包括当前文件属于的包名、定义的常量、结构体和函数等。

如果在语法解析的过程中发生了任何语法错误,都会被语法解析器发现并将消息打印到标准输出上,整个编译过程也会随着错误的出现而被中止。

我们会在这一章后面的小节词法与语法分析 中介绍 Go 语言的文法和它的词法与语法解析过程。

类型检查

当拿到一组文件的抽象语法树 AST 之后,Go 语言的编译器会对语法树中定义和使用的类型进行检查,类型检查分别会按照顺序对不同类型的节点进行验证,按照以下的顺序进行处理:

  1. 常量、类型和函数名及类型;
  2. 变量的赋值和初始化;
  3. 函数和闭包的主体;
  4. 哈希键值对的类型;
  5. 导入函数体;
  6. 外部的声明;

通过对每一棵抽象节点树的遍历,我们在每一个节点上都会对当前子树的类型进行验证保证当前节点上不会出现类型错误的问题,所有的类型错误和不匹配都会在这一个阶段被发现和暴露出来。

类型检查的阶段不止会对树状结构的节点进行验证,同时也会对一些内建的函数进行展开和改写,例如 make 关键字在这个阶段会根据子树的结构被替换成 makeslice 或者 makechan 等函数。

我们其实能够看出类型检查不止做了验证类型的工作,还做了对 AST 进行改写,处理 Go 语言内置关键字的活,所以,这一过程在整个编译流程中还是非常重要的,没有这个步骤很多关键字其实就没有办法工作,后面的章节类型检查 会介绍这一步骤。

中间代码生成

当我们将源文件转换成了抽象语法树、对整棵树的语法进行解析并进行类型检查之后,就可以认为当前文件中的代码基本上不存在无法编译或者语法错误的问题了,Go 语言的编译器就会将输入的 AST 转换成中间代码。

Go 语言编译器的中间代码使用了 SSA(Static Single Assignment Form) 的特性,如果我们在中间代码生成的过程中使用这种特性,就能够比较容易的分析出代码中的无用变量和片段并对代码进行优化。

在类型检查之后,就会通过一个名为 compileFunctions 的函数开始对整个 Go 语言项目中的全部函数进行编译,这些函数会在一个编译队列中等待几个后端工作协程的消费,这些 Goroutine 会将所有函数对应的 AST 转换成使用 SSA 特性的中间代码。

中间代码生成 这一章节会详细介绍中间代码的生成过程并简单介绍 Golang 是如何在中间代码中使用 SSA 的特性的,在这里就不展开介绍其他的内容了。

机器码生成

Go 语言源代码的 cmd/compile/internal 中包含了非常多机器码生成相关的包,不同类型的 CPU 分别使用了不同的包进行生成 amd64、arm、arm64、mips、mips64、ppc64、s390x、x86 和 wasm,也就是说 Go 语言能够在上述的 CPU 指令集类型上运行,其中比较有趣的就是 WebAssembly 了。

作为一种在栈虚拟机上使用的二进制指令格式,它的设计的主要目标就是在 Web 浏览器上提供一种具有高可移植性的目标语言。Go 语言的编译器既然能够生成 WASM 格式的指令,那么就能够运行在常见的主流浏览器中。

$ GOARCH=wasm GOOS=js go build -o lib.wasm main.go

我们可以使用上述的命令将 Go 的源代码编译成能够在浏览器上运行的『汇编语言』,除了这种新兴的指令之外,Go 语言还支持了几乎全部常见的 CPU 指令集类型,也就是说它编译出的机器码能够在使用上述指令集的机器上运行。

机器码生成 一节会详细介绍将中间代码翻译到不同目标机器的过程,在这个章节中也会简单介绍不同的指令集架构的区别。

编译器入口

Go 语言的编译器入口在 src/cmd/compile/internal/pc 包中的 main.go 文件,这个 600 多行的 Main 函数就是 Go 语言编译器的主程序,这个函数会先获取命令行传入的参数并更新编译的选项和配置,随后就会开始运行 parseFiles 函数对输入的所有文件进行词法与语法分析得到文件对应的抽象语法树:

func Main(archInit func(*Arch)) {
	// ...

	lines := parseFiles(flag.Args())

接下来就会分九个阶段对抽象语法树进行更新和编译,就像我们在上面介绍的,整个过程会经历类型检查、SSA 中间代码生成以及机器码生成三个部分:

  1. 检查常量、类型和函数的类型;
  2. 处理变量的赋值;
  3. 对函数的主体进行类型检查;
  4. 决定如何捕获变量;
  5. 检查内联函数的类型;
  6. 进行逃逸分析;
  7. 将闭包的主体转换成引用的捕获变量;
  8. 编译顶层函数;
  9. 检查外部依赖的声明;

了解了剩下的编译过程之后,我们重新回到词法和语法分析后的具体流程,在这里编译器会对生成语法树中的节点执行类型检查,除了常量、类型和函数这些顶层声明之外,它还会对变量的赋值语句、函数主体等结构进行检查:

for i := 0; i < len(xtop); i++ {
		n := xtop[i]
		if op := n.Op; op != ODCL && op != OAS && op != OAS2 && (op != ODCLTYPE || !n.Left.Name.Param.Alias) {
			xtop[i] = typecheck(n, ctxStmt)
		}
	}

	for i := 0; i < len(xtop); i++ {
		n := xtop[i]
		if op := n.Op; op == ODCL || op == OAS || op == OAS2 || op == ODCLTYPE && n.Left.Name.Param.Alias {
			xtop[i] = typecheck(n, ctxStmt)
		}
	}

	for i := 0; i < len(xtop); i++ {
		n := xtop[i]
		if op := n.Op; op == ODCLFUNC || op == OCLOSURE {
			typecheckslice(Curfn.Nbody.Slice(), ctxStmt)
		}
	}

	checkMapKeys()

	for _, n := range xtop {
		if n.Op == ODCLFUNC && n.Func.Closure != nil {
			capturevars(n)
		}
	}

	escapes(xtop)

	for _, n := range xtop {
		if n.Op == ODCLFUNC && n.Func.Closure != nil {
			transformclosure(n)
		}
	}

类型检查会对传入节点的子节点进行遍历,这个过程会对 make 等关键字进行展开和重写,类型检查结束之后并没有输出新的数据结构,只是改变了语法树中的一些节点,同时这个过程的结束也意味着源代码中已经不存在语法错误和类型错误,中间代码和机器码也都可以正常的生成了。

initssaconfig()

	peekitabs()

	for i := 0; i < len(xtop); i++ {
		n := xtop[i]
		if n.Op == ODCLFUNC {
			funccompile(n)
		}
	}

	compileFunctions()

	for i, n := range externdcl {
		if n.Op == ONAME {
			externdcl[i] = typecheck(externdcl[i], ctxExpr)
		}
	}

	checkMapKeys()
}

在主程序运行的最后,会将顶层的函数编译成中间代码并根据目标的 CPU 架构生成机器码,不过这里其实也可能会再次对外部依赖进行类型检查以验证正确性。

Go 语言的编译过程其实是非常有趣并且值得学习的,通过对 Go 语言四个编译阶段的分析和对编译器主函数的梳理,我们能够对 Golang 的实现有一些基本的理解,掌握编译的过程之后,Go 语言对于我们来讲也不再是一个黑盒,所以学习其编译原理的过程还是非常让人着迷的。

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码