<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          漫談Go語言編譯器(01)

          共 5393字,需瀏覽 11分鐘

           ·

          2021-05-09 10:37

          開場白

          我(史斌)曾在Gopher China 2020大會上分享過《Go語言編譯器簡介》(https://github.com/gopherchina/conference/tree/master/2020)。當時僅僅是泛泛的瀏覽了一下Go編譯器,但其實每一頁PPT都能單獨展開講。因此我準備寫一系列文章,把當時未能詳細闡述的內(nèi)容補充一下。
          歡迎轉(zhuǎn)載擴散,但請標注原創(chuàng)出自公眾號:"Golang Contributor Club"。



          為什么學習編譯器

          編譯器對多數(shù)人是一個黑盒,只需了解命令行參數(shù)即可;但我認為深入了解編譯器的內(nèi)部有很多好處。
          第一個好處是:了解編譯器能讓gopher深入了解Go程序的運行機制,從而寫出更優(yōu)質(zhì)的代碼。
          第二個好處是:能提升gopher的內(nèi)功。編譯器涉及正則表達式,樹搜索/樹變換,有向圖變換/有向圖搜索;這些對于寫上層業(yè)務邏輯的gopher以前是虛幻的屠龍術(shù),但是閱讀編譯器源碼后,你會發(fā)現(xiàn)它們其實很實用。
          第三個好處是:你有機會接觸匯編語言,以及各個硬件平臺的特點。ARM64服務器已經(jīng)成為一種趨勢,已經(jīng)在部分場景取代了X86_64服務器。所以我們需要了解不同的硬件平臺的特點。
          第四個好處是:編譯器會是未來的一個熱門行業(yè)。因為美國的制裁,當下是國內(nèi)芯片行業(yè)的春天,加強中國芯的建設已經(jīng)成為一個政治任務。而處理器(CPU/GPU/NPU/TPU/.../xPU)作為芯片領(lǐng)域的重要分支,是需要編譯器支持的。當前國內(nèi)編譯器領(lǐng)域嚴重缺人,因為入門難度高,起薪自然比其它容易入門的領(lǐng)域高。(我的團隊長期招人,歡迎砸簡歷。)
          第五個好處是:你可以基于Go編譯器定制自己的編程語言。Go編譯器從go-1.7開始已經(jīng)實現(xiàn)模塊化,這意味著你可以自己設計一個全新的編程語言(甚至不兼容Go語法),借助Go編譯器編譯成一個可執(zhí)行程序。


          閱讀前的準備

          雖然本系列的定位是科普文,但是我也不準備從最基本的正則表達式,語法樹,有向圖等最基礎(chǔ)的知識講起;因此假設讀者有一定的知識基礎(chǔ)。

          在這個前提下,我希望你看過我在Gopher China 2020上的講座,并閱讀過PPT(https://github.com/gopherchina/conference/tree/master/2020)。

          除此之外,我希望你閱讀過柴樹杉大神寫的《Go語法樹入門》(https://github.com/chai2010/go-ast-book),這是關(guān)于編譯前端非常優(yōu)秀的入門書籍。在此基礎(chǔ)上,本系列的重點是講解編譯中端和后端。

          坦白地說,編譯器確實是最難入門的領(lǐng)域;同時也是最難寫科普文的領(lǐng)域:專業(yè)人士看了覺得淺顯,沒相關(guān)基礎(chǔ)的讀者看了覺得云山霧罩。雖然如此,我還是想挑戰(zhàn)一下。希望能收到讀者更多反饋,我會據(jù)此調(diào)節(jié)講解的內(nèi)容。



          基礎(chǔ)知識回顧
          目前成熟的生產(chǎn)環(huán)境用的編譯器,都是基于久經(jīng)考驗的前中后三階段架構(gòu)。

          其中前端將高級語言的源代碼翻譯成IR(Intermediate Representation)序列,并傳遞給中端;中端對輸入的原始IR序列做通用優(yōu)化,并輸出優(yōu)化后的IR序列給后端;后端接收中端傳來的IR序列,將其映射成真正的匯編指令序列,并做進一步和硬件相關(guān)的特殊優(yōu)化。最終經(jīng)過鏈接生成可執(zhí)行程序。
          這種架構(gòu)的第一個好處是:新的高級語言無需支持所有的硬件,僅需生成IR即可;新的硬件無需適配所有的高級編程語言,僅需適配IR即可。從因果關(guān)系上看,前端和后端各自都是一個子編譯器,前端把高級語言編譯成IR序列,IR對于前端就是(偽)匯編;而后端把IR編譯成真匯編,IR對于后端就是(偽)高級語言。
          這種架構(gòu)的第二個好處是:避免重復性的優(yōu)化。例如把'a*8'優(yōu)化成'a<<3'在所有的硬件上都適用。因此沒必要每個后端都做一遍,把這個優(yōu)化放在中端一次性完成即可。
          這種架構(gòu)的第三個好處是:針對SSA(Single Static Assignment)形態(tài)的IR,已經(jīng)有無數(shù)計算機科學家做了大量細致的研究,有非常成熟的優(yōu)化算法可以借鑒。編譯原理最經(jīng)典的教材龍書的作者,就因為在此領(lǐng)域的開創(chuàng)性貢獻獲得了2021年度的圖靈獎。


          Go編譯器

          Go編譯器在go-1.7之前,采用的是非常老舊的語法樹(AST)覆蓋的編譯技術(shù),從go-1.7開始逐步采用更主流的基于SSA-IR的前中后三階段編譯技術(shù)。雖然Go編譯器無需支持別的高級編程語言,但是上述的第二點和第三點好處仍然適用。

          一個額外的好處是,Go編譯器的中端和后端被做成了獨立的庫"golang.org/x/tools/go/ssa"。這就意味著,你可以自己寫一個新的語言前端(甚至不兼容Go語法),并調(diào)用Go的ssa庫,生成可執(zhí)行程序;甚至于你自己定義的編程語言可以無縫地調(diào)用其它Go的代碼;進一步,你可以借助于Go的生態(tài)系統(tǒng)打造自己的編程語言!類似于Scala語言和Java語言的關(guān)系。


          SSA-IR

          SSA-IR(Single Static Assignment)是一種介于高級語言和匯編語言的中間形態(tài)的偽語言,從高級語言角度看,它是(偽)匯編;而從真正的匯編語言角度看,它是(偽)高級語言。

          顧名思義,SSA(Single Static Assignment)的兩大要點是:

          • Static:每個變量只能賦值一次(因此應該叫常量更合適);

          • Single:每個表達式只能做一個簡單運算,對于復雜的表達式a*b+c*d要拆分成:"t0=a*b; t1=c*d; t2=t0+t1;"三個簡單表達式;

          例如有如下Go源代碼:

          func foo(a, b int) int {  c := 8  return a*4 + b*c}

          它改寫成SSA形式是:

          func foo(a, b int) int {  c := 8  t0 := a * 4  t1 := b * c  t2 := t0 + t1  return t2}
          它被中端優(yōu)化后的SSA形式是:
          func foo(a, b int) int {  t0 := a << 2  t1 := b << 3  t2 := t0 + t1  return t2}
          說到這里,敏感的讀者可能會問:如果只有賦值,那么程序豈不是只有順序結(jié)構(gòu),而分支結(jié)構(gòu)和循環(huán)結(jié)構(gòu)是如何支持的?因此這里要提前劇透一點,Go編譯器的IR不僅有SSA,還有if-goto指令;Go語言的if語句和for循環(huán)語句,都是會被編譯成if-goto指令。事實上在結(jié)構(gòu)化編程(順序/分支/循環(huán))概念出現(xiàn)之前,就是因為goto語句的濫用而導致了1960年代的第一次軟件工程危機。說白了程序運行實際上還是依賴goto,而if/for語句只是為了讓程序更具有可讀性,減少潛在bug。下面是一個小例子:
          func foo(a, b int) int {  if (a > b) {    return a + b  } else {    return a - b}
          它被編譯器前端翻譯成IR后是如下形態(tài)(注意:if-goto-else-goto是一個整體的單條指令,條件/真目的地/假目的地是它的三個操作數(shù),就像除法指令有被除數(shù)和除數(shù)兩個操作數(shù)一樣):
          func foo(a, b int) int {  c := a > b  if (c) goto _true; else goto _false;
          _true:  t0 := a + b  return t0
          _false: t1 := a - b return t1}
          后續(xù)文章會逐步對Go的IR做完整的介紹。這里舉兩個最簡單的例子,便于讀者快速了解IR的核心概念和常見形態(tài)。


          實操

          Go編譯器提供了完備的調(diào)試手段,正好我們可以借用過來展示Go編譯器的內(nèi)部工作流程。本系列文章使用go-1.14.15做演示,請讀者安裝此版本。

          下面用一個例子test.go:

          // test.gopackage main
          func foo(a, b int) int { c := 8 return a*4 + b*c}
          func main() { println(foo(100, 150))}

          使用如下命令編譯,在得到可執(zhí)行目標程序的同時,還會得到一個額外的ssa.html,這個ssa.html就記錄了Go編譯器的工作流程和各階段的中間結(jié)果。其中GOSSAFUNC環(huán)境變量用于指定需要被調(diào)試的函數(shù),本例中是foo。

          $ GOSSAFUNC=foo go build a.go# command-line-argumentsdumped SSA to ./ssa.html
          開ssa.html,可以看到編譯foo函數(shù)一共經(jīng)過了40多道工序。

          其中,source和AST屬于編譯器前端,從start到writebarrier屬于編譯器中端,從lower到genssa屬于編譯器后端。

          這其中的start/writebarrier/genssa三道工序的輸出,請讀者認真看一下。

          start工序是編譯器前端的最后一步,輸出原始IR序列,請讀者對照源碼仔細體會。v10對應變量c,v11對應第一個乘法運算的乘數(shù)4,v12是第一個乘法的積,v13是第二個乘法的積,v14是加法的和。

          start
          b1:v1 (?) = InitMem <mem>v2 (?) = SP <uintptr>v3 (?) = SB <uintptr>v4 (?) = LocalAddr <*int> {a} v2 v1v5 (?) = LocalAddr <*int> {b} v2 v1v6 (?) = LocalAddr <*int> {~r2} v2 v1v7 (4) = Arg <int> {a} (a[int])v8 (4) = Arg <int> {b} (b[int])v9 (?) = Const64 <int> [0]v10 (?) = Const64 <int> [8] (c[int])v11 (?) = Const64 <int> [4]v12 (6) = Mul64 <int> v7 v11v13 (6) = Mul64 <int> v8 v10v14 (6) = Add64 <int> v12 v13v15 (6) = VarDef <mem> {~r2} v1v16 (6) = Store <mem> {int} v6 v14 v15Ret v16 (+6)

          writebarrier是編譯器中端的最后一步,輸出經(jīng)通用優(yōu)化后的IR序列。讀者可以看到,最初的乘法運算(Mul64)被優(yōu)化成了移位運算(Lsh64x64)。

          writebarrier [549 ns]
          b1:v1 (?) = InitMem <mem>v2 (?) = SP <uintptr>v6 (?) = LocalAddr <*int> {~r2} v2 v1v7 (4) = Arg <int> {a} (a[int])v8 (4) = Arg <int> {b} (b[int])v15 (6) = VarDef <mem> {~r2} v1v9 (+6) = Const64 <uint64> [2]v5 (6) = Const64 <uint64> [3]v12 (+6) = Lsh64x64 <int> [false] v7 v9v13 (6) = Lsh64x64 <int> [false] v8 v5v14 (6) = Add64 <int> v12 v13v16 (6) = Store <mem> {int} v6 v14 v15Ret v16 (+6)

          而genssa是后端的最后一步,輸出真正的最優(yōu)的x86_64匯編序列。


          小結(jié)和展望

          本文介紹了理解Go編譯器的所需背景知識,以及Go編譯器的整體工作流程。后續(xù)的文章會逐步針對上面的各道工序展開講解。

          瀏覽 90
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  性爱视频免费看 | 久久鲁在线视频 | 操人视频在线观看 | www.168亚洲毛片基地 | sm免费观看 |