每次都須要解釋大量指令?使用 PolarDB-X 向量化引擎

介紹

PolarDB-X是阿里巴巴自研的雲原生分佈式數據庫,採用了計算-存儲分離的架構,其中計算節點承擔着大量的表達式計算任務。這些表達式計算涉及到SQL執行的各個環節,對性能有着重要的影響。爲此PolarDB-X引入向量化執行引擎,爲表達式計算帶來了幾十倍的性能提高。數據庫

傳統數據庫執行器的缺陷數組

現代數據庫系統的執行引擎,大多采用一次計算一行數據(Tuple-at-a-time)的處理方式,而且須要在運行時對數據類型進行解析和判斷,來適應複雜的表達式結構。咱們稱之爲「標量(scalar)表達式」。這種方式雖然易於實現、結構清晰,可是當須要處理的數據量增大時,它具備顯著的缺陷:數據結構

爲了適應複雜的表達式結構,計算一條表達式每每須要引入大量的指令;對於行式執行來講,處理單條數據須要算子樹從新進行指令解釋(instruction interpretation),從而帶來了大量的指令解釋開銷。據論文《MonetDB/X100: Hyper-Pipelining Query Execution》統計,在MySQL執行TPC-H測試集的 Query1 時,指令解釋就耗費了90%的執行時間。架構

此外,在最初的Volcano結構設計中,算子內部邏輯並無避免分支預測(branch prediction)。錯誤的分支預測須要CPU終止當前的流水線,將ELSE語句中的指令從新載入,咱們將這一過程稱爲pipeline flush或pipeline break。頻繁的分支預測錯誤會嚴重影響數據庫的執行性能。分佈式

向量化執行系統函數

數據庫向量化執行系統最先由論文《MonetDB/X100: Hyper-Pipelining Query Execution》提出,它有如下幾個要點:oop

  1. 採用vector-at-a-time的執行模式,即以向量(vector)爲數據組織單位。
  2. 使用向量化原語(vectorization primitives)來做爲向量化算子的基本單位,從而構建整個向量化執行系統。原語中避免產生分支預測。
  3. 使用code generation(代碼生成)技術來解決靜態類型帶來的code explosion(代碼爆炸)問題。

向量化引擎爲PolarDB-X的表達式計算帶來了顯著的性能提高。在下圖中,橫軸爲向量大小,縱軸爲吞吐量,不一樣標量表達式和向量化表達式的性能測試對比結果以下:性能

case表達式性能測試對比結果以下:測試

總體流程

PolarDB-X中,向量化表達式的執行分爲如下幾個階段:fetch

  1. 用戶SQL經解析後,在validator中進行校驗,推導和修正表達式的類型信息;這一階段爲向量化運算提供正確的、靜態的類型信息;
  2. 在優化器造成執行計劃以後,須要對錶達式樹進行表達式綁定,實例化對應的向量化原語,同時分配好向量下標,供運行時內存分配;
  3. 執行階段,依據Volcano式的結構,自頂向下的觸發執行向量化原語,並將向量做爲運行時數據結構。

運行時結構

數據結構

在PolarDB-X向量化執行系統中,採用如下的數據結構來存放數據:

向量化表達式執行時,全部的數據都會存放在batch這一數據結構中。batch由許多向量(vector)和一個selection數組而組成。其中,向量vector包括一個存儲特定類型的數值列表(values)和一個標識null值位置的null數組組成,它們在內存中都是連續存儲的。null數組中的bit位以0和1來區分數值列表中的某個位置是否爲空值。

咱們能夠用vector(type, index)來標識batch中一個向量。每一個向量有其特定的下標位置(index),來表示向量在batch中的順序;類型信息(type)來指定向量的類型。在進行向量化表達式求值以前,咱們須要遍歷整個表達式樹,根據每一個表達式的操做數和返回值來分配好下標位置,最後根據下標位置統一爲向量分配內存。

延遲物化

selection數組的設計體現了延遲物化的思想,參考論文《Materialization Strategies in a Column-Oriented DBMS》。所謂延遲物化,就是儘量地將物化(matrialization)這一過程後推,減小內存訪問帶來的開銷。在執行表達式計算時,每每會先通過Filter表達式過濾一部分數據,再對過濾後的數據執行求值處理;每次過濾都會影響到batch中全部的向量。以上圖中的batch爲例,若是咱們針對第0個向量設置 vector(int, 0) != 1這一過濾條件,假設vector(int, 0)中有90%的數據知足該過濾條件(選擇率selectivity = 0.9),那麼咱們須要將batch中全部向量90%的數據從新物化到另外一塊內存中。而若是咱們只記錄知足該過濾條件的位置,存入selection數組,咱們就能夠避免這一物化過程。相應的,之後每次向量化求值過程當中,都須要參考此selection數組。

