日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

活动回顾|Apache Doris 向量化技术实现与后续规划

發(fā)布時(shí)間:2024/3/13 编程问答 49 豆豆
生活随笔 收集整理的這篇文章主要介紹了 活动回顾|Apache Doris 向量化技术实现与后续规划 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.




數(shù)倉(cāng)/? OLAP 分析是大數(shù)據(jù)領(lǐng)域的一個(gè)基本課題,近幾年隨著實(shí)時(shí)性訴求越來越強(qiáng)烈,如何對(duì)性能進(jìn)行提升變得更加重要,涌現(xiàn)出了非常多的技術(shù),從各維度進(jìn)行創(chuàng)新。 在 12 月 19 日?DataFunCon 大會(huì)的 極速 OLAP 論壇 上,來自百度的 Apache Doris Committer、數(shù)據(jù)庫(kù)內(nèi)核研發(fā)工程師 李昊鵬 為大家?guī)砹祟}為 「 Apache Doris 向量化技術(shù)實(shí)現(xiàn)與后續(xù)規(guī)劃 」 的技術(shù)分享,以下是分享內(nèi)容。


引言


今天和大家分享的內(nèi)容是 Apache Doris 的向量化技術(shù)實(shí)現(xiàn)與后續(xù)規(guī)劃。我是來自百度的數(shù)據(jù)庫(kù)內(nèi)核研發(fā)工程師李昊鵬,也是 Apache Doris 社區(qū)的 Committer ,一直在從事 Apache Doris 執(zhí)行引擎的開發(fā)工作。 今天的分享主要分為三個(gè)部分展開:第一部分首先介紹 Apache ?Doris 向量化的設(shè)計(jì)與實(shí)現(xiàn),第二部分是 Apache Doris 目前向量化版本的開發(fā)情況,第三部分介紹 Apache Doris 向量化的未來規(guī)劃以及下一個(gè)版本中即將發(fā)布的內(nèi)容。


Doris 向量化設(shè)計(jì)與實(shí)現(xiàn)


01????什么是向量化

向量化是指計(jì)算從一次對(duì)一個(gè)值進(jìn)行運(yùn)算轉(zhuǎn)換為一次對(duì)一組值進(jìn)行運(yùn)算的過程, 從不同的角度來分析,我將拆分成兩個(gè)方面來探討或思考這個(gè)問題。 從CPU的角度 現(xiàn)代 CPU 支持將單個(gè)指令應(yīng)用于多個(gè)數(shù)據(jù)(SIMD)的向量運(yùn)算。例如,具有 128 位寄存器的 CP U可以保存 4 個(gè) 32 位數(shù)并進(jìn)行一次計(jì)算,比一次執(zhí)行一條指令快 4 倍。


比如我們?cè)趦?nèi)存當(dāng)中有 4 個(gè) 32 位的 int ,進(jìn)行計(jì)算時(shí)傳統(tǒng)的 CPU 沒有 SIMD,或者說沒有向量化支持的 CPU 要進(jìn)行四次從內(nèi)存中 Load 數(shù)據(jù),再進(jìn)行 4 次乘法計(jì)算,然后把結(jié)果寫回到內(nèi)存當(dāng)中同樣要進(jìn)行 4 次。假如我們能夠支持 SIMD ,我們可以一次載入多個(gè)連續(xù)的內(nèi)存數(shù)據(jù),這樣我們就只有一次數(shù)據(jù)的 Load ,一次的計(jì)算,然后得到 4 個(gè)結(jié)果寫到 4 個(gè)寄存器里面,然后這 4 個(gè)寄存器再寫回到內(nèi)存當(dāng)中,就完成了一次向量化的指令計(jì)算操作。這樣的操作能夠比傳統(tǒng)的CPU快 4 倍。 隨著 CPU 的發(fā)展,現(xiàn)在大家常用的都是 128 位的 SSE 的指令,后面又多了 256 位的 AVX 指令,以及英特爾現(xiàn)在最新的 AVX512 的指令。隨著寄存器的位數(shù)不斷變長(zhǎng),我們一次向量化運(yùn)算的一組值可以變得越來越多,它的效率會(huì)越來越高。但這不一定是線性的關(guān)系,不一定隨著寄存機(jī)的位數(shù)呈線性的性能增長(zhǎng),但是能夠保證一次對(duì)一組值的操作是更多更快的。 這是在CPU角度,我們?nèi)タ聪蛄炕@個(gè)問題。

從數(shù)據(jù)庫(kù)的角度

從數(shù)據(jù)庫(kù)角度也是計(jì)算從一次對(duì)一個(gè)值進(jìn)行運(yùn)算轉(zhuǎn)換為一次對(duì)一組值進(jìn)行運(yùn)算的過程。
  • 數(shù)據(jù)庫(kù)執(zhí)行引擎:
    1. 將 Next Tuple ,變成 Next Batch 。
    2. 內(nèi)存中 Batch 的數(shù)據(jù)不是以行的形式存在,而是以列的形式存在,算子都是在列上進(jìn)行運(yùn)算。

