• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

Go程序如何编译成机器代码

原作者: [db:作者] 来自: [db:来源] 收藏 邀请
https://getstream.io/blog

Stream ,我们广泛使用Go,它大大提高了我们的生产率。 我们还发现,通过使用Go,速度非常出色,并且自从开始使用它以来,我们已经实现了堆栈的关键任务部分,例如由gRPC,Raft和RocksDB提供支持的内部存储引擎。

今天,我们将研究Go 1.11编译器以及它将Go源代码编译为可执行文件的方式,以了解我们如何使用日常工作工具。 我们还将看到为什么Go代码如此之快以及编译器如何提供帮助。 我们将看一下编译器的三个阶段:

  • 扫描程序将源代码转换为令牌列表,以供解析器使用。
  • 解析器,将令牌转换成抽象语法树,以供代码生成使用。
  • 代码生成,它将抽象语法树转换为机器代码。

注意: Go编译器不会使用 我们将要使用的软件包( go / scanner go / parser go / token go / ast 等),但主要是供在Go上操作的工具使用的源代码。 但是,实际的Go编译器具有非常相似的语义。 它不使用这些软件包,因为编译器曾经是用C编写并转换为Go代码的,所以实际的Go编译器仍然让人联想到该结构。

扫描器

每个编译器的第一步是将原始源代码文本分解为令牌,这是由扫描程序(也称为lexer)完成的。 令牌可以是关键字,字符串,变量名,函数名等。每个有效的程序“单词”都由令牌表示。 具体来说,这可能意味着我们拥有令牌“ package”,“ main”,“ func”等。

每个标记在Go中均由其位置,类型和原始文本表示。 Go even允许我们使用go / scannergo / token程序包在Go程序中执行扫描程序。 这意味着我们可以在扫描Go编译器之后检查程序的外观。 为此,我们将创建一个简单的程序来打印Hello World程序的所有标记。

该程序将如下所示:

我们将创建源代码字符串并初始化scanner.Scanner结构,它将扫描我们的源代码。 我们会尽可能多地调用Scan()并打印令牌的位置,类型和文字字符串,直到到达文件结尾( EOF )标记。

当我们运行该程序时,它将打印以下内容:

在这里,我们可以看到Go解析器在编译程序时使用的内容。 我们还可以看到,扫描仪添加了分号,通常将分号放在其他编程语言(如C)中。这解释了Go不需要分号的原因:扫描仪可以智能地放置分号。

解析器

扫描源代码后,它将被传递到解析器。 解析器是编译器的阶段,该阶段将令牌转换为抽象语法树(AST)。 AST是源代码的结构化表示。 在AST中,我们将能够看到程序结构,例如函数和常量声明。

Go再次向我们提供了用于解析程序和查看AST的软件包: go / parsergo / ast 我们可以像这样使用它们来打印完整的AST:

输出

在此输出中,您可以看到有关该程序的很多信息。 Decls字段中,有文件中所有声明的列表,例如导入,常量,变量和函数。 在这种情况下,我们只有两个: fmt包的导入和main函数。

为了进一步了解它,我们可以看一下该图,它是上述数据的表示,但只包括类型,红色表示与节点相对应的代码:

主要功能由三部分组成:名称,声明和主体。 名称表示为带有值main的标识符。 由类型字段指定的声明将包含参数列表和返回类型(如果我们指定了任何类型)。 主体由一系列语句组成,其中包含程序的所有行,在这种情况下,只有一行。

我们的单个fmt.Println语句由AST中的很多部分组成。 该语句是一个ExprStmt ,它表示一个表达式,它可以例如是此处的函数调用,也可以是文字,二进制运算(例如加法和减法),一元运算(例如实例取一个数字)等等。 在函数调用的参数中可以使用的任何东西都是表达式。

我们的ExprStmt包含一个CallExpr ,这是我们的实际函数调用。 这又包括几个部分,其中最重要的是FunArgs Fun包含对函数调用的引用,在这种情况下,它是SelectorExpr ,因为我们从fmt包中选择了Println标识符。 但是,在AST中,编译器尚未知道fmt是一个程序包,它也可能是AST中的变量。

Args包含一个表达式列表,这些表达式是该函数的参数。 在这种情况下,我们已经向函数传递了文字字符串,因此它由类型为STRINGBasicLit表示。

显然,我们能够从AST中得出很多结论。 这意味着我们还可以进一步检查AST并查找例如文件中的所有函数调用。 为此,我们将使用ast包中的Inspect函数。 该函数将递归地遍历树,并允许我们检查来自所有节点的信息。

要提取所有函数调用,我们将使用以下代码:

我们在这里正在寻找所有节点,以及它们是否为* ast.CallExpr类型,我们刚刚看到它们代表函数调用。 如果是这样,我们将使用打印机程序包打印Fun成员中存在的函数的名称。

该代码的输出为:

打印机

