Impala中的代碼生成技術

Cloudera Impala是一種爲Hadoop生態系統打造的開源MPP(massive parallel processing)數據庫,它主要爲分析型查詢負載而設計,而非OLTPImpala能最大限度地利用現代硬件和高效查詢執行的最新技術。LLVM下的運行時代碼生成就是用來提高執行性能的技術之一。html

LLVM簡介

LLVM是一個編譯器及相關工具的庫(toolchain),它不一樣於獨立應用式(stand-alone)的傳統編譯器,LLVM是模塊化且可重用的。它容許Impala這樣的應用在運行的進程內執行JIT(just-in-time)編譯。儘管LLVM因一些特殊的能力以及著名的工具,如比GCC更優的Clang編譯器,但真正使LLVM區別於其餘編譯器的是它的內部架構。前端

 

經典靜態編譯器(像多數C編譯器)中,最流行的設計是前端、優化器、後端組成的三階段設計。前端解析源碼並生成抽象語法樹(ASTAbstract Syntax Tree)。優化器會作不少優化來提高代碼性能。後端(或稱代碼生成器)將代碼轉換成目標平臺的指令集。這種模型對解釋器、JIT編譯器。JVM也是這種模型的一種實現,它使用字節碼做爲前端和優化器之間的接口。正則表達式



這種經典設計對於多語言(包括源語言和目標語言)支持很是重要。只要優化器內部使用一種公共代碼表示,前端和後端就可以編譯任意的語言。當須要移植(porting)編譯器來支持一種新語言時,只需實現一個新前端,而優化器和後端均可以重用。不然就要從新實現整個編譯器,支持M種源語言*N種目標語言。數據庫


 

儘管各類編譯器教科書中都講到三階段設計的種種優勢,但實際上它從未被實現過。像PerlPythonRubyJava的編譯器實現並無共享任何代碼。此外,還有各類各樣的特殊用途的編譯器,例如圖像處理、正則表達式等CPU密集型的子領域的JIT編譯器。GCC因爲混亂的代碼結構而沒法提取出可重用的組件,例如前端和後端重用了某些全局變量等,因此咱們沒法將GCC嵌入到應用程序中。下圖是LLVM對三階段設計的實現。編程


Impala中的LLVM

Impala使用LLVM在運行時產生徹底優化而且查詢特定的函數,這比通用的預編譯函數有更好的性能。尤爲是會在一次查詢中執行許屢次的內層循環(inner loop)的函數。例如,一個用來解析文件記錄並裝載進Impala內存元組的函數,在每一個文件的每一條記錄被掃描時都會被調用。對於這種函數,即便只是簡單的移除一些指令也會獲得速度上的巨大提高。後端

 

若是沒有運行時的代碼生成,爲了處理編譯時未知的運行時數據,函數中老是會包含低效的代碼。例如,僅僅處理整數的記錄解析函數,在處理只有整數的文件時,會比處理各類數據類型的通用函數要快得多。然而要掃描的文件schema在編譯時是未知的,因此這種通用的函數儘管低效,卻也是必要的。數組

 

下圖1中的代碼示例。編譯時記錄個數和類型都是未知的,因此處理函數要寫的儘量通用,避免發生未考慮到的狀況。但JIT與這種思路徹底相反,函數在運行時被徹底編譯成對當前數據最高效的寫法。這在咱們平時看來甚至都不能算做函數,由於徹底不通用,邏輯都用常量固定寫死了,但這正是JIT的策略!因此像下面的動態生成的MaterializeTuple對於不一樣的運行時信息(如不一樣的查詢)會有徹底不一樣的生成版本。架構


 

代碼生成中的經常使用優化技術:ide

Ø  移除條件分支:由於已知運行時信息,因此能夠優化if/switch語句。這是最簡單有效的方式,由於最終機器碼中的分支指令會阻止指令的管道化(instruction pipelining)和並行執行(instruction-level parallelism)。同時,經過展開for循環(由於咱們已經知道循環次數)和解析數據類型,分支指令能被一塊兒移除。模塊化

Ø  移除內存加載:從內存加載數據是開銷很大並且阻止管道化的操做。若是每次加載的結果都同樣的話,咱們就可使用代碼生成來替代數據加載。例如,以前圖1中的數組offsets_types_在每次查詢開始時建立而不會改變,因而在代碼生成的函數版本中,展開for循環後,這些數組中的值能夠直接內聯化。

Ø  內聯虛函數調用:虛函數對性能的影響很大,尤爲是函數很小很簡單,由於它沒法內聯化。所以當對象實例的類型在運行時可知時,咱們可使用代碼生成來取代虛函數的調用,並作內聯化。這對於表達式樹的求值尤其有價值。在Impala中,表達式由操做和函數的樹組成,例以下圖2。樹中出現的每種表達式都是覆蓋(override)表達式基類的函數來實現的,基類會遞歸地調用各個子表達式。許多表達式函數都是很是簡單的,例如兩數相加,因而虛函數調用的開銷甚至大過表達式求值的開銷。經過代碼生成移除虛函數並內聯化,表達式能夠無需函數調用而直接求值。此外,內聯後的函數使編譯器作進一步的優化,例如子表達式消除等。



LLVM生成代碼

Impala受到查詢計劃(query plan,由ImpalaJava前端負責生成)時,LLVM會被用來在查詢執行開始前,生成並編譯對性能相當重要的函數的查詢特定版本。LLVM主要使用IR(intermediate representation)來生成代碼,例如LLVM的前端Clang C++編譯器生成IRLLVM優化IR並將其編譯成機器碼。IR相似於彙編語言,由一些簡單的、可以直接映射成機器碼的指令組成。在Impala中有兩種技術來生成IR函數:使用LLVMIRBuilder API來編程式地生成IR指令;使用CLangC++函數交叉編譯成IR

 

下圖是IR的例子。能夠看出,IR是一種類RISC的虛擬指令集。它支持加減、比較、分支等指令。此外,IR還支持標籤。但與多數RISC不一樣的是:

Ø  LLVM是強類型的,它有一套簡單的類型系統,例如i32, i32**add i32

Ø  LLVM IR支持無限的臨時寄存器,以%開頭。

由於優化器不受源語言和目標平臺限制,因此IR的設計也要遵照這個原則。


 

LLVM中,優化器被組織成優化pass的管道,常見的pass有內聯化、表達式重組、循環不變量移動等。每一個pass都做爲繼承Pass類的C++類,並定義在一個私有的匿名namespace中,同時提供一個讓外界得到到pass的函數。


 

咱們能夠決定pass的執行順序甚至是否執行。當咱們實現一種圖像處理語言的JIT編譯器時,咱們能夠去掉沒用的pass。例如,若是一般都是大函數的話,就不必浪費時間內聯。若是指針不多的話,那麼別名分析和內存優化就變得無關緊要。可是LLVM不是萬能的,PassManager自己也並不知道每一個pass內部的邏輯,因此這仍是由咱們實現者來肯定的。


 

參考資料

1 Runtime Code Generation in Cloudera Impala

2 The Architecture of Open Source Application

 http://www.aosabook.org/en/llvm.html

相關文章
相關標籤/搜索