在數(shù)據(jù)庫(kù)執(zhí)行引擎的角度與 CPU 的角度也是類似,傳統(tǒng)的數(shù)據(jù)庫(kù)執(zhí)行引擎都是一行一行處理數(shù)據(jù)的。我們可以看下面這個(gè)圖。


比如我們做一個(gè) Scan,做一個(gè)過濾條件,然后再做一個(gè)乘法,一次進(jìn)行一行,在這一行上做對(duì)應(yīng)的數(shù)據(jù)的判斷、計(jì)算。我們實(shí)現(xiàn)數(shù)據(jù)庫(kù)的向量化引擎后,把一次對(duì)一個(gè) Tuple 的操作轉(zhuǎn)化成一次對(duì)一組值的操作。不再是一次只處理一行,而是一次處理一組值。
所以內(nèi)存當(dāng)中的數(shù)據(jù)不再以行的形式來存在,而是以列的形式存在,所有的計(jì)算都通過列的方式進(jìn)行連續(xù)的計(jì)算。我們可以看右邊這張圖,比如我們有一個(gè)數(shù)據(jù)掃描的操作,原本按照左邊這張圖,一行做一個(gè)判斷對(duì)一組值要判斷 4 次,現(xiàn)在對(duì)一個(gè)列進(jìn)行數(shù)據(jù)的判斷。所以在數(shù)據(jù)庫(kù)的角度同樣也是對(duì)一個(gè)值的運(yùn)算轉(zhuǎn)化成對(duì)一組值的運(yùn)算。

02????Doris?如何實(shí)現(xiàn)向量化

接下來介紹 Apache Doris 如何實(shí)現(xiàn)向量化,這也是我們目前正在做的工作。實(shí)現(xiàn)向量化核心工作主要分為這三塊:
  • 列式存儲(chǔ): 在 Doris 的執(zhí)行引擎中引入基于列存的存儲(chǔ)格式。在執(zhí)行引擎當(dāng)中, Doris 現(xiàn)在存儲(chǔ)層的數(shù)據(jù)是以列存儲(chǔ)的,但是在執(zhí)行引擎當(dāng)中我們還是基于行的方式來做運(yùn)算的。我們前面看到那張圖是基于 Tuple 運(yùn)算的,每次只能運(yùn)算一個(gè)值,所以我們要把它替換為一個(gè)列式存儲(chǔ)的執(zhí)行引擎,這樣我們才能夠?qū)崿F(xiàn)向量化。

  • 向量化函數(shù)計(jì)算框架 基于列式存儲(chǔ)重新設(shè)計(jì)一套向量化/列式計(jì)算引擎。在列式存儲(chǔ)的基礎(chǔ)上,我們要實(shí)現(xiàn)一套向量化的函數(shù)計(jì)算框架。基于現(xiàn)有的新的列式存儲(chǔ)格式,重新設(shè)計(jì)一套向量化列式存儲(chǔ)的計(jì)算引擎。

  • 向量化算子 基于前兩者,重新組織SQL算子。基于列式存儲(chǔ)和向量化的函數(shù)計(jì)算框架,我們要實(shí)現(xiàn)所有的向量化算子,這一塊工程量其實(shí)是比較大的。

接下來詳細(xì)介紹我們做了哪些工作。

1.列式存儲(chǔ)

改變了計(jì)算引擎對(duì)數(shù)據(jù)的組織方式,由行存的 Tuple 與 RowBatch 變?yōu)榱?Column 與 Block
原先 Doris 在執(zhí)行引擎當(dāng)中的數(shù)據(jù)內(nèi)存結(jié)構(gòu)是左邊的 RowBatch 結(jié)構(gòu),數(shù)據(jù)通過一個(gè)一個(gè) Tuple 進(jìn)行組織的,每一個(gè) Tuple 是個(gè)連續(xù)的內(nèi)存。我們可以看到左邊 RowBatch 的結(jié)構(gòu)分為三個(gè)列,但它每一個(gè)行是一個(gè)連續(xù)的內(nèi)存結(jié)構(gòu),在組織內(nèi)部處理當(dāng)中也是一行一行處理的。 右邊是我們新的結(jié)構(gòu) Block ,我們可以看到它的數(shù)據(jù)是按列來進(jìn)行組織的,每一個(gè)列是一個(gè)連續(xù)的內(nèi)存結(jié)構(gòu)。 2.向量化的函數(shù)計(jì)算框架 這里我簡(jiǎn)單舉一個(gè)例子
select a,abs(b) from test;


