什么是LLVM?Swift, Rust, Clang等語言背后的支持
要了解用于以編程方式生成機器原生代碼的編譯器框架是如何讓新語言的推出以及對現(xiàn)有的語言進行增強比以往更加容易了。
新的語言,還有對現(xiàn)有語言的提升,在整個編程環(huán)境中正大行其道。Mozilla 的 Rust、Apple 的 Swift、Jetbrains 的 Kotlin,以及許多其它的語言都給開發(fā)者在速度、安全性、便利性、可移植性還有能力這些方面提供了新的選擇。
為什么現(xiàn)在正當時呢?一個大因素就是那些用來構(gòu)建語言的新工具,特別是編譯器。它們中首當其沖就是 LLVM (底層虛擬機 Low-Level Virtual Machine),這是一個開源項目,最開始作為伊利諾伊州大學的一個研究項目由 Swift 語言的創(chuàng)始人 Chris Lattner 進行開發(fā)。
LLVM 使創(chuàng)建新語言變得更加容易,同時也可以增強現(xiàn)有語言的開發(fā)。它提供了一些工具,用于自動執(zhí)行語言創(chuàng)建任務中最不討人喜歡的部分:創(chuàng)建一個編譯器,將輸出的代碼移植到多個平臺和架構(gòu),編寫代碼來處理常見的語言隱喻,比如異常。它的自由授權(quán)意味著它可以自由地作為軟件組件重用或作為服務部署。
使用 LLVM 的語言名冊中有許多熟悉的名字。蘋果的 Swift 語言使用 LLVM 作為它的編譯器框架,而 Rust 則將 LLVM 作為其工具鏈的核心組件。而且,許多編譯器都有一個 LLVM 版本,如 Clang、C/C++ 編譯器(這個名稱叫做“C-lang”),它本身就是一個與 LLVM 緊密相連的項目。而 Kotlin,名義上是一種 JVM 語言,正在開發(fā)一種名為 Kotlin Native 的語言版本,它使用 LLVM 來編譯成機器原生代碼。
LLVM 定義
在它的核心,LLVM 是一個以編程方式創(chuàng)建機器原生代碼的庫。開發(fā)人員使用該 API 以一種稱為中間代理或 IR 的格式生成指令。然后 LLVM 可以將 IR 編譯成一個獨立的二進制文件,或者在另一個程序(如語言解釋器)的上下文中執(zhí)行 JIT (just-in-time) 編譯。
LLVM 的 API 為開發(fā)在編程語言中發(fā)現(xiàn)的許多常見結(jié)構(gòu)和模式提供了原始的方式。例如,幾乎每種語言都有函數(shù)和全局變量的概念。LLVM 將函數(shù)和全局變量作為其 IR 中的標準元素,因此,你只需在意 LLVM 的實現(xiàn),并關(guān)注需要注意的語言部分,而不是花費時間和精力重新創(chuàng)建這些特定的輪子。