向量化原語

向量化原語是向量化執行系統中的執行單位,它最大程度限制了執行期間的自由度。原語不用關注上下文信息,也不用在運行時進行類型解析和函數調用,只須要關注傳入的向量便可。它是類型特定(Type-Specific)的,即一類原語只能處理特定類型。

向量化原語的主體是Tight-Loop的代碼結構。在一個循環體內部,只須要進行取值和運算便可,沒有任何的分支運算和函數調用。一個簡單的向量化原語結構以下所示:

map_plus_double_col_double_col(int n,
double*__restrict__ res,
double*__restrict__ vector1, double*__restrict__ vector2,
int*__restrict__ selection)
{
  if (selection) {
        for(int j=0;j<n; j++) {
            int i = selection[j];
            res[i] = vector1[i] + vector2[i];
        } 
  } else {
        for(int i=0;i<n; i++)
          res[i] = vector1[i] + vector2[i];
  }   
}

其運算過程利用了selection數組,逐步對向量進行取值、運算和存值,以下圖所示:

向量化原語帶來了如下優勢:

  1. Type-Specific以及Tight-Loop的結構,大大減小了指令解釋的開銷;
  2. 避免分支預測失敗和虛函數調用對CPU流水線的干擾,同時也能有利於 loop pipeline 優化【論文引用】
  3. 從向量中存取數據,有利於觸發cache prefetch,減小cache miss帶來的開銷。

咱們爲各類標量化表達式提供相應的原語實現,從而完成從標量到向量化的轉變。例如將加法運算 plus(Object, Object) 針對不一樣操做數類型生成原語,包括plus(double,double),plus(long, long)等。

短路求值

在向量化原語的基礎上,咱們能夠進一步對分支運算(也稱爲控制流運算 Control-Flow)進行短路求值(short-circuit calculation)優化,提高表達式計算的性能。

例如,case 表達式由n個when表達式、n-1個then表達式、1個else表達式構成。對於表達式

select case when a > 1 then a * 2
             when b > 1 then b * 2
            else a * b

其邏輯語義是:

  • 對於知足 a > 1 的向量位置,計算 a * 2;
  • 對於知足 a <= 1 and b > 1 的向量位置,計算 b * 2;
  • 對於知足 a <= 1 and b <= 1 的向量位置,計算 a * b;
  • 把全部位置的數值組合在一塊兒造成新的向量,輸出。

具備如下樹形結構:

因爲標量化表達式按照volcano結構編排,並提供了統一的next()的接口,case表達式必須執行完全部的子表達式a>1,a*2,b>1,b*2和a*b以後,將所有結果彙總到一塊兒,最後作case語義處理。這種執行方式不能根據when表達式的處理結果及時終止計算過程,而是對所有子表達式無差異執行。

引入向量化執行器之後,咱們能夠設計短路求值來優化此問題,每個子表達式須要被提供合適的selection數組,從而正確選擇列中合適的位置來進行向量運算。設第i個when條件表達式接受的selection元素集合爲

,其輸出的selection元素集合爲

,也就是第i個then條件表達式接受的selection元素集合。那麼知足

,其中

是原始的selection數組中的下標集合。咱們把求取selection元素集合的步驟稱爲substract selection,case運算的整個過程以下圖所示:

總結

PolarDB-X向量化引擎利用原語(primitive)來構建表達式,以向量做爲運行時數據結構。每種原語僅爲特定類型進行服務,從而減小了指令總數;原語中的tight-loop結構不只對CPU流水線十分友好,也容許CPU進行數據預取,而且避免分支預測。此外,一些優化如延遲物化、短路求值,進一步提高了表達式求值性能。

然而,從用戶SQL到向量化執行之間,存在着一道巨大的鴻溝。咱們須要解決如下幾個重要問題:

1. 如何肯定表達式的輸入輸出類型,併爲SQL中的表達式分配合適的原語?
2. 每一個原語須要使用不一樣的向量來進行輸入和輸出,如何爲正確地爲原語分配向量?
3. 每種原語僅爲特定類型進行服務,那麼咱們必然須要爲一個表達式配備大量不一樣的原語,來適應不一樣的數據類型。如何應對原語數量爆炸這一問題?

做者:君啓

原文連接

本文爲阿里雲原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索