我們可以看到現(xiàn)在的 ?Block ?里面原先只有 a,b 兩列連續(xù)的內(nèi)存是按列組織的,abs() 是一個(gè)函數(shù)計(jì)算。向量化的函數(shù)計(jì)算流程是:首先我們有個(gè)原始的 Block ,輸入進(jìn)來分為 a,b 兩列,接下來我們對(duì) b 列做 abs 的函數(shù)計(jì)算,我們可以看到在原來行式存儲(chǔ)的邏輯上,如果我們要進(jìn)行 abs 計(jì)算的話,雖然計(jì)算跟 a 列這部分內(nèi)存沒有關(guān)系,但因?yàn)樵瓉硎腔谛械倪B續(xù)內(nèi)存,所以 a 列也會(huì)參與進(jìn)來。 而在向量化函數(shù)執(zhí)行框架當(dāng)中,a 列的內(nèi)存不再參與進(jìn)來了,在內(nèi)存結(jié)構(gòu)上和 b 列已經(jīng)獨(dú)立開了,所以我們可以看到在 b 列經(jīng)過計(jì)算之后生成一個(gè) abs(b) 列,而 a 列在整個(gè)計(jì)算過程當(dāng)中都沒有內(nèi)存上的交互。我們看右邊這張圖,我們做完 abs 計(jì)算之后,在原有的 Block 上新生成一個(gè)連續(xù)的內(nèi)存結(jié)構(gòu),新生成了一個(gè) abs(b) 列,最后我們?cè)侔?b 列過濾掉,最終的 ?Block 就留下 a 列跟 abs(b) 列,這兩個(gè)列就完成了一次列式存儲(chǔ)的向量化計(jì)算。 3. 向量化函數(shù)計(jì)算對(duì)比 剛才是通過一張圖和大家解釋了向量化的函數(shù)計(jì)算,這里我列了幾行代碼, 從代碼上我們更能看到為什么向量化函數(shù)計(jì)算跟原先的行存執(zhí)行邏輯相比能夠提高性能。 // 行存執(zhí)行邏輯for(int i = 0;i < rows; ++i) { Tuple Tuple = batch->get_row(i); for(int j = 0;j < desc.slots().size();++j) { void *data = Tuple.get_data(desc.slots(j).offset); // 獲取某一行的數(shù)據(jù) switch(desc.slots(j)) { // 通過desc中的描述類型來解釋執(zhí)行 case TYPE_INT: hash_int((int*)data); case TYPE_STRING: hash_string((StringRef*)data); // 列存執(zhí)行邏輯for i in range(types.size())switch(types[i]) {TYPE_INT:for(j = 0; j < rows < ++j) {hash_int((*(int*)column->data() + j));}TYPE_STRING: xxx; 較上方的這段代碼是原來行存執(zhí)行的邏輯,可以看到一個(gè) Batch 輸入進(jìn)來之后,我們要遍歷所有 Row ,每次獲取一行數(shù)據(jù),做完偏移之后才能拿到對(duì)應(yīng)的列,而基于這個(gè)列,每一次還要做類型的判斷,然后再確定調(diào)用什么函數(shù)進(jìn)行處理。
但是我們可以看下邊列存的執(zhí)行邏輯,相對(duì)行存的執(zhí)行邏輯更簡(jiǎn)潔。首先在整個(gè) ?Block 的執(zhí)行過程中只有一次類型判斷,我們可以看到假如 RawBatch 或者 ?Block 有 1024 行,原行存執(zhí)行結(jié)構(gòu)對(duì)于類型的判斷要走 1024 次,但對(duì)于列存來說只要一次就可以了,減少了大量類型判斷。另外,從代碼上可能不容易看出來其實(shí)每一次處理時(shí)列存是連續(xù)的,我們對(duì)連續(xù)的一段內(nèi)存做處理, Cache 親和度更高。 03????向量化計(jì)算優(yōu)點(diǎn) 相對(duì)于舊的行存計(jì)算框架,列式的函數(shù)計(jì)算框架有以下優(yōu)勢(shì):
  • Cache 更親和,列式計(jì)算由于數(shù)據(jù)是按列組織的,所以更容易命中 Cache 比如剛才的例子中,運(yùn)算 abs(b)?時(shí)沒有相關(guān)的列是不會(huì)參與到計(jì)算過程當(dāng)中來的。列式計(jì)算由于數(shù)據(jù)在列上連續(xù),所以更容易命中我們需要計(jì)算的 Cache ,從 Cache 中讀取數(shù)據(jù)和內(nèi)存中讀取數(shù)據(jù),性能有 10 倍至 100 倍的差距。

  • 減少虛函數(shù)的調(diào)用,減少分支跳轉(zhuǎn),降低 CPU 分支預(yù)測(cè)判斷失敗概率。 從兩段代碼能明顯看到減少很多類型的判斷,在向量化框架當(dāng)中大量運(yùn)用模板的方式做零成本的抽象減少虛函數(shù)調(diào)用。關(guān)于虛函數(shù)調(diào)用會(huì)有什么開銷、引起什么問題,我們?cè)诤竺鏁?huì)再詳細(xì)討論。

  • 函數(shù)計(jì)算的 SIMD ,包含編譯器自動(dòng) SIMD 與手動(dòng) Coding 的 SIMD指令集。 這一部分對(duì)應(yīng)一開始單獨(dú)提到的「從CPU角度去看向量化」,原先一次只能算一個(gè)值,現(xiàn)在向量化計(jì)算框架中一次可以計(jì)算多個(gè)值,這樣就會(huì)有數(shù)倍的性能提升。
