一統編譯器世界的 LLVM - Part 0. 關於 LLVM 的歷史、淺談及應用
# 一、LLVM 是什麼 ?
# 1-1. 此 VM 非彼 VM - LLVM
來自 LLVM Wiki (opens new window)
LLVM專案的發展起源於2000年伊利諾伊大學厄巴納-香檳分校維克拉姆·艾夫(Vikram Adve)與克里斯·拉特納(Chris Lattner)的研究,他們想要為所有靜態及動態語言創造出動態的編譯技術。LLVM是以BSD授權來發展的開源軟體。2005年,蘋果電腦雇用了克里斯·拉特納及他的團隊為蘋果電腦開發應用程式系統,LLVM為現今macOS及iOS開發工具的一部分。
LLVM的命名最早源自於底層虛擬機器(Low Level Virtual Machine)的首字母縮寫,由於這個專案的範圍並不侷限於建立一個虛擬機器,這個縮寫導致了廣泛的疑惑。LLVM開始成長之後,成為眾多編譯工具及低階工具技術的統稱,使得這個名字變得更不貼切,開發者因而決定放棄這個縮寫的意涵,現今LLVM已單純成為一個品牌,適用於LLVM下的所有專案,包含LLVM中介碼(LLVM IR)、LLVM除錯工具、LLVM C++標準函式庫等。
LLVM 由 Swift 之父 Chris Lattner 創造,並由 Apple 大力贊助,LLVM 最初是 Low Level Virtual Machine 四個字的縮寫,不過隨著專案的發展,LLVM 已經成為了一整套編譯器工具組合,官方也已經放棄 LLVM 四個字的縮寫意義,將專案的簡述訂為「The LLVM Compiler Infrastructure」,所以,LLVM 和虛擬機其實已經沒什麼關聯了,現在就是一個編譯器框架。就像官方給的副標一樣,我們可以將 LLVM 視為一個非常模組化的編譯器框架,對打造語言、研究編譯技術的開發者來說,其強大的地方在於可以將任一個部分抽換掉或插入新的模組,就可以做出一個新的語言、或是將現有程式移植到新硬體平台、或是為特定目的做最佳化後的編譯器。有了 LLVM,就可以讓任意語言都可以跑在任意 CPU 上。
LLVM作為編譯器框架,是需要各種功能模組支撐起來的,你可以將 clang 和 lld 都看做是 LLVM 的組成部分,框架的意思是,你可以基於 LLVM 提供的功能開發自己的模組,並集成在LLVM系統上,增加它的功能,或者就單純自己開發軟件工具,而利用LLVM來支撐底層實現。LLVM由一些函式庫和工具組成,正因為它的這種設計思想,使它可以很容易和 IDE 集成(因為 IDE 軟體可以直接調用函式庫來實現一些如靜態檢查這些功能),也很容易構建生成各種功能的工具(因為新的工具只需要用需要的函式庫就行)。
那什麼是到底什麼是框架呢,和函式庫有甚麼區別 ?
根據知乎庫,框架,架構,平台,有什麼明確的區別? (opens new window)
函式庫實現了可重用的功能,可以被不同應用使用,但只是被動使用,應用可以用這個函式庫,也可以用其他函式庫,但函式庫並不能左右應用。
框架是一個半成品的應用,和函式庫的最大區別是框架直接左右了應用怎麼來寫。
狹義的架構可以是一個應用內部結構,廣義的架構設計到多個應用之間關係、存儲策略、部署策略、危機處理策略。
平台一般來說是針對外部而言的,比如對於客戶你說我是一個電商平台,對內才說架構,對於開發者你說我是一個雲端服務平台,對自己開發者才談架構。
我們如果使用一個框架,就已經被規定了要怎麼撰寫我們的程式,像是 Angular、PHP Laravel、Flask、Django 框架,或是像你有寫過深度學習的程式,可能會用 tensorflow、pytorch 或是更簡單的 keras。你在使用框架時,開發流程、程式邏輯都要照著框架走,框架雖然限制一些撰寫的自由度,但是犧牲這些換取的是更一致性的寫法、更快的開發效率以及更少的除錯時間。你只需要學習怎麼使用這個框架去做你想要的東西就好。剩下的編譯、最佳化、加速、監聽等框架都會幫你做好,框架底層其實也封裝了很多細節,以深度學習為例,假設你不使用框架,你適必要撰寫張量運算、GPU 加速、倒傳遞演算法... 也是可以啦但會先瘋掉🤣。
框架就是一個程式骨架,已經幫你規劃好大致的格局,就像房子一樣,封裝細節的地基鋼筋埋在土裡面你看不到,在土上面的骨架的細部內容(你的程式碼)就要靠工人(碼農QQ)來填補。怎麼填補就要看要實現什麼房子(應用程式),同樣的骨架可以造出不一樣的房子。但使用框架的缺點就是框架大部分都不能一起使用,你有看過有人 pytorch 跟 keras 一起使用的嗎 XD ? 所以你要用其他框架基本上就要重新打掉框架,重新使用另外一個框架。
- 我的程式碼裡面使用了函式庫,執行的是我的程式碼,函式庫是子系統
- 我的程式碼填在框架裡,執行的是框架,我的程式碼是子系統
總結一句,框架就是書架,你只需要按照書架的格局擺放書(程式碼),但怎麼擺放書是你的自由。
# 二、為什麼要使用 LLVM ?
# 2-1. 沒效率的重複造輪子
典型的編譯器可以分成 Frontend、Optimizer(Middle-End)、Backend 三個部分,但傳統編譯器即使架構上遵循 Frontend、Optimizer、Backend 分工,三者之間通常會做得密不可分,在互相交換資料時都走內部私有 API,這使得要修改現有編譯器來做出自己的編譯器相當困難,不同編譯器之間也無法共用已經實作完成的程式碼。在傳統編譯器的開發,三個部份的耦合性太高,導致很多模組重複使用比較困難,因為每一種程式語言都造 屬於自己 的輪子。
綜觀上來說,我們希望可以透過抽換前端跟後端就可以生出一個新的編譯器,而不想要從新開始一條龍打造到結束,在模組的應用中,我們也希望可重複使用前端跟後端封裝好的函數庫或是物件,將我們前後端的開發更加快速,解決不用重新造輪子的窘境。(其實就是希望實踐物件導向的精神更徹底一點啦😂),在現代 compiler 的設計中,就加入了和平台獨立的中間語言(Intermediate Representation, 常用 IR 簡稱),改變了 compiler 開發的生態。這一模型的好處是,當我們要支持多種語言時,只需要添加多個前端就可以了。當需要支持多種目標機器時,只需要添加多個後端就可以了。對於中間的最佳化器,我們可以使用通用的IR。這種加入IR後的三段式的結構還有一個好處,開發前端的人只需要知道如何將原始碼轉換為最佳化器能夠理解的中間程式碼就可以了,他不需要知道最佳化器的工作原理,也不需要了解目標機器的知識。這大大降低了編譯器的開發難度,使更多的開發人員可以參與進來。
# 2-2. LLVM 的核心 - LLVM IR
在 Modern Compiler 設計,加入了中介語言(IR)的部分,大幅改善重複使用上的窘境。將每一個階段分得更獨立,而 LLVM 正是實現了這個理想狀況的編譯器工具包。
LLVM 定義了一個通用的程式中介表示法,LLVM IR。LLVM IR 是一種類似機器語言,但為了通用性以及給編譯器設計者方便而簡化的版本。在 LLVM 的世界裡,大家都講 LLVM IR:Frontend 把原始語言的邏輯翻譯成 LLVM IR、Optimizer 把 LLVM IR 整理成效率更好的 LLVM IR、Backend 拿到 LLVM IR 來生成機器目標平台的機器語言。如此一來,無論是語言設計者想要創造一個新語言、演算法設計師想改進程式的效能、或是硬體或虛擬機製造者要做一個新的平台,都能得益於 LLVM 的世界,新語言只要設計好 frontend parser 就能使用現有的 optimizer 技術並編譯到各種不同平台、新的 optimizer 可以套用在各種語言和平台的編譯器上、新硬體或虛擬機只要支援 LLVM IR 就可以在上面跑各種主流語言。
另外,LLVM Pass 的 input 是 LLVM IR,output 是經過特定改良的 LLVM IR。每個 LLVM Pass 都只會做一項改良(例如直接使用 register 中的值來做計算,而省下 memory 存取次數),而實際要編譯語言的時候,我們就可以依照語言與用途的特性,挑選適合的 LLVM Pass 來裝載。高度彈性且模組化的設計,在編譯器的開發上無往不利, LLVM 的精神就是希望每一個模組都能被重複使用。
在這裡只是簡單介紹 IR 的概念和由來,之後有時間會仔細探討 IR 的最佳化技術。
# 三、LLVM v.s. GCC
在使用編譯器的時候,常常會以 gcc 或是 clang 編譯器前端工具去編譯、除錯你的程式碼,看完前段敘述後,
- 為什麼有 gcc 這麼厲害還要開創另外一個 clang 編譯器 ?
- 那這兩個編譯器的優劣比較
接下來就花一些篇幅娓娓道來。
# 3-1. 有現成的 GCC,為何還要 clang ?
這是有歷史淵源的,眾所皆知 GCC 是一個開源的專案,本身支援很多指令集架構,也支援豐富的程式語言。Apple 一開始編譯工具也是使用 GCC。
但如果大家有用過 git / github 就知道了,有時候 GCC 更新了,蘋果就更新 GCC 的最新功能到自己的專案裡,或是蘋果開發新模組想要合併到 GCC 裡,GCC就會審核後就會合併進去。看似很順利,然而之後蘋果先後又開發了 objective-C
、swift
等程式語言,蘋果希望將開發的功能合併到 GCC 的專案裡,不過蘋果卻吃了很多次閉門羹,GCC 開發者有時候不買單,這讓蘋果很不爽。原因如下
Apple 自己開發的 GCC 模組很難得到GCC委員會的合併。這對於語言的發展不好,如果有什麼語言新特性要加入的話會造成不好的影響、甚至會拖到整體的開發流程。
因為 GCC 的程式碼耦合度太高,很難獨立出來重複使用,程式碼的品質很差。
Apple 想要開發更好的 IDE,需要使用到 GCC 的模組,但是 GCC 不給做。
所以蘋果一氣之下,就自己研發自己的編譯器,找來了剛剛從博士班畢業的編譯器大神 Chris Lattner 共同研發 LLVM 框架。因為LLVM只是一個編譯器框架,所以還需要一個前端來支撐整個系統,起初前端還是用 GCC,之後 Apple 索性撥款撥人一起研發了Clang,作為整個編譯器的前端。往後開發的新語言或是更新語言特性時,就不用合併到 GCC 了,這就是蘋果自行開發 Clang 編譯器前端的原因。有了 LLVM 的專案,蘋果重新拿回編譯器開發的主控權。
# 3-2. 一詞多義的 LLVM / Clang
事實上,你在看文章時,要利用上下文來辨別文章的作者用何種角度看待 LLVM / Clang,他們雖然定義只有一種,但是隨著看待角度的不同,也會有不同的陳述方法。
LLVM 的不同角色
角色 意義 例子 LLVM Infrastructure 整個 LLVM 編譯器框架的程式 (前端、最佳化器、後端、組譯器、鏈接器...) LLVM 項目由以下幾個模組構成 LLVM-based compiler 部分或基於 LLVM 開發的編譯器,可能用 LLVM 的前端或後端來實現 我用 LLVM 把 C 語言編譯到 MIPS 平台上 LLVM Library LLVM 框架由很多函式庫、工具組成的 我用 LLVM 的即時編譯函式庫 LLVM core 通常指 LLVM 最核心的技術 IR 和 Backend,通常會和 Clang 成對出現 Clang / LLVM 是兩個不同的項目 LLVM IR 指 LLVM 的中間表示 Clang是一個前端,能將原始程式碼翻譯成 LLVM Clang
Clang 是 LLVM 的編譯器前端,實現了語法檢查、分析等功能。
大部分的說法和 GCC 一樣,編譯驅動程式(compiler driver),因為LLVM本質上只是一個編譯器框架,所以需要一個驅動程式把整個編譯器的功能串起來,clang能夠監控整個編譯器的流程,即能夠調用到Clang和LLVM的各種函式庫,最終實現編譯的功能。所以跟 GCC 一樣封裝了編譯、組譯、鏈結的流程。
# 3-3. GCC v.s LLVM / Clang
GCC | LLVM / Clang | |
---|---|---|
支援語言 | 多 (Java, Ada, Golang...) | 少 |
指令集架構 | 多 | 較少(但主流的還是有支持) |
編譯器語言擴展功能 (例如 C語言擴充語法 ) | 多 | 少(所以無法編譯 Linux Kernel) |
程式碼品質 | 較差 | 優良 |
程式碼重用性 | 差(本來就沒有要設計成reuse) | 好 |
編譯速度 | 較慢(相對於 clang ) | 較快 |
錯誤提示 | 不太容易理解 | 好閱讀看得懂 |
License | GPL(禁止改作品變成閉源專利軟體) | BSD(可以改作品變成閉源專利軟體,大勝!😁) |
因為 Clang 程式碼品質非常好,開源而且是 BSD 授權,且較年輕,錯誤提示也很完善,因此 Clang 在很多時候都顯得比 GCC 好用。然而現在 LLVM / Clang
仍把能編譯 Linux Kernel 當成重要的目標之一,因為 kernel 的程式碼有用到很多 GCC 的擴展 C 語言語法,而且在最佳化的地方做得仍然比 LLVM / Clang
來的好。支援的語言數量也比較多,導致現在仍然是 GCC 為主流,但是因為現在異構計算的需求,GCC 的專案太亂,LLVM更容易造一些DSL,異構計算也很需要DSL、LLVM可以對硬體進行更高層次的抽象化,未來很有機會與 GCC 分庭抗禮。再來 LLVM突出了它的IR,這樣它的前後端和IR的耦合度就降低了,這樣就非常方便開發新的前端和後端。
# 四、結語
LLVM是一個編譯器基礎架構(infrastructure),把很多編譯器需要的功能以可使用的模組形式實現出來並包裝成函式庫,供其他編譯器開發者可以根據自己的需要選擇使用或者擴展。但是LLVM本身並不是一個完整的編譯器,這是與 GCC 最大的區別。Apple之所以全力支持LLVM主要是因為GCC的GPL許可證限制了其修改但不開源的使用,而LLVM無此限制。
另外,LLVM 讓我們開發編譯器更加輕鬆,也統一了中介語言,將前端、最佳化、後端三個步驟的職責分得更清楚,讓你可以只專注在某一部分的開發。LLVM的架構關鍵在於LLVM IR,前端將語言轉為IR,Optimizer接受IR,進行最佳化後產生更有效率的IR,後端接受IR並產生目標平臺需要的(機器)語言,因為溝通都採用同一種表示方式,這就使得每個階段,都可以有各自實作、改進的可能性。之後的文章會對 LLVM 有更加深入的探討。