概述
目前Go编译器是C写的,是时候换成Go啦。
背景
“gc"Go工具链来自Plan 9编译器的工具链。组装器、C编译器和链接器基本没变。Go的编译器(cmd/gc,cmd/5g,cmd/6g,cmd/8g)是配合工具链写的新的C程序。
项目起始时,用C而不是Go写编译器有很多好处。突出的比如,首先,那时候Go还不存在,没法儿写编译器。而且实际上,就算存在,也会经常有明显的不兼容的变化。用C不用Go可以避免初始和持续开发导致的问题。然而如今Go 1已经稳定,所以这些持续的问题减少了很多。
持续开发的问题已经消除,为了让Go实现的编译器比C更有吸引力,另一些工程问题出现:
- 写正确的Go代码比写正确的C代码更容易。
- 调试错误的Go代码比调试错误的C代码更容易。
- 使用Go编译器需要对Go有一定理解。而用C编译器还需要一定理解C。
- Go使并发执行比C更方便。
- Go有更好的标准支持模块化,自动重写,单元测试和性能分析。
- Go比C更有趣(fun)。
基于以上理由,我们相信是时候用Go写Go编译器啦。
计划设想
我们打算用自动化翻译工具来用Go重写现在C的编译器。这个翻译需要一些阶段,将从Go 1.3开始持续到未来的发行版。
第一阶段。开发和调试一个自动化翻译工具。这可以在日常开发时同步进行。而且,人们还可以在这个阶段为C编译器继续改进。这个工 具工作量很大,不过我们有信心完成这个特殊使命的工具。有许多C的观念没法儿直接转换成Go;macros(宏),unions(联合,共用 体,),bit fields(位域)可能最先考虑。比较幸运(不是巧合),这些功能功能用的少,都会被翻译掉。指针运算和数组也需要一些转换工作,尽管编译器里很少。编 译器里主要是tree(树)和linked list(链表)。翻译工具会保留注释和C代码的结构,所以翻译后的代码和当前的编译器代码一样可阅读。
第二阶段。用翻译工具转换C代码到Go,并删除C源码。这时我们已经开始翻译,但是Go还是运行在C编译器上。非常乐观的,这可能发生在Go 1.3。不过更可能是Go 1.4。
第三阶段。使用一些工具,可能来自gofix和the Go oracle,拆分编译器到包,清理和文档化代码,添加适当的单元测试。这是编译器会是地道的Go程序。目前打算在Go 1.4实现。
第四a阶段。使用标准的分析和测试工具优化编译器的CPU和内存使用。可能要引入并行。如果真这样,Race Detector(Go的并行竞争检测工具,)会有很大帮助。这目标在Go 1.4,可能部分会延后到1.5。基本的优化分析会在第三阶段完成。
第四b阶段。(和四a几段同时进行)当编译器依照明显的界限分割成包之后, 需要明确引入一个中介码,在结构无关的无序树(Node*s)和结构相关的有序链表(Prog*s)之间。这个中介码应该不依赖整体架构,但是包含准确的 执行顺序信息,可以用于有顺序但是结构无关的操作的优化,比如清理多余的nil检测和出界检测。这些过程基于SSA(静态单赋值),你可以从Alan Donovan的 go.tools/ssa 包中了解更多。
第五阶段。替换go/parser和go/types到最新(全新)的版本。Robert Griesemer参考现在的经验,讨论了设计新的parser和types的可能。如果联系他们到编译器后端,相信对设计新的API有很大帮助。
自展(Bootstrapping)
用Go语言实现的Go的编译器,从一开始就要考虑如何自展。我们考虑的规则就是Go1.3编译器必须由Go1.2编译,Go1.4的编译器必须由Go1.4编译,以此类推。
这时,我们就有了一个清晰的流程来生成当前的程序:编译Go1.2的工具链(由C编写),然后使用它编译Go1.3的工具链,以此类推。这里需要一 个脚本来做这个事情,来保证只会消耗CPU的时间而非某个人的时间。这样的自展,每个机器只会做一次,Go1.x的工具链将会在本地保留,并在执行 all.bash来编译Go1.(x+1)工具链的时候被再次使用。
显然,随着时间的推移这种自举方式是不充分的。在后面的多个版本被发布之前,为编译器写一个后端来生成C代码也许是一个更有意义的事情。这些C代码 不要求效率或可读性,只要正确即可。这些C代码将会被签入,就像我们签入由yacc生成的y.tab.c文件一样。这样,自展过程就会变成:先用gcc编 译C代码生成一个自展编译器,然后使用这个自展编译器来编译真正的编译器。类似于另一个自展过程,这个自展编译器将会在本地保留,并在每次执行 all.bash的时候重复使用(不用重新编译)。
替代选择
还有一些比较明显的替代方案,需要我们说明一下为什么放弃了这些选择。
从一开始写一个编译器。现在的编译器有一个非常重要的特征:他们能够正常工作(或者其至少能够满足所有用户的要求)。尽管Go语言比较简单,但是编译器中有很多细微的细节优化和改写,直接丢弃10或数年的在这上面的努力是比较愚蠢的。
对编译器进行人工翻译。我们已经以人工的方式翻译了一小部分C/C++代码到Go语言了。这个过程是枯燥而且易错的,且这些错误非常的细微及难以发 现。相反,使用机械翻译会形成一些比较一致的错误,而这些错误是易于发现的;而且不会因为枯燥的过程开小差。Go编译器的代码明显的比我们翻译的代码多很 多:超过60,000行C代码,机械翻译会使这个过程容易一些。就像Dick Sites在1974年说的一样:“相比写程序,我宁愿写一个程序来帮我写 程序。“ 使用机械来翻译编译器也方便于在准备好切换之前,我们可以继续开发完善现有的C程序。
只翻译后端并链接到go/parser和go/types.从前端传给后端的数据结构所包含的 信息中,go/parser和go/types所能提供的除了API就没其他的东西了。如果使用这些库来替代前端,需要写代码来转换go/parser和 go/types所能提供数据结构到后端,这是一个非常宽泛且易出错的工作。我们相信使用这些库是有意义的,但更明智的是,等到将编译器代码调整的更像 Go程序,分成确定边界的、包含说明文档和单元测试子包之后再使用。
放弃现有的编译器,使用gccgo(或者go/parser + go/types + LLVM, …)。现有的编译器是Go语言显得比较灵活的一个重要组成部分。如果尝试使用基于大量代码的GCC或LLVM来开发Go程序,感觉会有碍到Go语言的灵 活性。另外,GCC是大量C代码(现在有部分C++)、LLVM是大量C++代码的程序。以上列举的、用于解释不使用现有编译框架代码的几个原因,也都适 用于更多的类似的代码库。
C语言的长期使用
临近结束,这个计划还留下了由C写成的Plan9的工具链的一部分。在长期发展中,还是将所有的C从代码树排除掉比较好。本章节推测了一下这件事将会如何发生,但不保证其指定会发生或者按照这种套路发生。
运行时包(runtime)。 runtime包的大部分都是用C写成,基于一些同样的原因,Go编译器也是用C实现。但是,runtime包远比 编译器的代码量要小,且它现在已经是用Go和C混合编写。将C代码转换为Go代码时,一次转化一部分貌似也是可行的。其中,主要部分有:调度器 (scheduler),垃圾回收(the garbage collector),散列映射表(hash map)的实现,和channel的实现。(这里Go和C代码混合的很融洽,是因为这里使用的6c而不是gcc来编译的C代码。)
C编译器。 Plan 9的C编译器本身就是用C写成,如果我们要从Go包实现里面移除所有的C代码,那么我们将移除这些编译工具:“go tool 6c”等等,另外,.c的文件也将不被支持出现的Go包的目录里面。我们应该提前声明这样的计划,以便使用C的第三方包有时间去移除这类C代码的使用。 (Cgo,由于使用了gcc来替代6c,所以它仍然可以作为一个途径来在Go包中使用C实现部分功能。)在Go1的兼容性文档中没有包含工具链修改的描 述,也就是说去掉C编译器是被允许的。
汇编器。 Plan 9的汇编器也是用C实现的,但这个汇编器只不过是一系列解析树组成的简单解析器,这使得不论手动还是自动将它翻译成Go语言都比较简单。
连接器。 Plan 9的连接器也是由C写成。最近的一些工作,已经将大部分的连接器工作放到的编译器中,而且,也已经有个计划将剩余的部分重写成一个新的、更简单的Go程序。转移到编译器的部分连接器代码,现在需要随着编译器的原有代码一起进行翻译。
基于Libmach的工具: nm, pack, addr2line, 和objdump。 Nm现在已经使用Go语言重写。Pack和addr2line可以任何一天被重写。Objdump现在依赖于libmach的反汇编器,但这些转换为Go也是比较简单的,不论是使用机械还是人工翻译。所以基于这几点,libmach本身将来也可以被移除。
原文链接:https://docs.google.com/document/d/1P3BLR31VA8cvLJLfMibSuTdwTuF7WWLux71CYD0eeD8/preview?sle=true