本篇文章給大家分享的是有關(guān)Golang中編譯器的原理是什么,小編覺得挺實(shí)用的,因此分享給大家學(xué)習(xí),希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。
創(chuàng)新互聯(lián)公司是專業(yè)的六合網(wǎng)站建設(shè)公司,六合接單;提供成都做網(wǎng)站、網(wǎng)站設(shè)計(jì),網(wǎng)頁(yè)設(shè)計(jì),網(wǎng)站設(shè)計(jì),建網(wǎng)站,PHP網(wǎng)站建設(shè)等專業(yè)做網(wǎng)站服務(wù);采用PHP框架,可快速的進(jìn)行六合網(wǎng)站開發(fā)網(wǎng)頁(yè)制作和功能擴(kuò)展;專業(yè)做搜索引擎喜愛的網(wǎng)站,專業(yè)的做網(wǎng)站團(tuán)隊(duì),希望更多企業(yè)前來合作!
首先先來認(rèn)識(shí)以下go的代碼源文件分類
命令源碼文件:簡(jiǎn)單說就是含有 main 函數(shù)的那個(gè)文件,通常一個(gè)項(xiàng)目一個(gè)該文件,我也沒想過需要兩個(gè)命令源文件的項(xiàng)目
測(cè)試源碼文件:就是我們寫的單元測(cè)試的代碼,都是以 _test.go 結(jié)尾
庫(kù)源碼文件:沒有上面特征的就是庫(kù)源碼文件,像我們使用的很多第三方包都屬于這部分
go build 命令就是用來編譯這其中的 命令源碼文件 以及它依賴的 庫(kù)源碼文件。下面表格是一些常用的選項(xiàng)在這里集中說明以下。
可選項(xiàng) | 說明 |
---|---|
-a | 將命令源碼文件與庫(kù)源碼文件全部重新構(gòu)建,即使是最新的 |
-n | 把編譯期間涉及的命令全部打印出來,但不會(huì)真的執(zhí)行,非常方便我們學(xué)習(xí) |
-race | 開啟競(jìng)態(tài)條件的檢測(cè),支持的平臺(tái)有限制 |
-x | 打印編譯期間用到的命名,它與 -n 的區(qū)別是,它不僅打印還會(huì)執(zhí)行 |
接下來就用一個(gè) hello world 程序來演示以下上面的命令選項(xiàng)。
如果對(duì)上面的代碼執(zhí)行 go build -n 我們看一下輸出信息:
來分析下整個(gè)執(zhí)行過程
這一部分是編譯的核心,通過 compile、 buildid、 link 三個(gè)命令會(huì)編譯出可執(zhí)行文件 a.out。
然后通過 mv 命令把 a.out 移動(dòng)到當(dāng)前文件夾下面,并改成跟項(xiàng)目文件一樣的名字(這里也可以自己指定名字)。
文章的后面部分,我們主要講的就是 compile、 buildid、 link 這三個(gè)命令涉及的編譯過程。
編譯器原理
這是go編譯器的源碼路徑
如上圖所見,整個(gè)編譯器可以分為:編譯前端與編譯后端;現(xiàn)在我們看看每個(gè)階段編譯器都做了些什么事情。先來從前端部分開始。
詞法分析
詞法分析簡(jiǎn)單來說就是將我們寫的源代碼翻譯成 Token,這是個(gè)什么意思呢?
為了理解 Golang 從源代碼翻譯到 Token 的過程,我們用一段代碼來看一下翻譯的一一對(duì)應(yīng)情況。
圖中重要的地方我都進(jìn)行了注釋,不過這里還是有幾句話多說一下,我們看著上面的代碼想象以下,如果要我們自己來實(shí)現(xiàn)這個(gè)“翻譯工作”,程序要如何識(shí)別 Token 呢?
首先先來給Go的token類型分個(gè)類:變量名、字面量、操作符、分隔符以及關(guān)鍵字。我們需要把一堆源代碼按照規(guī)則進(jìn)行拆分,其實(shí)就是分詞,看著上面的例子代碼我們可以大概制定一個(gè)規(guī)則如下:
鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)
識(shí)別空格,如果是空格可以分一個(gè)詞;
遇到 ( 、)、'<'、'>' 等這些特殊運(yùn)算符的時(shí)候算一個(gè)分詞;
遇到 " 或者 數(shù)字字面量算分詞。
通過上面的簡(jiǎn)單分析,其實(shí)可以看出源代碼轉(zhuǎn) Token 其實(shí)沒有非常復(fù)雜,完全可以自己寫代碼實(shí)現(xiàn)出來。當(dāng)然也有很多通過正則的方式實(shí)現(xiàn)的比較通用的詞法分析器,像 Golang 早期就用的是 lex,在后面的版本中才改用了用go來自己實(shí)現(xiàn)。
語法分析
經(jīng)過詞法分析后,我們拿到的就是 Token 序列,它將作為語法分析器的輸入。然后經(jīng)過處理后生成 AST 結(jié)構(gòu)作為輸出。
所謂的語法分析就是將 Token 轉(zhuǎn)化為可識(shí)別的程序語法結(jié)構(gòu),而 AST 就是這個(gè)語法的抽象表示。構(gòu)造這顆樹有兩種方法。
1. 自上而下
這種方式會(huì)首先構(gòu)造根節(jié)點(diǎn),然后就開始掃描 Token,遇到 STRING 或者其它類型就知道這是在進(jìn)行類型申明,func 就表示是函數(shù)申明。就這樣一直掃描直到程序結(jié)束。
2. 自下而上
這種是與上一種方式相反的,它先構(gòu)造子樹,然后再組裝成一顆完整的樹。
go語言進(jìn)行語法分析使用的是自下而上的方式來構(gòu)造 AST,下面我們就來看一下go語言通過 Token 構(gòu)造的這顆樹是什么樣子。
這其中有意思的地方我全部用文字標(biāo)注出來了。你會(huì)發(fā)現(xiàn)其實(shí)每一個(gè) AST 樹的節(jié)點(diǎn)都與一個(gè) Token 實(shí)際位置相對(duì)應(yīng)。
這顆樹構(gòu)造后,我們可以看到不同的類型是由對(duì)應(yīng)的結(jié)構(gòu)體來進(jìn)行表示的。這里如果有語法、詞法錯(cuò)誤是不會(huì)被解析出來的。因?yàn)榈侥壳盀橹拐f白了都是進(jìn)行的字符串處理。
語義分析
編譯器里邊都把語法分析后的階段叫做 語義分析,而go的這個(gè)階段叫 類型檢查;但是我看了以下go自己的文檔,其實(shí)做的事情沒有太大差別,我們還是按照主流規(guī)范來寫這個(gè)過程。
那么語義分析(類型檢查)究竟要做些什么呢?
AST 生成后,語義分析將使用它作為輸入,并且的有一些相關(guān)的操作也會(huì)直接在這顆樹上進(jìn)行改寫。
首先就是 Golang 文檔中提到的會(huì)進(jìn)行類型檢查,還有類型推斷,查看類型是否匹配,是否進(jìn)行隱式轉(zhuǎn)化(go沒有隱式轉(zhuǎn)化)。如下面的文字所說:
The AST is then type-checked. The first steps are name resolution and type inference, which determine which object belongs to which identifier, and what type each expression has. Type-checking includes certain extra checks, such as "declared and not used" as well as determining whether or not a function terminates.
大意是:生成AST之后是類型檢查(也就是我們這里說的語義分析),第一步是進(jìn)行名稱檢查和類型推斷,簽定每個(gè)對(duì)象所屬的標(biāo)識(shí)符,以及每個(gè)表達(dá)式具有什么類型。類型檢查也還有一些其它的檢查要做,像“聲明未使用”以及確定函數(shù)是否中止。
Certain transformations are also done on the AST. Some nodes are refined based on type information, such as string additions being split from the arithmetic addition node type. Some other examples are dead code elimination, function call inlining, and escape analysis.
這一段是說:AST也會(huì)進(jìn)行轉(zhuǎn)換,有些節(jié)點(diǎn)根據(jù)類型信息進(jìn)行精簡(jiǎn),比如從算術(shù)加法節(jié)點(diǎn)類型中拆分出字符串加法。其它一些例子像dead code的消除,函數(shù)調(diào)用內(nèi)聯(lián)和逃逸分析。
上面兩段文字來自 golang compile
這里多說一句,我們常常在debug代碼的時(shí)候,需要禁止內(nèi)聯(lián),其實(shí)就是操作的這個(gè)階段。
# 編譯的時(shí)候禁止內(nèi)聯(lián) go build -gcflags '-N -l' -N 禁止編譯優(yōu)化 -l 禁止內(nèi)聯(lián),禁止內(nèi)聯(lián)也可以一定程度上減小可執(zhí)行程序大小
經(jīng)過語義分析之后,就可以說明我們的代碼結(jié)構(gòu)、語法都是沒有問題的。所以編譯器前端主要就是解析出編譯器后端可以處理的正確的AST結(jié)構(gòu)。
接下來我們看看編譯器后端又有哪些事情要做。
機(jī)器只能夠理解二進(jìn)制并運(yùn)行,所以編譯器后端的任務(wù)簡(jiǎn)單來說就是怎么把AST翻譯成機(jī)器碼。
中間碼生成
既然已經(jīng)拿到AST,機(jī)器運(yùn)行需要的又是二進(jìn)制。為什么不直接翻譯成二進(jìn)制呢?其實(shí)到目前為止從技術(shù)上來說已經(jīng)完全沒有問題了。
但是,
我們有各種各樣的操作系統(tǒng),有不同的CPU類型,每一種的位數(shù)可能不同;寄存器能夠使用的指令也不同,像是復(fù)雜指令集與精簡(jiǎn)指令集等;在進(jìn)行各個(gè)平臺(tái)的兼容之前,我們還需要替換一些底層函數(shù),比如我們使用make來初始化slice,此時(shí)會(huì)根據(jù)傳入的類型替換為:makeslice64 或者 makeslice。當(dāng)然還有像painc、channel等等函數(shù)的替換也會(huì)在中間碼生成過程中進(jìn)行替換。這一部分的替換操作可以在這里查看
中間碼存在的另外一個(gè)價(jià)值是提升后端編譯的重用,比如我們定義好了一套中間碼應(yīng)該是長(zhǎng)什么樣子,那么后端機(jī)器碼生成就是相對(duì)固定的。每一種語言只需要完成自己的編譯器前端工作即可。這也是大家可以看到現(xiàn)在開發(fā)一門新語言速度比較快的原因。編譯是絕大部分都可以重復(fù)使用的。
而且為了接下來的優(yōu)化工作,中間代碼存在具有非凡的意義。因?yàn)橛心敲炊嗟钠脚_(tái),如果有中間碼我們可以把一些共性的優(yōu)化都放到這里。
中間碼也是有多種格式的,像 Golang 使用的就是SSA特性的中間碼(IR),這種形式的中間碼,最重要的一個(gè)特性就是最在使用變量之前總是定義變量,并且每個(gè)變量只分配一次。
代碼優(yōu)化
在go的編譯文檔中,我并沒找到獨(dú)立的一步進(jìn)行代碼的優(yōu)化。不過根據(jù)我們上面的分析,可以看到其實(shí)代碼優(yōu)化過程遍布編譯器的每一個(gè)階段。大家都會(huì)力所能及的做些事情。
通常我們除了用高效代碼替換低效的之外,還有如下的一些處理:
并行性,充分利用現(xiàn)在多核計(jì)算機(jī)的特性
流水線,cpu有時(shí)候在處理a指令的時(shí)候,還能同時(shí)處理b指令
指令的選擇,為了讓cpu完成某些操作,需要使用指令,但是不同的指令效率有非常大的差別,這里會(huì)進(jìn)行指令優(yōu)化
利用寄存器與高速緩存,我們都知道cpu從寄存器取是最快的,從高速緩存取次之。這里會(huì)進(jìn)行充分的利用
機(jī)器碼生成
經(jīng)過優(yōu)化后的中間代碼,首先會(huì)在這個(gè)階段被轉(zhuǎn)化為匯編代碼(Plan9),而匯編語言僅僅是機(jī)器碼的文本表示,機(jī)器還不能真的去執(zhí)行它。所以這個(gè)階段會(huì)調(diào)用匯編器,匯編器會(huì)根據(jù)我們?cè)趫?zhí)行編譯時(shí)設(shè)置的架構(gòu),調(diào)用對(duì)應(yīng)代碼來生成目標(biāo)機(jī)器碼。
這里比有意思的是,Golang 總說自己的匯編器是跨平臺(tái)的。其實(shí)他也是寫了多分代碼來翻譯最終的機(jī)器碼。因?yàn)樵谌肟诘臅r(shí)候他會(huì)根據(jù)我們所設(shè)置的 GOARCH=xxx 參數(shù)來進(jìn)行初始化處理,然后最終調(diào)用對(duì)應(yīng)架構(gòu)編寫的特定方法來生成機(jī)器碼。這種上層邏輯一致,底層邏輯不一致的處理方式非常通用,非常值得我們學(xué)習(xí)。我們簡(jiǎn)單來一下這個(gè)處理。
首先看入口函數(shù) cmd/compile/main.go:main()
var archInits = map[string]func(*gc.Arch){ "386": x86.Init, "amd64": amd64.Init, "amd64p32": amd64.Init, "arm": arm.Init, "arm64": arm64.Init, "mips": mips.Init, "mipsle": mips.Init, "mips64": mips64.Init, "mips64le": mips64.Init, "ppc64": ppc64.Init, "ppc64le": ppc64.Init, "s390x": s390x.Init, "wasm": wasm.Init, } func main() { // 從上面的map根據(jù)參數(shù)選擇對(duì)應(yīng)架構(gòu)的處理 archInit, ok := archInits[objabi.GOARCH] if !ok { ...... } // 把對(duì)應(yīng)cpu架構(gòu)的對(duì)應(yīng)傳到內(nèi)部去 gc.Main(archInit) }
然后在 cmd/internal/obj/plist.go 中調(diào)用對(duì)應(yīng)架構(gòu)的方法進(jìn)行處理
func Flushplist(ctxt *Link, plist *Plist, newprog ProgAlloc, myimportpath string) { ... ... for _, s := range text { mkfwd(s) linkpatch(ctxt, s, newprog) // 對(duì)應(yīng)架構(gòu)的方法進(jìn)行自己的機(jī)器碼翻譯 ctxt.Arch.Preprocess(ctxt, s, newprog) ctxt.Arch.Assemble(ctxt, s, newprog) linkpcln(ctxt, s) ctxt.populateDWARF(plist.Curfn, s, myimportpath) } }
整個(gè)過程下來,可以看到編譯器后端有很多工作需要做的,你需要對(duì)某一個(gè)指令集、cpu的架構(gòu)了解,才能正確的進(jìn)行翻譯機(jī)器碼。同時(shí)不能僅僅是正確,一個(gè)語言的效率是高還是低,也在很大程度上取決于編譯器后端的優(yōu)化。特別是即將進(jìn)入AI時(shí)代,越來越多的芯片廠商誕生,我估計(jì)以后對(duì)這方面人才的需求會(huì)變得越來越旺盛。
總結(jié)
總結(jié)一下學(xué)習(xí)編譯器這部分古老知識(shí)帶給我的幾個(gè)收獲:
鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)
知道整個(gè)編譯由幾個(gè)階段構(gòu)成,每個(gè)階段做什么事情;但是更深入的每個(gè)階段實(shí)現(xiàn)的一些細(xì)節(jié)還不知道,也不打算知道;
就算是編譯器這種復(fù)雜,很底層的東西也是可以通過分解,讓每一個(gè)階段獨(dú)立變得簡(jiǎn)單、可復(fù)用,這對(duì)我在做應(yīng)用開發(fā)有一些意義;
分層是為了劃分指責(zé),但是某些事情還需要全局的去做,比如優(yōu)化,其實(shí)每一個(gè)階段都會(huì)去做;對(duì)于我們?cè)O(shè)計(jì)系統(tǒng)也是有一定參考意義的;
了解到 Golang 對(duì)外暴露的很多方法其實(shí)是語法糖(如:make、painc etc.),編譯器會(huì)幫我忙進(jìn)行翻譯,最開始我以為是go代碼層面在運(yùn)行時(shí)去做的,類似工廠模式,現(xiàn)在回頭來看自己真是太天真了;
5. 對(duì)接下來準(zhǔn)備學(xué)習(xí)Go的運(yùn)行機(jī)制、以及Plan9匯編進(jìn)行了一些基礎(chǔ)準(zhǔn)備。
以上就是Golang中編譯器的原理是什么,小編相信有部分知識(shí)點(diǎn)可能是我們?nèi)粘9ぷ鲿?huì)見到或用到的。希望你能通過這篇文章學(xué)到更多知識(shí)。更多詳情敬請(qǐng)關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。