這里給大家推薦一篇論文 《 DBMSs?On A Modern Processor:Where Does Time Go?》 ,這篇論文分享了在現(xiàn)代處理器上,數(shù)據(jù)庫(kù)花費(fèi)了大量時(shí)間在什么樣的地方。論文總結(jié)出來這三點(diǎn):Memory stalls, Branch mispredictions,以及 Resource stalls. 真正在查詢當(dāng)中做計(jì)算時(shí)間可能只有 20% 至 30% 甚至不到,所以我們要去解決這些問題。而向量化框架就是去解決這些問題的。 接下來再展開來談它為什么更快。 1. Cache 親和度, Cache?的開銷與代價(jià) 1.1?指令 Cache /數(shù)據(jù) Cache / TLB 的 Cache 我們?cè)?CPU 的角度來看 Cache 分為這三部分。TLB 的 Cache 大家可能關(guān)注較少,一個(gè)虛擬地址轉(zhuǎn)化成物理地址在 CPU 的內(nèi)部會(huì)有一個(gè) TLB 表,這其實(shí)也是 Cache 。我們真正理解上的 Cache ,或者說是真正能夠感知到的 Cache 就這三部分,這三部分如果處理不好,會(huì)對(duì)整個(gè)查詢有很大的性能影響。 1.2?L1/L2/L3/DDR -> 1/5/20/100 ns
這邊我列了下面這個(gè)表格。


這是英特爾官方列出來的一個(gè)表格,L1 ,L2 ,L3 Cache 到實(shí)際的 DDR 當(dāng)中訪問的時(shí)間開銷,可以看到 L1 只有 1 納秒;L2 有 4 至 5 納秒;L3 有 10 至 20 納秒,與 L2 是 10 倍的差距。我們實(shí)際訪問內(nèi)存可能要 100 納秒以上,可以想象如果我們一個(gè)數(shù)據(jù)能夠在 L1 處緩存的話,能夠節(jié)省的 CPU 時(shí)鐘周期是很可觀的。 1.3 內(nèi)存帶寬 訪問 DDR 當(dāng)中的數(shù)據(jù)還涉及到內(nèi)存帶寬。內(nèi)存帶寬不是單個(gè)程序可控的,整體的硬件邏輯上所有程序可能都要訪問內(nèi)存,所以還涉及到內(nèi)存帶寬調(diào)度的問題。
2.虛函數(shù)調(diào)用的開銷。 虛函數(shù)帶來的開銷主要是下面兩點(diǎn): 2.1 ?虛函數(shù)的動(dòng)態(tài)調(diào)用是無(wú)法進(jìn)行函數(shù)內(nèi)聯(lián)的,這會(huì)大大減少編譯器可能進(jìn)行的優(yōu)化空間。 虛函數(shù)實(shí)際調(diào)用時(shí)需要查表,編譯器無(wú)法知道當(dāng)時(shí)動(dòng)態(tài)狀態(tài)下需要調(diào)用什么樣的函數(shù) ,所以沒有辦法進(jìn)行內(nèi)聯(lián)。我們知道函數(shù)內(nèi)聯(lián)真正意義就是給編譯器更多的優(yōu)化空間,這與數(shù)據(jù)庫(kù)當(dāng)中的優(yōu)化器是類似的,給更多的信息才能做更好的優(yōu)化。編譯器的角度也是同樣的道理,但因?yàn)樘摵瘮?shù)沒有辦法進(jìn)行函數(shù)內(nèi)聯(lián),所以編譯器獲取的信息就會(huì)大大減少,這樣編譯器代碼優(yōu)化空間就會(huì)減少。 2.2 ?虛函數(shù)查表帶來額外的分支跳轉(zhuǎn)的開銷,分支預(yù)測(cè)失敗會(huì)導(dǎo)致CPU流水線重新執(zhí)行。
分支跳轉(zhuǎn)在 CPU 當(dāng)中是有一定開銷的。下面這張圖是 Pipeline 的執(zhí)行流程,分為 Fetch , Decode , Executed , Write-back,這四個(gè) Stage 。現(xiàn)在的 CPU 還要比這個(gè)復(fù)雜很多。