這是一個 LLVM 中間代理(IR)的例子。右邊是一個簡單的 C 程序;左邊是由 Clang 編譯器翻譯成 LLVM IR 的代碼。
LLVM:專為可移植性而生
關(guān)于 LLVM 的一個說法是它像常提到的 C 編程語言:C 語言有時候被認為是一種便攜式、高級的匯編語言,因為它可以緊密地映射到系統(tǒng)硬件的結(jié)構(gòu),而且它已經(jīng)被移植到幾乎所有的系統(tǒng)架構(gòu)。但是,C 語言只是作為一種可移植的匯編語言,是其工作方式的另一種效果;這并不是它的設(shè)計目標之一。
相比之下,LLVM 的 IR 是從一開始就設(shè)計為可移植的組件。它實現(xiàn)這種可移植性的一種方法是提供獨立于任何特定機器架構(gòu)的原語。例如,整數(shù)類型不局限于底層硬件的***位寬度(例如 32 或 64 位),您可以根據(jù)需要使用盡可能多的比特字節(jié)來創(chuàng)建基本的整數(shù)類型,比如 128 位整數(shù)。您也不必擔心手工輸出來匹配特定處理器的指令集;LLVM 也會為你處理這個問題。
如果你希望看到 LLVM IR 的現(xiàn)場示例,請訪問 ELLCC 項目網(wǎng)站,并嘗試在瀏覽器中將 C 代碼轉(zhuǎn)換為 LLVM IR 的現(xiàn)場演示 Demo。
編程語言中如何使用 LLVM
LLVM 最常見的用例是作為一種語言的預先(AOT ahead-of-time)編譯器。但 LLVM 也可以用于即時編譯。
用 LLVM 進行即時編譯
有些情況下需要在運行時動態(tài)生成代碼,而不是預先編譯。例如,Julia 語言就是使用 JIT 編譯代碼,因為它需要快速運行,并通過 REPL(read-eval-print loop)或交互式提示與用戶交互。.Net 和 Mono 可以選擇通過 LLVM 后端方式編譯為原生代碼。
Numba 是一個 Python 的數(shù)學加速包,JIT 將所選擇的 Python 函數(shù)編譯成機器碼。它也可以預先編譯使用 Numba 裝飾器裝飾的代碼,但是(比如 Julia)Python 作為一種快速發(fā)展的解釋性語言,使用 JIT 編譯來產(chǎn)生這樣的代碼更好地補充了 Python 的交互式工作流,比 Python 的預先編譯方式更好。
其他人正在嘗試以非正統(tǒng)方式使用 LLVM 作為 JIT 編譯方式,例如編譯 PostgreSQL 查詢,據(jù)說性能提高了五倍。

