Go如何如此迅速地编译?


216

我已经在Go网站上搜索了Google,但在Go的非凡构建时间上似乎找不到解释。它们是语言功能(或缺少语言功能),高度优化的编译器或其他产品的产物吗?我不是要推广Go;我只是好奇。


12
@支持,我知道这一点。我认为,以过快的速度进行编译以实现编译器的目的只是过早的优化。它很可能代表了良好的软件设计和开发实践的结果。另外,我不能忍受看到Knuth的单词脱离上下文而被错误地使用。
亚当·克罗斯兰

55
这个问题的悲观主义者认为“为什么C ++编译这么慢?” stackoverflow.com/questions/588884/...
dan04

14
我投票决定重新提出这个问题,因为它不是基于观点的。可以对语言和/或编译器选择提供一种很好的技术(非专栏)概述,以方便编译速度。
马丁·图尔诺伊

对于小型项目,Go在我看来似乎很慢。这是因为我记得Turbo-Pascal在可能慢了数千倍的计算机上速度要快得多。prog21.dadgum.com/47.html?repost=true。每次我输入“ go build”,几秒钟都没反应,我回想起陈旧的Fortran编译器和穿孔卡。YMMV。TLDR:“慢”和“快”是相对术语。
RedGrittyBrick '17

绝对建议阅读dave.cheney.net/2014/06/07/five-things-that-make-go-fast以获取更详细的见解
Karthik,

Answers:


192

依赖性分析。

转到FAQ用于包含下面的句子:

Go提供了一种用于软件构建的模型,该模型使依赖关系分析变得容易,并避免了C样式包含文件和库的大量开销。

虽然该短语不再是FAQ中的内容,但在Google的Go讨论中对该主题进行了详细说明,该演讲比较了C / C ++和Go的依赖关系分析方法。

这是快速编译的主要原因。这是设计使然。


这个词不再出现在Go常见问题解答中,但是在Google的Go
rob

76

我认为不是Go编译器很快,而是其他编译器很

C和C ++编译器必须解析大量的标头-例如,编译C ++“ hello world”需要编译18k行代码,这几乎是源代码的一半!

$ cpp hello.cpp | wc
  18364   40513  433334

Java和C#编译器在VM中运行,这意味着操作系统必须先加载整个VM,然后才能将它们编译成从字节码到本机代码的JIT编译程序,所有这些过程都需要花费一些时间。

编译速度取决于几个因素。

某些语言旨在快速编译。例如,Pascal被设计为使用单遍编译器进行编译。

编译器本身也可以优化。例如,Turbo Pascal编译器是用手动优化的汇编器编写的,该汇编器与语言设计结合在一起,从而使真正的快速编译器可以在286类硬件上运行。我认为即使是现在,现代的Pascal编译器(例如FreePascal)也比Go编译器更快。


19
Microsoft的C#编译器不在VM中运行。它仍然是用C ++编写的,主要是出于性能原因。
blucz 2011年

19
Turbo Pascal和更高版本的Delphi是极快的编译器的最佳示例。两者的架构师都迁移到Microsoft之后,我们已经看到MS编译器和语言都得到了巨大的改进。这不是一个偶然的巧合。
TheBlastOne

7
18k行(确切地说是18364行)的代码是433334字节(〜0.5MB)
el.pescado15年

9
自2011年以来,C#编译器已使用C#进行编译。只是一个更新,以防以后有人阅读。
Kurt Koller 2015年

3
但是,运行生成的MSIL的C#编译器和CLR是不同的。我相当确定CLR不是用C#编写的。
jocull

39

Go编译器比大多数C / C ++编译器快得多的原因有很多:

  • 主要原因:大多数C / C ++编译器的设计都非常差(从编译速度的角度来看)。同样,从编译速度的角度来看,C / C ++生态系统的某些部分(例如,程序员在其中编写代码的编辑器)在设计时并未考虑到编译速度。

  • 主要原因:快速的编译速度是Go编译器和Go语言的有意识选择

  • Go编译器比C / C ++编译器具有更简单的优化器

  • 与C ++不同,Go没有模板,也没有内联函数。这意味着Go不需要执行任何模板或函数实例化。

  • Go编译器会更快地生成低级汇编代码,而优化器将对汇编代码进行处理,而在典型的C / C ++编译器中,优化将对原始源代码的内部表示进行传递。C / C ++编译器的额外开销来自需要生成内部表示的事实。

  • Go程序的最终链接(5l / 6l / 8l)可能比链接C / C ++程序要慢,因为Go编译器正在遍历所有使用的汇编代码,也许它还在执行C / C ++的其他额外操作链接器没有做

  • 一些C / C ++编译器(GCC)生成文本形式的指令(传递给汇编器),而Go编译器生成二进制形式的指令。为了将文本转换为二进制,需要做额外的工作(但不多)。

  • Go编译器仅针对少量CPU体系结构,而GCC编译器针对大量CPU

  • 旨在提高编译速度的编译器(例如Jikes)速度很快。在2GHz CPU上,Jikes每秒可以编译20000+行Java代码(并且增量编译模式效率更高)。