一旦 CPU 進(jìn)入一個(gè)跳轉(zhuǎn)流程當(dāng)中,原則上來講 Pipeline 是要暫停下來的,因?yàn)樘D(zhuǎn)指令是依賴前一個(gè)指令執(zhí)行結(jié)果,完成后我們才能確定要執(zhí)行什么指令。但是現(xiàn)在 CPU 進(jìn)行分支預(yù)測(cè)不會(huì)讓流水線空轉(zhuǎn)。分支預(yù)測(cè)一旦成功的話,已經(jīng)順著流水線執(zhí)行下去的指令能夠得到很好的收益,但一旦分支預(yù)測(cè)失敗會(huì)導(dǎo)致 CPU 流水線重新執(zhí)行,這也會(huì)帶來極大的性能開銷 。 對(duì)于分支預(yù)測(cè),給大家舉個(gè)小例子,大家可以實(shí)際去用代碼去實(shí)踐一下。 假如我有個(gè) vector 存儲(chǔ)的指針對(duì)象有虛函數(shù)調(diào)用,我們是把不同的對(duì)象交替地放在這個(gè)
vector 當(dāng)中,還是把這個(gè) vector 基于對(duì)象類型做一次排序之后調(diào)用,哪個(gè)執(zhí)行效率更高,大家可以實(shí)際寫代碼去驗(yàn)證一下。有意思的是他們實(shí)現(xiàn)的匯編都是一模一樣,但是后者更快,這就是分支預(yù)測(cè)失敗帶來的一個(gè)影響。

3.函數(shù)執(zhí)行如何 SIMD 3.1 Auto?vectorized 自動(dòng)向量化,也就是編譯器自動(dòng)去分析 for 循環(huán)是否能夠向量化。GCC 開啟的 -O3 優(yōu)化便會(huì)開啟自動(dòng)向量化。 自動(dòng)向量化 tips : (1)足夠簡(jiǎn)單的 for 循環(huán)。
(2)足夠簡(jiǎn)單的代碼,避免:函數(shù)調(diào)用,分支跳動(dòng)。
(3)規(guī)避數(shù)據(jù)依賴,即避免下一個(gè)計(jì)算結(jié)果依賴上一個(gè)循環(huán)的計(jì)算結(jié)果。
(4)連續(xù)的內(nèi)存和對(duì)齊的內(nèi)存。SIMD 指令本身對(duì)于內(nèi)存要求是比較多的。首先前面我們講到了 CPU 是連續(xù)載入多個(gè)數(shù)據(jù)的,數(shù)據(jù)在不連續(xù)的內(nèi)存上沒有辦法一次做連續(xù)的載入。第二是內(nèi)存對(duì)齊會(huì)影響向量化指令的執(zhí)行效率。GCC 的文檔上給了一個(gè)比較完整的 case ,講到如何寫代碼編譯器能夠做自動(dòng)的向量化,大家有興趣的話可以參考 GCC 的文檔 CASE:
https://gcc.gnu.org/projects/tree-ssa/vectorization.html


如何確認(rèn)代碼編譯器的自動(dòng)向量化生效了呢? (1)編譯器的 Hint 提示。 -fopt-info-vec-all:打印所有編譯器進(jìn)行向量化的信息,如果循環(huán)代碼被向量化了,會(huì)打印如下信息 main.cpp:5: note: LOOP VECTORIZED. -fopt-info-vec-missed:沒有被向量化的原因 -fdump-tree-vect-all:進(jìn)一步分析沒有被向量化的原因 大家可以把 Hint ?提示打開,打開后就能在編譯的過程中看到編譯器的提示,如果循環(huán)代碼被向量化了,編譯器會(huì)打印對(duì)應(yīng)的信息。LOOP VECTORIZED 告訴你這個(gè)循環(huán)被向量化。如果是沒有被向量化的話可以打印 ?vec-missed 的 Hint ,它會(huì)告訴你為什么函數(shù)沒有被向量化,但通常只會(huì)提供一個(gè)簡(jiǎn)單的原因。如果要更深入分析的話,可以用最下面我給大家的 Hint 進(jìn)一步分析為什么沒有被向量化。大家可以實(shí)際寫代碼去實(shí)踐一下。 (2)直接通過 perf/objdump 查看生成的匯編代碼。 在一個(gè)大型程序上一個(gè)一個(gè)看 for 循環(huán)的話比較困難,我們可以把編譯出來的產(chǎn)出通過 perf 或 objdump 直接查看生成的匯編代碼,就可以看到有沒有向量化了。我這邊截了一張圖:


向量化指令是以 v 開頭的,大家看到 v 開頭的話大概率就是向量化了。然后可以看到使用 xmm ?寄存器。前面講到了 X86 的 128 位的寄存器是 xmm,256 位是 ymm , ?512 位是zmm。看到這幾個(gè)寄存器就可以確認(rèn)被向量化了。 3.2 手寫 SIMD SIMD 本身通過庫(kù)的方式進(jìn)行了支持,可以直接通過向量化 API 庫(kù)進(jìn)行向量化的編程,這種實(shí)現(xiàn)方式是最為高效的。但是需要程序員熟悉 SIMD 的編碼方式,在不同的 CPU 架構(gòu)之間并不通用。比如實(shí)現(xiàn)的 AVX 的向量化算法并不能在不支持 AVX 指令集的機(jī)器上運(yùn)行,也無(wú)法用 SSE 指令集代替。




Doris?向量化的當(dāng)前情況


