Impala是如何提升3~90倍查詢效率的?
Cloudera Impala是基于Hadoop的實時檢索引擎開源項目,其效率較Hive提升3~90倍,詳見Cloudera的 blog。
為什么是代碼生成?
這一切的基礎是最優的查詢引擎一定是原生的應用,因為它們針對你的數據格式而開發,而且僅僅支持你的查詢。舉個例子,這是一個理想的代碼:
|
1
2
3
|
select count(*)from tblwhere col like %XYZ% |
這與 grep -c "XYZ" tbl 的效率一樣高。
另一個例子,select sum(col) from tbl。如果表格只有一個int64的列,使用little endian編碼,這可以通過專用的應用來運行:
|
1
2
3
4
5
|
int64_t sum = 0;int64_t* values = (int64_t*)buffer;for (int i = 0; i < num_rows; ++i) { sum += values[i]; } |
這兩個查詢都是十分合理的(因為第二段代碼用于列式的數據),不過在運行已有的查詢引擎時會變得緩慢。(這是假設強制執行的情況;一個數據庫當然可以使用索引或者預先計算的值來運行,其效率要高過簡單的應用。當然,這里的技術同樣應該使用非強制執行策略來實現。)這是因為如今的應用程序大多數都遵循了添加多種執行開銷這一解釋方法。 #p#
增加的運行成本來自:
調用虛函數。沒有編譯就解釋執行表達式(例如col1 + col2 < col3),致使在每個表達式上產生虛函數調用。(這當然依賴于安裝啟用,但是我們,也可能包括大多數其它人采用一種類似“Eval”函數,每一個操作符都會生效。)在這種情況下,表達式自身占用資源很少,但虛函數調用的資源占用是很多的。
各種類型的大的代碼分支判斷,操作符,以及沒有被查詢引用的函數。分支預測器可以緩和這類問題,但同時分支指令會阻止流水線的效率以及指令集的并行性。
不能傳送所有的常量。Impala能計算一個固定寬度的元組格式(列3字節偏移值為16)。好處是這些常量不用重復寫入代碼,而不用在內存中去查找。
生成代碼的目標就是讓每個查詢都使用同樣數量的指令,就像定制代碼一樣,因為查詢執行支持廣泛的功能, 工具可以精確的匹配查詢,而并不需要額外的資源。
Impala的IR(Intermediate Representation)使用
在SQL語義分析階段后,我們為查詢的操作符生成獨立的“內核”代碼:這意味著代碼內部循環花費了大部分CPU周期。在代碼生成的時候,我們知道所有類型,包括元組的布局、SQL操作以及表達式都將用于這個查詢。其結果是非常緊密的內循環并與所有函數調用內聯,并且沒有外來的指令。
我們首先需要得到IR函數對象的代碼路徑。LLVM提供兩種機制生成IR。第一種是使用LLVM的IrBuilder (C++) API,通過它編程生成IR, 逐條按指令產生。第二種方式是是用Clang的編譯器將C++源碼轉換成IR。Impala同時使用這兩種方式。
簡單的說,關于執行代碼生成,我們:
通過IRBuilder生成IR,可以獲得更高效率的代碼以及附加的運行時間信息。
我們需要為函數讀取預編譯的IR,但不會從運行時間信息中獲取價值。
通過同時使用以上的1和2方法來置換調用的函數。這讓我們可以把本應該用虛函數實現的地方改用內聯來實現。
LLVM優化隨同一些我們定制的優化一同進行。這與將你的代碼進行優化編譯很相似,要考慮很多事情。除了可以有更少的代碼,這一步還可以幫助去掉子表達式、常量傳播、更多函數插入、指令重排序、無用代碼去除以及其它編譯優化技術。
運行時進行編譯執行優化將IR轉換到機器編碼。LLVM返回一個函數指針,用于替代請求引擎的解釋函數。 #p#
實例和結果
關于代碼生成話題,我們討論最多的問題是究竟可以提升多少速度?性能有多大變化?
這是一個運行 TPCH-Q1查詢的測試,該集群擁有10個數據節點,每個節點有10塊硬盤、48GB內存以及8核CPU(16個超線程)。查詢代碼如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
select l_returnflag, l_linestatus,sum(l_quantity), sum(l_extendedprice), sum(l_extendedprice * (1 - l_discount)), sum(l_extendedprice * (1 - l_discount) * (1 + l_tax)), avg(l_quantity), avg(l_extendedprice), avg(l_discount), count(1)fromtpch.lineitemwhere l_shipdate<='1998-09-02'group by l_returnflag, l_linestatus |
Impala以操作樹的形式來批處理數據元組,在這個例子里,有兩個操作符:一個是掃描并讀取磁盤上的數據,另一個是哈希聚合,其中包括了求和和求平均值。
我們將關注點放到聚合這步。對于哈希聚合,我們一批一批的對元組進行迭代,評估并哈希分組列(l_returnflags and l_linestatus),檢查哈希表,然后評估聚合表達式(求和、平均值以及select的元素個數)。對于聚合的操作符,代碼生成階段編譯所有的行組進入一個獨立的完整內聯循環的評估邏輯。
我們將在兩個不同大小的數據集上運行這個查詢,首先將在1TB的數據集上運行,接下來在100GB的數據集上運行。文件順序用 Snappy塊壓縮。對于100GB的數據集,它足夠小以適應集群的操作系統緩存。這可以防止可能出現的磁盤性能瓶頸。

對于這兩個數據集,代碼生成可以減少2/3的查詢時間。所有的生成代碼的時間大約150ms。(生成代碼可以通過設置查詢時的參數打開或關閉,所以你可以做同樣的測試。你可以通過在Impala的shell中輸入‘set’來查看查詢選項。) 為了更長遠的生成代碼所帶來的好處,我們可以對比更詳細的數值。在這里例子中,查詢運行在一臺服務器上,它的數據集小得多(只有700MB)。使用 perf stat工具,它通過概括精簡的方式提供被調試程序運行的整體情況和匯總數據。結果來自5次查詢后的匯總。

你能發現,沒有代碼生成的情況下,我們的指令運行了兩次,分支錯誤多過了兩倍。
結論
我們在代碼生成的投資已經獲得回報,同時也期望通過持續的升級查詢引擎獲得更好的新能提升。對于列式數據格式,將有更高效的編碼,以及更大的(內存)緩存,我們期待I/O性能戲劇性的提升,這導致CPU的性能越來越重要。
代碼生成對執行簡單表達式的查詢性能非常有幫助。例如,一個使用常用表達式的查詢對每一行的性能提升不會很明顯,因為解釋成本要少于正則表達式的運行時間。
目前的Impala 0.5版里仍然還有部分執行路徑沒有被轉化為本地代碼,我們還沒有時間來完成。大部分的代碼補丁將會集成在即將推出的GA發行版中。我們有很多方法,讓GA發行版的代碼生成發揮最大的優勢。