Numba 使用 LLVM 進行即時編譯數(shù)字代碼并加速其執(zhí)行。JIT 加速過的 sum2d 函數(shù)的執(zhí)行速度比常規(guī) Python 代碼快 139 倍。
使用 LLVM 進行自動代碼優(yōu)化
LLVM 不僅將 IR 編譯為原生機器碼。你也可以直接以編程的方式在整個鏈接過程中高度精細地優(yōu)化代碼。優(yōu)化方式是相當積極主動的,能夠?qū)崿F(xiàn)包括內(nèi)聯(lián)函數(shù)在內(nèi),消除死代碼(包括未使用的類型聲明和函數(shù)參數(shù))和展開循環(huán)這些事情。
這里再一次強調(diào),LLVM 的力量讓你不必自己實現(xiàn)所有這一切。LLVM 可以為您處理它們,您也可以根據(jù)需要直接禁用。例如,如果你想要一些更小的二進制代碼,那么你可以讓你的編譯器告訴 LLVM 禁用循環(huán)展開。
使用 LLVM 的領(lǐng)域特定語言
LLVM 已被用于生成多種通用語言的編譯器,但它也可用于生成高度垂直或排他性問題域的語言。從某種意義上說,這就是 LLVM 最閃光的地方,因為它在創(chuàng)造這樣一類語言方面消除了諸多苦差事,并使其表現(xiàn)良好。
例如,Emscripten 項目采用 LLVM IR 代碼并將其轉(zhuǎn)換為 JavaScript,理論上支持使用 LLVM 作為后端的任何語言導出可在瀏覽器中運行的代碼。長期規(guī)劃是支持基于 LLVM 的后端并能夠生成 WebAssembly 代碼,Emscripten 是 LLVM 靈活性的一個很好的例子。
LLVM 可以被使用的另一種方法是將特定領(lǐng)域的擴展添加到現(xiàn)有語言。Nvidia 使用 LLVM 創(chuàng)建了 Nvidia CUDA 編譯器,該編譯器允許語言為 CUDA 添加原生支持,它是作為你生成的原生代碼的一部分編譯的,而不是通過附帶的庫進行調(diào)用的。
在不同語言中使用 LLVM
使用 LLVM 的典型方式是通過你所熟悉的語言來編寫代碼(當然也要有支持 LLVM 的庫)。
兩種常見的可選語言是 C 和 C++。許多 LLVM 開發(fā)者會因為以下的原因而默認選擇其中的一個:
- LLVM 本身是用 C++ 編寫的
- LLVM 的 API 以 C 和 C++ 版本提供
- 大量的語言開發(fā)往往會以 C/C++ 作為一個基礎(chǔ)
不過,這兩種語言并不是唯一的選擇。許多語言都可以原生調(diào)用 C 語言庫,所以理論上可以用任何這樣的語言進行 LLVM 開發(fā)。但需要有一個實際的語言庫可以很好地封裝 LLVM API。幸運的是,許多語言和語言運行時都有這樣的庫,包括 C#/.Net/Mono, Rust, Haskell, OCAML, Node.js, Go, 和 Python。
需要注意的是,一些與 LLVM 的語言綁定可能不完整。以 Python 為例,有很多種綁定選擇,但每個選項的完整性和實用性各不相同:
LLVM 項目維護著自己的一套到 LLVM 的 C API 的綁定,但是目前他們沒有繼續(xù)維護。
llvmpy 在 2015 年后就沒有進行維護了 —— 這對于任何軟件項目都是不利的,在使用 LLVM 時更是如此,因為每個版本的 LLVM 都有一些變化。
由創(chuàng)建 Numba 的團隊開發(fā)的 llvmlite 已經(jīng)成為當前在 Python 中的 LLVM 的競爭者。它只實現(xiàn)了 LLVM 功能的一個子集,正如 Numba 項目的需求所規(guī)定的那樣。但是這個子集滿足了絕大多數(shù) LLVM 用戶所需。
llvmcpy 旨在為 C 庫帶來***的 Python 綁定,它以自動化的方式保持更新,并使用 Python 的習慣用法來訪問它們。llvmcpy 還處于早期階段,但是已經(jīng)可以用 LLVM API 做一些基本的工作。
如果你對如何使用 LLVM 庫構(gòu)建語言感興趣,不妨看看 LLVM 的創(chuàng)建者撰寫的使用 C++ 或 OCAML 語言的教程,它將一步步指導你創(chuàng)建一種簡單的名為 Kaleidoscope 的語言。它還被移植到其他語言之上:
- Haskell:參考原始教程的直接移植。
- Python:在此網(wǎng)站的教程和原始版本非常相近,而另一個版本則是用交互式命令行進行更為雄心勃勃的重寫。這兩種版本都使用 llvmlite 作為到 LLVM 的綁定。
- Rust 和 Swift:不可避免地,我們不得不將該教程移植到這兩種語言之上,它們都是由 LLVM 自身幫助使其誕生的。
***,這個教程也有其他國家語言版本的。這里有中文版,分別是使用原始的 C++ 和 Python 版本。
LLVM 尚未實現(xiàn)的功能
了解 LLVM 可以實現(xiàn)的功能的同時,有必要知道 LLVM 目前尚未實現(xiàn)的功能。
例如,LLVM 不解析語言的語法。因為目前已經(jīng)有許多工具實現(xiàn)這個功能,比如 lex/yacc, flex/bison,以及ANTLR。解析語法就意味著必須從編譯中解耦出來,難怪 LLVM 并沒有涉及這個領(lǐng)域。
LLVM 也不會直接干涉到開發(fā)語言的軟件文化,比如安裝編譯器的二進制文件、如何在安裝中管理軟件包、升級工具鏈 —— 這些都需要開發(fā)者自己去實現(xiàn)。
***也是最重要的一點是,LLVM還沒有對部分通用語言成分給出原語。許多語言都具有某種垃圾回收的內(nèi)存管理方式,或者是作為管理內(nèi)存的主要方式,或者作為對 RAII ( C++ 底層實現(xiàn)的自動垃圾回收,表面使用 Rust 語法)等策略的附屬方式。LLVM 并不會給你一個垃圾回收機制,但是它提供了實現(xiàn)垃圾回收的工具,它允許在代碼中使用元數(shù)據(jù)標記,讓編寫垃圾回收器變得更加容易。
盡管如此,但是 LLVM 未來還有有可能添加原生的機制來實現(xiàn)垃圾回收機制。LLVM 正在快速發(fā)展中,大概 6 個月就會有一次大版本的更新。由于當前的許多語言都使用 LLVM 作為開發(fā)的核心,因此 LLVM 的迭代速度只會更快而不會放慢。