17
Go的编译器内联小的函数。我不确定以少量的CPU为目标会使您的速度变慢...我假设在为x86编译时gcc不会生成PPC代码。
布拉德·菲茨帕特里克

@BradFitzpatrick不想复活旧的评论,但是通过针对较少数量的平台,编译器的开发人员可以将更多的时间用于为每个评论对其进行优化。
坚持

使用中间形式可以使您支持更多的架构,因为现在您只需要为每个新架构编写一个新的后端
phuclv

34

编译效率是主要设计目标:

最后,它旨在快速:在单台计算机上构建大型可执行文件最多需要几秒钟。为了实现这些目标,需要解决许多语言问题:一个富有表现力但轻巧的字体系统;并发和垃圾回收;严格的依赖规范;等等。常问问题

关于与解析相关的特定语言功能,语言FAQ非常有趣:

其次,该语言的设计易于分析,无需符号表即可进行解析。


6
这不是真的。没有符号表,您将无法完全解析Go源代码。

12
我也看不出为什么垃圾回收会增加编译时间。只是没有。
TheBlastOne

3
这些是FAQ中的引号:golang.org/doc/go_faq.html我不能说他们是否未能实现目标(符号表)或逻辑是否有误(GC)。
拉里·奥布莱恩

5
@FUZxxl转到golang.org/ref/spec#Primary_expressions并考虑两个序列[操作数,呼叫]和[转换]。示例Go源代码:identifier1(identifier2)。没有符号表,就无法确定此示例是调用还是转换。| 任何语言都可以在没有符号表的情况下进行某种程度的解析。的确,可以在没有符号表的情况下解析Go源代码的大部分内容,但是可以识别golang规范中定义的所有语法元素并不是事实。

3
@Atom您正在努力防止解析器成为报告错误的代码段。解析器通常在报告一致性错误消息方面做得很差。在这里,您为表达式创建了一个解析树,就好像它aType是一个变量引用一样,稍后在语义分析阶段,发现它不是您当时要打印有意义的错误。
Sam Harwell 2014年

26

尽管以上大多数情况都是正确的,但有一个非常重要的要点并未真正提及:依赖管理。

转到只需要包括包要导入直接(如那些已经进口了什么,他们不需要)。这与C / C ++形成了鲜明的对比,在C / C ++中,每个文件开始时都包含x头,其中包括y头等。底线:Go的编译将线性时间耗费到了导入包的数量上,其中C / C ++花费了指数时间。


22

自编译是对编译器转换效率的一个很好的测试:自定义编译器需要多长时间进行编译?对于C ++,它需要很长时间(几小时?)。相比之下,Pascal / Modula-2 / Oberon编译器可以在一台现代机器上不到秒钟的时间自行编译[1]。

Go受到了这些语言的启发,但是提高效率的一些主要原因包括:

  1. 明确定义的语法在数学上是合理的,可进行有效的扫描和解析。

  2. 一种类型安全且静态编译的语言,使用独立的编译功能模块边界进行依赖和类型检查,以避免不必要的重新读取头文件和其他模块的重新编译,这与C / C ++中的独立编译相反编译器不会执行此类跨模块检查(因此,即使对于简单的单行“ hello world”程序,也需要一遍又一遍地重新读取所有这些头文件)。

  3. 高效的编译器实现(例如,单遍,递归下降自上而下的解析)-上面的第1点和第2点当然可以极大地帮助您。

这些原则已在1970年代和1980年代以Mesa,Ada,Modula-2 / Oberon等语言和其他几种语言广为人知并得到了全面实施,并且直到现在(2010年代)才逐渐被Go(谷歌)等现代语言所采用。 ,Swift(Apple),C#(Microsoft)等。

我们希望这将很快成为规范,而不是例外。要到达那里,需要发生两件事:

  1. 首先,诸如Google,Microsoft和Apple之类的软件平台提供商应首先鼓励应用程序开发人员使用新的编译方法,同时使他们能够重用其现有代码库。这就是Apple现在尝试使用的Swift编程语言,它可以与Objective-C共存(因为它使用相同的运行时环境)。

  2. 其次,底层软件平台本身最终应使用这些原理随着时间的流逝而重新编写,同时在流程中同时重新设计模块层次结构,以减少它们的整体性。当然,这是一项艰巨的任务,并且可能会花费十年的大部分时间(如果他们有足够的勇气真正做到这一点-对于Google,我完全不确定)。

无论如何,推动语言采用的是平台,而不是相反。

参考文献:

[1] http://www.inf.ethz.ch/personal/wirth/ProjectOberon/PO.System.pdf,第6页:“编译器会在大约3秒钟内完成编译”。该报价是针对低成本Xilinx Spartan-3 FPGA开发板,该开发板以25 MHz的时钟频率运行,并具有1 MB的主存储器。对于运行频率远高于1 GHz的现代处理器和几GB的主内存(即,比Xilinx Spartan-3 FPGA板强大几个数量级)的现代处理器,可以很容易地将其推断为“不到1秒”,即使考虑到I / O速度。早在1990年,Oberon在具有2-4 MBytes主内存的25MHz NS32X32处理器上运行时,编译器便在几秒钟内完成了编译。实际等待的概念当时甚至对于Oberon程序员来说,编译器完成编译周期都是完全未知的。对于典型程序,从等待触发编译命令的鼠标按钮移开手指的时间通常比等待编译器完成刚刚触发的编译要花费更多的时间。真正的即时满足感,等待时间几乎为零。而且,尽管所生成的代码的质量始终不能与当时的最佳编译器完全相提并论,但它对于大多数任务而言还是非常出色的,并且通常是可以接受的。


1
Pascal / Modula-2 / Oberon / Oberon-2编译器可以在一台现代机器上不到一秒钟的时间自行编译 [需要引用]
CoffeeandCode 2014年

1
添加了引文,请参见参考文献[1]。
Andreas 2014年

1
“ ...原理...逐渐进入Go(Google),Swift(Apple)等现代语言中。”不确定Swift如何进入该列表:Swift编译器是glacial。在最近的CocoaHeads柏林聚会上,有人为中型框架提供了一些数字,每秒达到16 LOC。
mpw

13

Go的设计速度很快,并且可以证明。

  1. 依赖管理:没有头文件,您只需要查看直接导入的包即可(无需担心它们的导入方式),因此具有线性依赖关系。
  2. 语法:语言的语法很简单,因此很容易解析。尽管减少了功能部件的数量,但编译器代码本身却很紧凑(路径很少)。
  3. 不允许重载:您看到一个符号,知道该符号指的是哪种方法。
  4. 并行编译Go很简单,因为每个包都可以独立编译。

请注意,GO并不是唯一具有这种功能的语言(模块是现代语言中的规范),但是它们做得很好。


点(4)并非完全正确。相互依赖的模块应按照相关性的顺序进行编译,以允许跨模块内联和填充。
2013年

1
@FUZxxl:虽然这仅涉及优化阶段,但您可以在后端IR生成之前拥有完美的并行性;因此,仅涉及跨模块优化,这可以在链接阶段完成,并且链接无论如何都不并行。当然,如果您不想重复工作(重新解析),则最好以“格状”方式进行编译:1 /不依赖模块,2 /仅依赖(1)的模块,3 /模块仅取决于(1)和(2),...
Matthieu M.

2
使用诸如Makefile之类的基本实用程序,这非常容易做到。
2013年

12

引用艾伦·多诺万(Alan Donovan)和布莱恩·科尼根(Brian Kernighan)的著作《The Go Programming Language》:

即使是从头开始构建,Go编译也比大多数其他编译语言要快。编译器速度的三个主要原因。首先,必须在每个源文件的开头明确列出所有导入,因此编译器不必读取和处理整个文件即可确定其依赖性。其次,包的依赖性形成有向无环图,并且由于没有循环,因此包可以分别编译,也可以并行编译。最后,已编译的Go软件包的目标文件不仅记录软件包本身的导出信息,而且还记录其依赖项的导出信息。编译软件包时,编译器必须为每个导入读取一个目标文件,但不必超出这些文件。


9

编译的基本思想实际上非常简单。原则上,递归下降解析器可以以I / O绑定速度运行。代码生成基本上是一个非常简单的过程。符号表和基本类型系统不需要大量计算。

但是,放慢编译器的速度并不难。

如果存在一个预处理程序阶段,并且具有多级include指令,宏定义和条件编译,那么它们就非常有用,那么将其加载起来并不难。(例如,我正在考虑Windows和MFC头文件。)这就是为什么需要预编译头的原因。

在优化生成的代码方面,对该阶段可以添加多少处理没有限制。


7

简单(用我自己的话),因为语法非常容易(分析和解析)

例如,没有类型继承意味着没有问题的分析来确定新类型是否遵循基本类型强加的规则。

例如,在以下代码示例中:“ interfaces”编译器在分析该类型时不会去检查目标类型是否实现了给定的接口。仅在使用它之前(如果使用了它,则在使用之前)进行检查。

在另一个示例中,编译器会告诉您是否声明了变量并且未使用它(或者是否应该保留返回值而您没有)

以下内容无法编译:

package main
func main() {
    var a int 
    a = 0
}
notused.go:3: a declared and not used

这种强制性和原则使生成的代码更安全,并且编译器不必执行程序员可以执行的额外验证。

总的来说,所有这些细节使语言更易于解析,从而可以快速进行编译。

再用我自己的话说。


3

我认为Go是在创建编译器的同时进行设计的,因此他们从一开始就是最好的朋友。(IMO)


0
  • Go会一次为所有文件导入依赖项,因此导入时间不会随项目大小的增加而呈指数增长。
  • 更简单 语言学意味着解释它们需要较少的计算。

还有什么?

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.