Go如何快速编译?
我已经Googled围绕Go网站,但我似乎无法findGo的非凡build设时间的解释。 他们是语言function(或缺乏),高度优化的编译器,或其他产品? 我并不是要去推广Go; 我只是好奇。
依赖分析。
从转到常见问题 :
Go为软件构build提供了一个模型,使得依赖分析变得简单,避免了C风格包含文件和库的大部分开销。
这是快速编译的主要原因。 这是devise。
我认为这不是Go编译器速度很快 ,而是其他编译器很慢 。
C和C ++编译器必须parsing大量的头文件,例如,编译C ++“hello world”需要编译18k行代码,这几乎是半个兆字节的源代码!
$ cpp hello.cpp | wc 18364 40513 433334
Java和C#编译器运行在虚拟机上,这意味着在编译任何东西之前,操作系统必须加载整个虚拟机,然后必须从字节码到本地代码进行JIT编译,所有这些都需要一些时间。
编译速度取决于几个因素。
一些语言被devise为快速编译。 例如,Pascal被devise为使用单通编译器进行编译。
编译器本身也可以被优化。 例如,Turbo Pascal编译器是用手工优化的汇编语言编写的,结合语言devise,可以在286级的硬件上实现一个非常快速的编译器。 我认为即使是现在,现代的Pascal编译器(比如FreePascal)也比Go编译器更快。
编译效率是一个主要的devise目标:
最后,它的目的是快速的:在一台计算机上最多需要几秒钟的时间来构build一个大的可执行文件。 为了达到这些目标需要解决一些语言问题:一个expression性但轻量级的types系统; 并发和垃圾收集; 严格依赖规范; 等等。 常问问题
语言常见问题在与parsing相关的特定语言特性方面非常有趣:
其次,这个语言被devise成易于分析,并且可以在没有符号表的情况下被parsing。
Go编译器比大多数C / C ++编译器快得多的原因有很多:
-
最重要的原因 :大多数C / C ++编译器都展示了非常糟糕的devise(从编译速度的angular度来看)。 而且,从编译速度的angular度来看,C / C ++生态系统的某些部分(比如程序员正在编写他们的代码的编辑器)并没有考虑到编译速度。
-
最重要的原因 :快速的编译速度是Go编译器和Go语言的一个有意识的select
-
Go编译器比C / C ++编译器有更简单的优化器
-
与C ++不同,Go没有模板,也没有内联函数。 这意味着Go不需要执行任何模板或函数实例化。
-
Go编译器会尽快生成低级汇编代码,而优化器将在汇编代码上工作,而在典型的C / C ++编译器中,优化将传递原始源代码的内部表示forms。 C / C ++编译器的额外开销来自于需要生成内部表示的事实。
-
Go程序的最终链接(5l / 6l / 8l)可能比链接C / C ++程序慢,因为Go编译器正在经历所有使用的汇编代码,也许它还在执行其他额外的操作,即C / C ++连接器没有做
-
一些C / C ++编译器(GCC)以文本forms生成指令(传递给汇编器),而Go编译器以二进制forms生成指令。 额外的工作(但不多)需要完成,以便将文本转换为二进制文件。
-
Go编译器只针对less量的CPU架构,而GCC编译器则针对大量的CPU
-
编译器的编译速度很快,比如Jikes,速度很快。 在2GHz的CPU上,Jikes可以每秒编译20000行以上的Java代码(并且编译的增量模式更加高效)。
虽然以上大部分都是事实,但还有一点非常重要,那就是:依赖pipe理。
只需要包括你直接导入的包(因为那些已经导入了他们需要的)。 这与C / C ++形成了鲜明的对比,其中每个单独的文件开始包括x头文件,其中包括y头文件等等。底线:Go的编译需要线性时间和导入包的数量,C / C ++需要指数时间。
对编译器翻译效率的一个很好的testing是自编译的:一个给定的编译器自己编译多长时间? 对于C ++来说,需要很长时间(几个小时?)。 相比之下,一台Pascal / Modula-2 / Oberon编译器可以在不到一秒的时间内在现代机器上进行编译[1]。
Go受到这些语言的启发,但这种效率的一些主要原因包括:
-
清晰定义的math语法,用于高效扫描和分析。
-
一种types安全和静态编译的语言,它使用单独的编译, 在模块边界上使用依赖和types检查,以避免不必要的重读头文件和重新编译其他模块 – 而不像C / C ++中的独立编译那样没有这样的跨模块检查是由编译器执行的(因此需要重复读取所有这些头文件,即使是简单的单行“hello world”程序)。
-
一个有效的编译器实现(例如,单通,recursion下降自顶向下parsing) – 这当然是由上面的第1点和第2点大大帮助。
这些原则在二十世纪七十年代和八十年代已经被Mesa,Ada,Modula-2 / Oberon和其他一些语言所熟知并得到了充分的实施,并且直到现在(2010年)才开始进入Go(Google) ,Swift(苹果),C#(微软)等等。
让我们希望这将很快成为规范,而不是例外。 为了达到目的,需要发生两件事:
-
首先,谷歌,微软和苹果等软件平台提供商应该首先鼓励应用程序开发人员使用新的编译方法,同时使他们能够重新使用现有的代码库。 这就是苹果现在正在尝试使用Swift编程语言,它可以与Objective-C共存(因为它使用相同的运行时环境)。
-
其次,底层软件平台本身应该最终随着时间的推移重新编写,同时在这个过程中重新devise模块层次结构,使它们不那么单一。 这当然是一项艰巨的任务,而且可能会花费十年以上的时间(如果他们有足够的勇气去真正做到这一点 – 我对Google的情况一点也不确定)。
无论如何,这是推动语言采用的平台,而不是相反。
参考文献:
[1] personal/wirth/ProjectOberon/PO.System.pdf ,第6页:“编译器在大约3秒内编译”。 此报价适用于以25 MHz时钟频率运行的低成本Xilinx Spartan-3 FPGA开发板,具有1 MB主内存。 从这一点可以很容易地推断出现代处理器的运行时钟频率远高于1 GHz,主存储器数GB(即比Xilinx Spartan-3 FPGA板强几个数量级)的“小于1秒”,即使考虑到I / O速度。 早在1990年,Oberon就已经在具有2-4 MB的主内存的25MHz NS32X32处理器上运行,编译器在几秒钟内编译完成。 实际上等待编译器完成一个编译周期的概念对于Oberon程序员来说是完全不为人知的。 对于典型的程序来说,从触发编译命令的鼠标button上移除手指总是要花费更多的时间,而不是等待编译器完成刚刚触发的编译。 这是真正的即时满足,接近零的等待时间。 所产生的代码的质量,尽pipe并不总是与当时最好的编译器完全一致,但是对于大多数任务而言是非常好的,而且一般来说是完全可以接受的。
去devise是快速的,它显示。
- 依赖pipe理:没有头文件,你只需要看看直接导入的包(不需要担心导入的内容),因此你有线性依赖关系。
- 语法:语言的语法简单,易于parsing。 尽pipe特征的数量减less了,编译器代码本身也很紧(很lesspath)。
- 不允许超载:你看到一个符号,你知道它指的是哪个方法。
- 因为每个软件包都可以独立编译,所以可以并行编译Go。
请注意,GO不是唯一具有这些特征的语言(模块是现代语言中的常用语言),但他们做得很好。
编译的基本思想其实很简单。 原则上,recursion下降parsing器可以以I / O绑定速度运行。 代码生成基本上是一个非常简单的过程。 符号表和基本types系统不需要大量的计算。
但是,放慢编译器并不难。
如果有一个预处理器阶段,使用多级包含指令,macros定义和条件编译,这些都是很有用的,不难加载它。 (例如,我正在考虑Windows和MFC头文件。)这就是为什么预编译头是必要的。
就优化生成的代码而言,可以向该阶段添加多less处理没有限制。
简单地(用我自己的话来说),因为语法很简单(分析和parsing)
例如,没有typesinheritance意味着没有问题的分析来确定新types是否遵循基types规定的规则。
例如,在这个代码示例中: “interfaces”编译器不会去检查预期types是否在分析该types时实现给定的接口。 只有在使用(如果使用)之前,才执行检查。
其他例子,编译器会告诉你,如果你声明了一个variables而不使用它(或者如果你应该保持一个返回值而你不是)
以下不编译:
package main func main() { var a int a = 0 } notused.go:3: a declared and not used
这种强制和原则使结果代码更安全,编译器不必执行程序员可以执行的额外validation。
总之,所有这些细节使得语言更容易parsing,从而导致快速编译。
再次,用我自己的话来说。
我认为Go是与编译器创build并行devise的,所以他们从出生就是最好的朋友。 (IMO)