这确实是我们简单程序中唯一的函数调用,因此我们确实找到了所有函数调用。

构造AST之后,所有导入将使用GOPATH或Go 1.11及更高版本的模块来解决。 然后,将检查类型,并应用一些初步优化,以使程序执行更快。

代码生成

在解决了导入问题并检查了类型之后,我们确定程序是有效的Go代码,并且可以开始将AST转换为(伪)机器代码的过程。

此过程的第一步是将AST转换为程序的低级表示形式,尤其是将其转换为静态单一分配(SSA)形式。 这个中间表示不是最终的机器代码,但是它确实代表了更多的最终机器代码。 SSA具有一组属性,可以更轻松地应用优化,其中最重要的是始终在使用变量之前定义变量,并且每个变量只分配一次。

生成SSA的初始版本后,将应用许多优化过程。 这些优化应用于某些代码段,可以使这些代码段更简单或更快速地执行。 例如,可以消除诸如if(false){fmt.Println(“ test”)}之类的无效代码,因为它将永远不会执行。 优化的另一个示例是可以删除某些nil检查,因为编译器可以证明这些检查永远不会出错。

现在让我们看一下这个简单程序的SSA和一些优化过程:

如您所见,该程序只有一个功能和一个导入。 运行时将打印2。 但是,此样本足以查看SSA。

注意:仅显示主要功能的SSA,这是有趣的部分。

为了显示生成的SSA,我们需要将GOSSAFUNC环境变量设置为要查看其SSA的函数,在本例中为main。 我们还需要将-S标志传递给编译器,因此它将打印代码并创建HTML文件。 我们还将编译用于64位Linux的文件,以确保机器代码与您在此处看到的代码相同。 因此,要编译文件,我们将运行:

$ GOSSAFUNC = main GOOS = linux GOARCH = amd64 go build -gcflags“ -S” simple.go

它会打印所有SSA,但也会生成一个ssa.html交互式文件,因此我们将使用它。

当您打开ssa.html时,将显示许多通行证,其中大多数已折叠。 起始阶段是从AST生成的SSA; 较低的通道将非机器专用的SSA转换为机器专用的SSA,并且genssa是最终生成的机器代码。

开始阶段的代码如下所示:

这个简单的程序已经生成了很多SSA(总共35行)。 但是,很多都是样板,可以消除很多(最终的SSA版本有28行,而最终的机器代码版本有18行)。

每个v是一个新变量,可以单击以查看使用它的位置。 b是块,因此在这种情况下,我们有三个块: b1,b2b3。 b1将始终被执行。 b2b3是条件块,可以通过b1末尾的If v19→b2 b3(可能)看到。 我们可以在该行点击V19查看V19在何处被定义。 我们将其定义为IsSliceInBounds <bool> v14 v15 ,通过查看Go编译器源代码,我们可以看到IsSliceInBounds检查0 <= arg0 <= arg1 我们还可以单击v14v15来查看它们的定义方式,然后将看到v14 = Const64 <int> [0]; Const64是一个常数64位整数。 v15被定义为与1相同。 因此,我们基本上有0 <= 0 <= 1 ,这显然是正确的

编译器也能够证明这一点,当我们看opt阶段(“与机器无关的优化”)时,我们可以看到它已将v19重写为ConstBool <bool> [true] 这将在opt死代码阶段中使用,其中b3被删除,因为之前显示的条件中的v19始终为true。

现在,我们将看看在SSA转换为特定于机器的SSA之后,Go编译器进行的另一种更简单的优化,因此这将是amd64体系结构的机器代码。 为此,我们将比较降低的死代码和降低的死代码。 这是较低阶段的内容:

在HTML文件中,某些行显示为灰色,这意味着它们将在下一阶段之一中被删除或更改。 例如, v15MOVQconst <int> [1] )变灰。 通过点击它进一步审查V15,我们看到它不能用于其他地方,并且MOVQconst基本上是相同的指令,我们看到之前,Const64,只有机专用的AMD64。 因此,我们将v15设置为1 但是, v15在其他任何地方都无法使用,因此它是无用的(死)代码,可以消除。

Go编译器应用了许多此类优化。 因此,虽然AST的第一代SSA可能不是最快的实现,但编译器会将SSA优化为更快的版本。 HTML文件中的每个阶段都是可能会加速的阶段。

如果您有兴趣了解Go编译器中有关SSA的更多信息,请查看Go编译器的SSA源 在此,定义了所有操作以及优化。

结论

Go是一种非常高效的语言,它的编译器和优化为其提供了支持。 要了解有关Go编译器的更多信息, 源代码提供了不错的自述文件。

如果您想了解有关Stream为什么使用Go的更多信息,尤其是为什么我们从Python转到Go的原因,请查看有关切换到Go的博客文章

最初于 2018 年9月25日 发布在 getstream.io 上。

From: https://hackernoon.com/how-a-go-program-compiles-down-to-machine-code-e4532dc8b8ca


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap