Go 1.3+ 編譯器變革
概述
目前Go編譯器是C寫的,是時(shí)候換成Go啦。
背景
“gc"Go工具鏈來自Plan 9編譯器的工具鏈。組裝器、C編譯器和鏈接器基本沒變。Go的編譯器(cmd/gc,cmd/5g,cmd/6g,cmd/8g)是配合工具鏈寫的新的C程序。
項(xiàng)目起始時(shí),用C而不是Go寫編譯器有很多好處。突出的比如,首先,那時(shí)候Go還不存在,沒法兒寫編譯器。而且實(shí)際上,就算存在,也會(huì)經(jīng)常有明顯的不兼容的變化。用C不用Go可以避免初始和持續(xù)開發(fā)導(dǎo)致的問題。然而如今Go 1已經(jīng)穩(wěn)定,所以這些持續(xù)的問題減少了很多。
持續(xù)開發(fā)的問題已經(jīng)消除,為了讓Go實(shí)現(xiàn)的編譯器比C更有吸引力,另一些工程問題出現(xiàn):
- 寫正確的Go代碼比寫正確的C代碼更容易。
- 調(diào)試錯(cuò)誤的Go代碼比調(diào)試錯(cuò)誤的C代碼更容易。
- 使用Go編譯器需要對(duì)Go有一定理解。而用C編譯器還需要一定理解C。
- Go使并發(fā)執(zhí)行比C更方便。
- Go有更好的標(biāo)準(zhǔn)支持模塊化,自動(dòng)重寫,單元測試和性能分析。
- Go比C更有趣(fun)。
基于以上理由,我們相信是時(shí)候用Go寫Go編譯器啦。
計(jì)劃設(shè)想
我們打算用自動(dòng)化翻譯工具來用Go重寫現(xiàn)在C的編譯器。這個(gè)翻譯需要一些階段,將從Go 1.3開始持續(xù)到未來的發(fā)行版。
第一階段。開發(fā)和調(diào)試一個(gè)自動(dòng)化翻譯工具。這可以在日常開發(fā)時(shí)同步進(jìn)行。而且,人們還可以在這個(gè)階段為C編譯器繼續(xù)改進(jìn)。這個(gè)工 具工作量很大,不過我們有信心完成這個(gè)特殊使命的工具。有許多C的觀念沒法兒直接轉(zhuǎn)換成Go;macros(宏),unions(聯(lián)合,共用 體,),bit fields(位域)可能最先考慮。比較幸運(yùn)(不是巧合),這些功能功能用的少,都會(huì)被翻譯掉。指針運(yùn)算和數(shù)組也需要一些轉(zhuǎn)換工作,盡管編譯器里很少。編 譯器里主要是tree(樹)和linked list(鏈表)。翻譯工具會(huì)保留注釋和C代碼的結(jié)構(gòu),所以翻譯后的代碼和當(dāng)前的編譯器代碼一樣可閱讀。
第二階段。用翻譯工具轉(zhuǎn)換C代碼到Go,并刪除C源碼。這時(shí)我們已經(jīng)開始翻譯,但是Go還是運(yùn)行在C編譯器上。非常樂觀的,這可能發(fā)生在Go 1.3。不過更可能是Go 1.4。
第三階段。使用一些工具,可能來自gofix和the Go oracle,拆分編譯器到包,清理和文檔化代碼,添加適當(dāng)?shù)膯卧獪y試。這是編譯器會(huì)是地道的Go程序。目前打算在Go 1.4實(shí)現(xiàn)。
第四a階段。使用標(biāo)準(zhǔn)的分析和測試工具優(yōu)化編譯器的CPU和內(nèi)存使用??赡芤氩⑿?。如果真這樣,Race Detector(Go的并行競爭檢測工具,)會(huì)有很大幫助。這目標(biāo)在Go 1.4,可能部分會(huì)延后到1.5?;镜膬?yōu)化分析會(huì)在第三階段完成。
第四b階段。(和四a幾段同時(shí)進(jìn)行)當(dāng)編譯器依照明顯的界限分割成包之后, 需要明確引入一個(gè)中介碼,在結(jié)構(gòu)無關(guān)的無序樹(Node*s)和結(jié)構(gòu)相關(guān)的有序鏈表(Prog*s)之間。這個(gè)中介碼應(yīng)該不依賴整體架構(gòu),但是包含準(zhǔn)確的 執(zhí)行順序信息,可以用于有順序但是結(jié)構(gòu)無關(guān)的操作的優(yōu)化,比如清理多余的nil檢測和出界檢測。這些過程基于SSA(靜態(tài)單賦值),你可以從Alan Donovan的 go.tools/ssa 包中了解更多。
第五階段。替換go/parser和go/types到最新(全新)的版本。Robert Griesemer參考現(xiàn)在的經(jīng)驗(yàn),討論了設(shè)計(jì)新的parser和types的可能。如果聯(lián)系他們到編譯器后端,相信對(duì)設(shè)計(jì)新的API有很大幫助。
自展(Bootstrapping)
用Go語言實(shí)現(xiàn)的Go的編譯器,從一開始就要考慮如何自展。我們考慮的規(guī)則就是Go1.3編譯器必須由Go1.2編譯,Go1.4的編譯器必須由Go1.4編譯,以此類推。
這時(shí),我們就有了一個(gè)清晰的流程來生成當(dāng)前的程序:編譯Go1.2的工具鏈(由C編寫),然后使用它編譯Go1.3的工具鏈,以此類推。這里需要一 個(gè)腳本來做這個(gè)事情,來保證只會(huì)消耗CPU的時(shí)間而非某個(gè)人的時(shí)間。這樣的自展,每個(gè)機(jī)器只會(huì)做一次,Go1.x的工具鏈將會(huì)在本地保留,并在執(zhí)行 all.bash來編譯Go1.(x+1)工具鏈的時(shí)候被再次使用。
顯然,隨著時(shí)間的推移這種自舉方式是不充分的。在后面的多個(gè)版本被發(fā)布之前,為編譯器寫一個(gè)后端來生成C代碼也許是一個(gè)更有意義的事情。這些C代碼 不要求效率或可讀性,只要正確即可。這些C代碼將會(huì)被簽入,就像我們簽入由yacc生成的y.tab.c文件一樣。這樣,自展過程就會(huì)變成:先用gcc編 譯C代碼生成一個(gè)自展編譯器,然后使用這個(gè)自展編譯器來編譯真正的編譯器。類似于另一個(gè)自展過程,這個(gè)自展編譯器將會(huì)在本地保留,并在每次執(zhí)行 all.bash的時(shí)候重復(fù)使用(不用重新編譯)。
替代選擇
還有一些比較明顯的替代方案,需要我們說明一下為什么放棄了這些選擇。
從一開始寫一個(gè)編譯器?,F(xiàn)在的編譯器有一個(gè)非常重要的特征:他們能夠正常工作(或者其至少能夠滿足所有用戶的要求)。盡管Go語言比較簡單,但是編譯器中有很多細(xì)微的細(xì)節(jié)優(yōu)化和改寫,直接丟棄10或數(shù)年的在這上面的努力是比較愚蠢的。
對(duì)編譯器進(jìn)行人工翻譯。我們已經(jīng)以人工的方式翻譯了一小部分C/C++代碼到Go語言了。這個(gè)過程是枯燥而且易錯(cuò)的,且這些錯(cuò)誤非常的細(xì)微及難以發(fā) 現(xiàn)。相反,使用機(jī)械翻譯會(huì)形成一些比較一致的錯(cuò)誤,而這些錯(cuò)誤是易于發(fā)現(xiàn)的;而且不會(huì)因?yàn)榭菰锏倪^程開小差。Go編譯器的代碼明顯的比我們翻譯的代碼多很 多:超過60,000行C代碼,機(jī)械翻譯會(huì)使這個(gè)過程容易一些。就像Dick Sites在1974年說的一樣:“相比寫程序,我寧愿寫一個(gè)程序來幫我寫 程序。“ 使用機(jī)械來翻譯編譯器也方便于在準(zhǔn)備好切換之前,我們可以繼續(xù)開發(fā)完善現(xiàn)有的C程序。
只翻譯后端并鏈接到go/parser和go/types.從前端傳給后端的數(shù)據(jù)結(jié)構(gòu)所包含的 信息中,go/parser和go/types所能提供的除了API就沒其他的東西了。如果使用這些庫來替代前端,需要寫代碼來轉(zhuǎn)換go/parser和 go/types所能提供數(shù)據(jù)結(jié)構(gòu)到后端,這是一個(gè)非常寬泛且易出錯(cuò)的工作。我們相信使用這些庫是有意義的,但更明智的是,等到將編譯器代碼調(diào)整的更像 Go程序,分成確定邊界的、包含說明文檔和單元測試子包之后再使用。
放棄現(xiàn)有的編譯器,使用gccgo(或者go/parser + go/types + LLVM, …)?,F(xiàn)有的編譯器是Go語言顯得比較靈活的一個(gè)重要組成部分。如果嘗試使用基于大量代碼的GCC或LLVM來開發(fā)Go程序,感覺會(huì)有礙到Go語言的靈 活性。另外,GCC是大量C代碼(現(xiàn)在有部分C++)、LLVM是大量C++代碼的程序。以上列舉的、用于解釋不使用現(xiàn)有編譯框架代碼的幾個(gè)原因,也都適 用于更多的類似的代碼庫。
C語言的長期使用
臨近結(jié)束,這個(gè)計(jì)劃還留下了由C寫成的Plan9的工具鏈的一部分。在長期發(fā)展中,還是將所有的C從代碼樹排除掉比較好。本章節(jié)推測了一下這件事將會(huì)如何發(fā)生,但不保證其指定會(huì)發(fā)生或者按照這種套路發(fā)生。
運(yùn)行時(shí)包(runtime)。 runtime包的大部分都是用C寫成,基于一些同樣的原因,Go編譯器也是用C實(shí)現(xiàn)。但是,runtime包遠(yuǎn)比 編譯器的代碼量要小,且它現(xiàn)在已經(jīng)是用Go和C混合編寫。將C代碼轉(zhuǎn)換為Go代碼時(shí),一次轉(zhuǎn)化一部分貌似也是可行的。其中,主要部分有:調(diào)度器 (scheduler),垃圾回收(the garbage collector),散列映射表(hash map)的實(shí)現(xiàn),和channel的實(shí)現(xiàn)。(這里Go和C代碼混合的很融洽,是因?yàn)檫@里使用的6c而不是gcc來編譯的C代碼。)
C編譯器。 Plan 9的C編譯器本身就是用C寫成,如果我們要從Go包實(shí)現(xiàn)里面移除所有的C代碼,那么我們將移除這些編譯工具:“go tool 6c”等等,另外,.c的文件也將不被支持出現(xiàn)的Go包的目錄里面。我們應(yīng)該提前聲明這樣的計(jì)劃,以便使用C的第三方包有時(shí)間去移除這類C代碼的使用。 (Cgo,由于使用了gcc來替代6c,所以它仍然可以作為一個(gè)途徑來在Go包中使用C實(shí)現(xiàn)部分功能。)在Go1的兼容性文檔中沒有包含工具鏈修改的描 述,也就是說去掉C編譯器是被允許的。
匯編器。 Plan 9的匯編器也是用C實(shí)現(xiàn)的,但這個(gè)匯編器只不過是一系列解析樹組成的簡單解析器,這使得不論手動(dòng)還是自動(dòng)將它翻譯成Go語言都比較簡單。
連接器。 Plan 9的連接器也是由C寫成。最近的一些工作,已經(jīng)將大部分的連接器工作放到的編譯器中,而且,也已經(jīng)有個(gè)計(jì)劃將剩余的部分重寫成一個(gè)新的、更簡單的Go程序。轉(zhuǎn)移到編譯器的部分連接器代碼,現(xiàn)在需要隨著編譯器的原有代碼一起進(jìn)行翻譯。
基于Libmach的工具: nm, pack, addr2line, 和objdump。 Nm現(xiàn)在已經(jīng)使用Go語言重寫。Pack和addr2line可以任何一天被重寫。Objdump現(xiàn)在依賴于libmach的反匯編器,但這些轉(zhuǎn)換為Go也是比較簡單的,不論是使用機(jī)械還是人工翻譯。所以基于這幾點(diǎn),libmach本身將來也可以被移除。
原文鏈接:https://docs.google.com/document/d/1P3BLR31VA8cvLJLfMibSuTdwTuF7WWLux71CYD0eeD8/preview?sle=true