01????SQL 算子

在 0.15 版本我們發(fā)布了一個(gè)單表的向量化執(zhí)行引擎,能夠滿足大寬表的向量化查詢需求,實(shí)現(xiàn)向量化的 SQL 算子包括 Sort,Agg,Scan,Union。當(dāng)前 Doris 的數(shù)據(jù)存儲(chǔ)是基于列式存儲(chǔ),在數(shù)據(jù)進(jìn)來之后的一些計(jì)算過程是基于行存來進(jìn)行的,在 ScanNode 上轉(zhuǎn)成列式存儲(chǔ),基于列式存儲(chǔ)實(shí)現(xiàn)了這4種向量化的算子執(zhí)行向量化的計(jì)算。

02????如何開啟向量化

1.設(shè)置環(huán)境變量?set enable_vectorized_engine = true;(必須) 我們可以看到這張圖,設(shè)置后的執(zhí)行計(jì)劃會(huì)帶一個(gè)v,這其實(shí)跟匯編指令生成 ?SIMD 指令集是相似的,代表開啟向量化。

2.設(shè)置環(huán)境變量set batch_size = 4096; (推薦)

我們?cè)趯?shí)際的測(cè)試當(dāng)中測(cè)試下來,?推薦設(shè)置 batch_size 為 4096 ,或者說在向量化當(dāng)中應(yīng)該講? Block 實(shí)測(cè)下來設(shè)置為 4096 行性能是最好的。當(dāng)然這里僅作推薦,不調(diào)整的話性能也還是不錯(cuò)的。

03????向量化的單表性能

這是 0.15 版本我們?cè)趩伪砩献龅囊恍y(cè)試,與原來行存做了一個(gè)對(duì)比。 Q1:select count(*) FROM lineorder_flat; Q2:select min(lo_revenue) as min_revenue from lineorder_flat group by c_nation; Q3:select count(distinct lo_commitdate), count(distinct lo_discount) from lineorder_flat;
可以看到性能提升的效果還是比較可觀的。這是在 0.15 版本我們實(shí)測(cè)的向量化的性能,后續(xù)的版本性能會(huì)更激動(dòng)人心,值得大家期待!


Doris?向量化的未來規(guī)劃

接下來是向量化的未來規(guī)劃,包括功能的完善與發(fā)布,以及關(guān)于后續(xù)版本的想法,這部分我想也是大家最關(guān)心的。

01????SQL 算子

Join 算子是 Doris 最為核心的算子,絕大多數(shù)場(chǎng)景使用 Doris 其實(shí)也是看中 Doris 本身在 MPP 場(chǎng)景下的多表? Join 能力,所以 Join 開發(fā)也是我們向量化開發(fā)當(dāng)中的重中之重。


可以告訴大家我們已經(jīng)實(shí)現(xiàn)了 Join 的向量化,只是還沒有做系統(tǒng)的調(diào)優(yōu)。這是我們基于 SSB測(cè)試集的 Join 向量化性能情況,這是第一版大家可以簡(jiǎn)單看一下,在 SSB 測(cè)試集當(dāng)中 Join 性能基本有 30% 至 40% 甚至 1 倍的提升。 剩下一些 SQL 算子,除了 Join 之外大家可能還會(huì)用到的:交集,差集, cross Join,ODBC/MySQL Scan Node,窗口函數(shù)(WIP),ES Scan Node(WIP)?,這些算子其實(shí)都是當(dāng)前 Doris 行存支持的SQL算子,除了部分算子還在開發(fā)中,基本都已經(jīng)全部 Ready,現(xiàn)在還在做一些穩(wěn)定性和性能調(diào)優(yōu)工作。

02? ? 存儲(chǔ)層向量化

前面講到目前 Doris 的數(shù)據(jù)是基于列存儲(chǔ)在磁盤上。但我們讀下來之后要對(duì)這個(gè)數(shù)據(jù)做聚合,這個(gè)過程是通過行存來表示的。這部分因?yàn)闅v史原因存在了很多冗余低效的代碼,目前我們?cè)趯?shí)際的向量化開發(fā)中發(fā)現(xiàn)已經(jīng)嚴(yán)重影響了整個(gè)向量化代碼的執(zhí)行邏輯與性能。主要是如下幾個(gè)問題: 1.計(jì)算表達(dá)式受限制。 很多計(jì)算表達(dá)式因?yàn)閮?nèi)存結(jié)構(gòu)不同、接口不同沒有辦法作用于存儲(chǔ)層,導(dǎo)致表達(dá)式?jīng)]有辦法下推,以及無(wú)法做向量化的計(jì)算。 2.額外的轉(zhuǎn)換的性能開銷比較嚴(yán)重, 嚴(yán)重影響到導(dǎo)入和查詢性能。我們實(shí)際做了一個(gè) POC 的測(cè)試,在 DupKey 表上通過完整的向量化改造性能還能有10倍級(jí)別的提升。 3.代碼可維護(hù)性降低。 因?yàn)榻Y(jié)構(gòu)不同,所以目前Doris 在執(zhí)行引擎實(shí)現(xiàn)的聚合算子沒有辦法作用于存儲(chǔ)引擎。同樣的聚合代碼要在存儲(chǔ)層和查詢層,也就是存儲(chǔ)引擎和查詢引擎分別實(shí)現(xiàn),代碼可維護(hù)性就很差。另外在查詢層進(jìn)行性能優(yōu)化沒有辦法作用在存儲(chǔ)層下,性能優(yōu)化需要考慮兩部分,所以也帶來了一些問題。我們目前在做的一個(gè)很重要的工作就是把 Doris存儲(chǔ)層通過行存做去重聚合的邏輯進(jìn)行向量化。


這是我們做的一個(gè) POC ,進(jìn)行存儲(chǔ)層向量化之后,可以看到相對(duì)于原先向量化的版本,能再有一倍的提升。這里我也列了幾個(gè)比較典型的聚合查詢,在我們做完存儲(chǔ)層向量化改造之后,與原先的向量化版本做了一個(gè)對(duì)比,還能有兩倍的性能差距。

03? ? 導(dǎo)入向量化

?前 Doris 進(jìn)行數(shù)據(jù)導(dǎo)入時(shí)存在 Tuple 轉(zhuǎn) RowCursor ,數(shù)據(jù)行式聚合等一系列有冗余開銷的工作。而我們后續(xù)希望通過導(dǎo)入向量化的開發(fā)實(shí)現(xiàn):

  • 減少額外內(nèi)存格式的轉(zhuǎn)換開銷,提高 CPU 的利用率
  • 復(fù)用計(jì)算層的算子,減少冗余低效的代碼,簡(jiǎn)潔 Doris 現(xiàn)有的代碼結(jié)構(gòu)
  • 利用 SIMD 加快導(dǎo)入過程當(dāng)中的聚合計(jì)算,進(jìn)一步提升 Doris 導(dǎo)入性能

目前向量化版本實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的導(dǎo)入框架。Block 計(jì)算完成之后是以列組織的,通過向量化的計(jì)算引擎做格式的轉(zhuǎn)化,但最終在發(fā)送數(shù)據(jù)時(shí),由于接收端還沒有做向量化改造,所以我們現(xiàn)在還是基于 RowBatch 的數(shù)據(jù)格式進(jìn)行數(shù)據(jù)發(fā)送。下一步我們要規(guī)劃的工作就是把通過 Tuple 轉(zhuǎn) Rowcursor 進(jìn)行運(yùn)算這部分全部剔除掉,轉(zhuǎn)換成向量化的格式或者說列存的格式進(jìn)行運(yùn)算。

04? ??SQL函數(shù)

1.函數(shù)豐富度支持 前面講到了目前 Doris 已有的函數(shù)是比較豐富的,但是因?yàn)橄蛄炕腔诹写鎸?shí)現(xiàn)的,函數(shù)接口包括調(diào)用的方式都有所不同,所以我們不單單要實(shí)現(xiàn) SQL 算子,還要實(shí)現(xiàn)浩如煙海的 SQL 函數(shù)。目前我們已經(jīng)實(shí)現(xiàn)了向量化支持的函數(shù)大概有 200 多個(gè),其他還在不斷的開發(fā)過程當(dāng)中,需要盡快補(bǔ)齊原先行存支持但是向量化還沒有支持的函數(shù)。這部分工作也歡迎大家參與進(jìn)來。

2.函數(shù)的 SIMD 化

我們保證在列存上能夠利用前面提到的幾個(gè)優(yōu)勢(shì)使分支跳轉(zhuǎn)變少,Cache 親和度更高。但目前很多實(shí)現(xiàn)向量化的函數(shù)沒有考慮 SIMD 化。尤其對(duì)于熱點(diǎn)的核心函數(shù),我們后續(xù)規(guī)劃盡量通過各種可能的方式進(jìn)行 SIMD 化,包括我們前面提到手寫 SIMD 。我自己在開發(fā)過程當(dāng)中手寫了一些 SIMD 的函數(shù)。目前在 Apache 的 Master 庫(kù)也能看到我自己手寫的一些在字符串實(shí)現(xiàn)的SIMD 的函數(shù)。

3.向量化的 UDF 的框架設(shè)計(jì)和實(shí)現(xiàn)。

UDF 也是大家經(jīng)常用到一個(gè)功能,向量化上需要重新設(shè)計(jì)一個(gè) UDF 框架,這也是我們后續(xù)要做的。


05? ??代碼重構(gòu)

1.基礎(chǔ)類型的重構(gòu) 。目前 Doris 向量化上所有的數(shù)據(jù)類型都能夠支持,但是有這幾個(gè)問題:
  • Data / Datatime:更小的內(nèi)存占用,對(duì) SIMD 更友好的內(nèi)存布局。 Doris 現(xiàn)在的 Data 和 DataTime 類型復(fù)用了行存,毫秒的數(shù)據(jù)其實(shí)在現(xiàn)在的 Doris 當(dāng)中沒有在用了。 現(xiàn)在 Data 和 DataTime 占用 16 字節(jié),而毫秒就占了 8 字節(jié),這八字節(jié)完全沒有用到,額外占用了很大的內(nèi)存。 這部分內(nèi)存占用在原來函數(shù)當(dāng)中還不明顯,但在向量化當(dāng)中8個(gè)字節(jié)的占用就會(huì)帶來很大的內(nèi)存開銷,對(duì) SIMD 不友好。 另外目前 Data / DataTime 內(nèi)存布局還有待優(yōu)化,在做 SIMD 時(shí)會(huì)有各種各樣的問題。 所以我們準(zhǔn)備重構(gòu) Data / DataTime,實(shí)現(xiàn)在向量化版本有更小的內(nèi)存占用,對(duì) SIMD 更友好的內(nèi)存布局。

  • Decimal:根據(jù)精度切換內(nèi)存占用,解決 Doris 中當(dāng)前精度浪費(fèi)的問題。 當(dāng)前的Doris在精度上沒有根據(jù)實(shí)際用戶設(shè)置的精度切換內(nèi)存占用,統(tǒng)一占用 16 字節(jié)的內(nèi)存,造成很大一部分開銷。 我們準(zhǔn)備對(duì) Decimal 的這一問題進(jìn)行重構(gòu)。

  • HLL:減少 HLL 類型無(wú)效的序列化的開銷。 前我們已經(jīng)把 Bitmap 重構(gòu)完成,用了一個(gè)新的結(jié)構(gòu)表示 Bitmap ,減少了大量的序列化開銷,整體性能表現(xiàn)也很不錯(cuò)。 減少 HLL 在計(jì)算時(shí)無(wú)效的序列化開銷就是下個(gè)階段我們要做的事情。
2. String / Array 更豐富的類型支持。 Doris 目前行存的版本能夠執(zhí)行 String 類型,向量化對(duì) String 類型還需重新設(shè)計(jì)一套更好的格式。同時(shí)還需要對(duì) Array 類型進(jìn)行支持。 3.聚合表類型的再梳理,更合理的設(shè)計(jì)聚合列的類型和狀態(tài)。 目前 Doris 聚合表上有表意不明的問題,需要重新審視一下 Doris 目前聚合表的結(jié)果,設(shè)計(jì)更合理的聚合列的狀態(tài)和類型。 4.結(jié)合 Doris 團(tuán)隊(duì)在打磨的 CBO 優(yōu)化器,進(jìn)一步提升向量化執(zhí)行引擎的性能。 就像前面所提到的函數(shù)內(nèi)聯(lián)的問題,如果優(yōu)化器能夠提供更多的信息,那么在向量化執(zhí)行引擎上就能進(jìn)行更多更深層次的優(yōu)化,來進(jìn)一步提高目前向量化執(zhí)行引擎的性能。


寫在最后


01? ? 感謝 Clickhouse 社區(qū) Doris 在進(jìn)行向量化代碼開發(fā)時(shí),在列存模型和函數(shù)框架上引用了部分 Clickhouse 19.16.2.2 版本的代碼,我們?cè)谝么a的 License 上注明了相應(yīng)的工作,同時(shí)與 Clickhouse 社區(qū)進(jìn)行了對(duì)應(yīng)的溝通。十分感謝 Clickhouse 在 Doris 向量化代碼開發(fā)上給予的幫助。
02?? ?版本發(fā)布 如果順利的話 1 月底或 2 月初我們就會(huì)帶來下一版的向量化執(zhí)行引擎了,前面所提到的很多后續(xù)規(guī)劃內(nèi)容將在下一版本中落地。希望大家屆時(shí)多多試用捧場(chǎng)~


—— End ——




歡迎關(guān)注:

Apache Doris(incubating)官方公眾號(hào)



【精彩文章】 凡是過往,皆為序章|Apache Doris 社區(qū) 2021 年終回顧
社區(qū)人物志|魏祚:道阻且長(zhǎng),行則將至,做有溫度的開源項(xiàng)目
從NoSQL到Lakehouse,Apache Doris的13年技術(shù)演進(jìn)之路



相關(guān)鏈接:

Apache Doris官方網(wǎng)站:

http://doris.incubator.apache.org

Apache?Doris?Githu b:

https://github.com/apache/incubator-doris

Apache Doris 開發(fā)者郵件組:

dev@doris.apache.org








本文分享自微信公眾號(hào) - ApacheDoris(gh_80d448709a68)。
如有侵權(quán),請(qǐng)聯(lián)系 support@oschina.cn 刪除。
本文參與“ OSC源創(chuàng)計(jì)劃 ”,歡迎正在閱讀的你也加入,一起分享。

總結(jié)

以上是生活随笔為你收集整理的活动回顾|Apache Doris 向量化技术实现与后续规划的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。