基於Renascence架構的sqlite查詢優化
sqlite查詢優化方案是一開始是在Hw時設計的,但當時只實現一些簡單case,並未完成sql的普遍支持。後面考慮到這可以做爲 Renascence 架構的一個實驗場景,因此將其方案做了一番修改,代碼也重寫了一遍,現在做成一個能支持普通sql查詢的demo。
sqlite架構
參考:http://wiki.dzsc.com/info/7440.html
sqlite是移動設備廣泛使用的輕量級數據庫引擎。它主要由前端——虛擬機——後端三部分組成,這裏不做詳細介紹。
VM機制
參考:
http://www.cnblogs.com/hustcat/archive/2010/06/23/1762987.html
對於任何一條sql語句,sqlite都是先編譯爲一串虛擬機碼(執行計劃),然後基於虛擬機碼執行程序:
如:
select * from TEST where age >20 and salary<99999666;
被翻譯爲如下的虛擬機碼:
addr:0 opcode:Init p1:0 p2:16 p3:0 p4: p5:00 comment:
addr:1 opcode:OpenRead p1:0 p2:2 p3:0 p4:5 p5:00 comment:
addr:2 opcode:Rewind p1:0 p2:14 p3:0 p4: p5:00 comment:
addr:3 opcode:Column p1:0 p2:2 p3:1 p4: p5:00 comment:
addr:4 opcode:Le p1:2 p2:13 p3:1 p4:(BINARY) p5:53 comment:
addr:5 opcode:Column p1:0 p2:3 p3:3 p4: p5:00 comment:
addr:6 opcode:Ge p1:4 p2:13 p3:3 p4:(BINARY) p5:53 comment:
addr:7 opcode:Column p1:0 p2:0 p3:5 p4: p5:00 comment:
addr:8 opcode:Column p1:0 p2:1 p3:6 p4: p5:00 comment:
addr:9 opcode:Copy p1:1 p2:7 p3:0 p4: p5:00 comment:
addr:10 opcode:Copy p1:3 p2:8 p3:0 p4: p5:00 comment:
addr:11 opcode:Column p1:0 p2:4 p3:9 p4: p5:00 comment:
addr:12 opcode:ResultRow p1:5 p2:5 p3:0 p4: p5:00 comment:
addr:13 opcode:Next p1:0 p2:3 p3:0 p4: p5:01 comment:
addr:14 opcode:Close p1:0 p2:0 p3:0 p4: p5:00 comment:
addr:15 opcode:Halt p1:0 p2:0 p3:0 p4: p5:00 comment:
addr:16 opcode:Transaction p1:0 p2:0 p3:3 p4:0 p5:01 comment:
addr:17 opcode:TableLock p1:0 p2:2 p3:0 p4:TEST p5:00 comment:
addr:18 opcode:Integer p1:20 p2:2 p3:0 p4: p5:00 comment:
addr:19 opcode:Integer p1:99999666 p2:4 p3:0 p4: p5:00 comment:
addr:20 opcode:Goto p1:0 p2:1 p3:0 p4: p5:00 comment:
架構缺陷與優化思路
缺陷:
1、採用虛擬機模式(VM),且粒度爲1行,switch case 太頻繁,使CPU的cache命中率不高。
2、操作粒度太小,一列一列地取值操作,不利於進一步優化(GPU加速,多線程等)。
3、進行優化時,需要同時修改前端與後端,雙重工作。
優化思路:
1、保留前端與後端,繞開VM機制,採用有向無環圖(DAG)——算子的架構,每個算子次處理一批數據。
2、對於算子本身可作GPU/多線程優化加速
Renascence(Genetic-Program-Frame)介紹
Renascence是自動編程架構的框架庫,一般而言它可視爲一個特殊的編譯器,算法庫不直接提供對外API,而是將往Renascence中註冊函數。上層應用或上層庫調用算法庫時,輸入一條公式,然後 Renascence將公式編譯爲有向無環圖,間接調用算法庫。
Renascence的作用,優化上層應用調用下層引擎的邏輯,並起自適配作用。
1、優化調用邏輯,防止重複計算
新查詢引擎的設計,主要便利用了Renascence這個功能。
比如如下的調用邏輯:f(g(x0), g(x0)),便可優化爲
T節點表示轉發,R節點表示傳遞,如是便省去g(x0)的第二次計算。
2、允許自動化參數調節
寫程序,尤其是算法類程序時,調參數是不可避免的事情,如多線程優化中開啓的線程數,開啓多線程優化的閾值,使用OpenCL時device端緩存的大小,一次運算的globalsize, localsize,等等。
Renascence可提供一個框架,以公式鏈接起來的所有函數參數,會通過一個通一的方案調優。這個優化的目標,對查詢引擎的優化而言,可以設爲時間的快慢或內存佔用的多少。
對sqlite查詢優化這個場景而言,主要就是內存池一個BLOCK的大小,每次取多少行的數據,哈希表初始大小等等。
3、允許部分程序邏輯根據實際應用場景動態生成與調節
比如一個where條件 where A > 30 and B < 50 and C >100,且 A、B、C 三列均無索引
公式解釋爲:FILTER(x0, AND(AND(x1, x2), x3)),其中x0爲源數據,x1、x2、x3是算符,分別表示 A>30、B<50和C>100
但由於註冊等價匹配: FILTER(x0, AND(x1, x2)) = FILTER(FILTER(x0, x1), x2) = FILTER(FILTER(x0, x2), x1)
將會被轉換爲如下一系列等價形式:
FILTER(x0, AND(AND(x1, x2), x3)) = FILTER(FILTER(x0, AND(x1, x2)), x3) | FILTER(FILTER(x0, x3), AND(x1,x2)) =
FILTER(FILTER(FILTER(x0, x1), x2), x3) | FILTER(FILTER(FILTER(x0, x2), x1), x3)
FILTER(FILTER(FILTER(x0, x3), x1), x2) | FILTER(FILTER(FILTER(x0, x3), x2), x1)
這四個等價形式中選取哪一個,可以通過數據庫採樣實驗(目標函數)動態決定。
2、3事實上是Renascence架構的核心,但對於數據庫的查詢引擎而言,程序邏輯相對固定,沒有太多讓Renascence發揮的空間,儘管如此,在Renascence架構之上實現CBO類似的優化還是會容易一些。
注:CBO 即 Cost Based Optimizer,爲ORACLE的優化器,
參考:http://blog.csdn.net/suncrafted/article/details/4239237
查詢引擎設計
圖中 libGP.so 即 Renascence 庫。
新引擎分爲如下部分:
對接層
1、轉換sqlite解析的語法樹爲 dag庫所支持的格式
2、調用sqlite後端B-Tree的代碼,獲取數據源
3、將dag庫返回的結果轉換爲sqlite的內存格式(Mem)
公式層
數據類型
在描述公式生成過程之前,先說明dag庫的主要數據類型定義:
RawData:從對接層調用sqlite後端,讀取出來的原始數據格式,sqlite是按行存儲的,但會作一個序列化操作
FormalData:RawData中的數據,經過投影器解析(實際是反序列化),將所需要的列解出而得的數據
VirtualColumn:一列數據,由 FormalData 中複製或經表達式運算而得
VirtualColumnSet:列集,VirtualColumn 的集合
每個查詢子句最終輸出的是一個 VirtualColumnSet,而輸入是一系列 RawData
語法分析與公式生成
我們先來看一下sqlite解析出來的語法樹結構
如圖所示,Select 結構包含的主要是表達式ExprList(由一系列Expr組成)和源SrcList(由一系列SrcList_item組成)
解析的核心思路就是依次解析從句,然後依次解析表達式提取數值。
SQL語句:select age, age*2, SNO from TEST where salary=99999666 order by SNO, SNAME; (salary 無索引)
Sort(MergeSet(MergeSet(SetAlloc(Column(Project(Filter(x0, Op2(Column(Project(x0, x1), x3), Const(x4), x2)), x5), x6)), Op2(Column(Project(Filter(x0, Op2(Column(Project(x0, x1), x3), Const(x4), x2)), x5), x6), Const(x8), x7)), Column(Project(Filter(x0, Op2(Column(Project(x0, x1), x3), Const(x4), x2)), x5), x9)),MergeSet(SetAlloc(Column(Project(Filter(x0, Op2(Column(Project(x0, x1), x3), Const(x4), x2)), x5), x9)), Column(Project(Filter(x0, Op2(Column(Project(x0, x1), x3), Const(x4), x2)), x5), x10)), x11)
這個公式是如何生成的呢:
第一步,決定數據源
與這一步有關的子項是SrcList和where
查看 Select 結構體的內容可知,只有一個Source,且不是子查詢,存在where條件條件,需要作過濾
得到的公式爲:
Project(Filter(x0, Op2(Column(Project(x0, x1), x3), Const(x4), x2)), x5)
爲了避免解析不通過where條件的行,在第一步解析where子句所需的列,在過濾之後再解析其他所需要的列。
(當需要連接時,處理邏輯會複雜一些,如兩個表內連接的對應公式爲Join(Project(x0, x2),SortAndMask(Project(x1, x3),x4), x5),x0 是大表的 RawData,x1是小表的 RawData,本篇暫不詳述)
第二步,解析Select的表達式組
解析表達式,直接按表達式樹一層組織公式就好,如圖所示的表達式樹:
被解析爲公式:
Op2(Column(x0, x1), Op2(Column(x0, x1), Const(x3), x2), x2)
Op2表示通用二目算子,x2爲算符*
x0爲 FormalData數據源
x3爲 表達式 2,經過 Const 解析爲常量 2
Column爲取列算子,x1記錄取的是age這一列
(其實這樣解析並不太好,最好方式是每個表達式對應一個算子,但這樣工作量很大,因此暫時這麼做)
在解析時,Column對應的源,會被替換爲第一步所得到的數據源,即Project(Filter(x0, Op2(Column(Project(x0, x1), x3), Const(x4), x2)), x5)
因此公式中會重複出現這一段,顯得所長。但後面 Renascence 作解析時,會去除冗餘,因此不會影響性能。
每個表達式解析完後,輸出是一個 列(VirtualColumn),需要用 SetAlloc 和 MergeSet 算子將其一個個合併爲列集(VirtualColumnSet)
第三步,加前綴(GroupBy和AggFunc)
由於此語句無 GroupBy,不詳細介紹
第四步,加後綴(Distinct、Sort、Limit)
這裏只有一個Sort子句。
Sort算子輸入是 Select 和 Order By 兩個表達式組產生的列集,輸出與 Select 表達式組的列集相同的一個列集。
因此解析 Order by 表達式集,將其合併,然後將其與 Select 列集一併輸入 Sort 算子,即得出最終的列集。
經過 Renascence 優化之後,這個公式將被拆成如下片段計算:
Root: Sort(MergeSet(MergeSet(SetAlloc(x12), Op2(x12, Const(x8), x7)), x13), MergeSet(SetAlloc(x13), Column(x15, x10)), x11)
x12: Column(x15, x6)
x13: Column(x15, x9)
x14: Filter(x0, Op2(Column(Project(x0, x1), x3), Const(x4), x2))
x15: Project(x14, x5)
對應的有向無環圖太複雜就不畫了。
運行DAG並讀取其輸出結果
首先根據公式創建一個 ADF:
AGPProducer* producer = ProducerInstance::getInstance();
mADF = GP_Function_Create_ByFormula(producer, formula, NULL);
這個ADF的輸入由兩部分組成:參數與數據源
參數parameters在創建公式的過程中生成,數據源則在運行ADF時臨時讀取。
配置輸入的代碼:
int i=0; i<maxnumber; ++i)
{
mInputs.push(NULL, NULL);
}
for (auto pair:parameters)
{
HWASSERT(pair.first < mInputs.size());
mInputs.contents[pair.first] = pair.second;
}
for (auto pair:sources)
{
HWASSERT(pair.first < mInputs.size());
mInputs.contents[pair.first].type = gHwOriginDataBatch;
}
運行的代碼:
for (auto pair : mSources)
{
auto batch = pair.second.produce(pair.second);
mInputs.contents[pair.first].content = batch;
}
mOutput = GP_Function_Run(mADF, &mInputs);
算子層
1、基於RawData的運算:
投影(Project):作數據解析,將 RawData 需要提取的列取出來,生成 FormalData
過濾(Filter):作數據過濾,將 RawData 中不滿足條件的行去掉。
2、基於列的運算:
Column:從FormalData複製一列數據出來
Const:將常數映射爲一個數據列
Op(表達式):對數據列作運算,生成新數據列
3、基於列集的運算:
去重(Distinct):對列集進行去重,目前使用 stl的set實現,效率比較低。
排序(Sort):對列集進行排序
分組(GroupBy):實際上是在排序的基礎額外做一個標誌,把相同的行合併。
這部分算子是最耗時間的,最需要重點優化
4、列轉換爲列集的算子:SetAlloc、MergeSet
SetAlloc:生成一個列集
MergeSet:往列集中加一個列
執行的過程如圖所示:
性能測試結果
小米pad上數據,TEST表數據量爲91W
TEST2錶行數 20W
時間爲運行5次的總時間:
語句 | 優化前時間/ms | 優化後時間/ms | 性能提升率% |
---|---|---|---|
select * from TEST where age >20 and salary<99999666 | 14252.2 | 12389.5 | 15.0 |
select * from TEST where age >20 | 13416.2 | 11889.1 | 12.8 |
select age, SNO, SNAME from TEST order by SNO, SNAME | 30262.1 | 22727.2 | 33.2 |
select age*2, SNO, SNAME from TEST order by SNO, SNAME | 31254.6 | 24319.0 | 28.5 |
select * from TEST where SNO in (select SNO from TEST2) | 23383.9 | 19860.9 | 17.7 |
select * from TEST where SNO in (‘aa’, ‘bb’, ‘cc’) | 2697.3 | 1914.26 | 40.9 |
select * from TEST2 | 3316.53 | 3061.97 | 8.3 |
select TEST.age, TEST.SNO, TEST.salary, TEST2.SNAME from TEST JOIN TEST2 on (TEST.salary=TEST2.SALARYDD) | 50112.1 | 42631.2 | 17.5 |
select sno, SNAME, sum(age+salary), count(age) from TEST group by SNO, SNAME | 31813.3 | 25966.2 | 22.5 |
select count(sno), sum(age) from TEST | 3167.34 | 2830.28 | 11.9 |
Renascence並行機制實現
(2016.7.15修改)
Renascence並行機制
並行與代碼優化的解耦
當前的並行計算框架,如多線程的openMP、大數據計算的hadoop-map-reduce、spark,都需要做算法代碼本身做改造。算法並行實現後的性能、效果跟改造的情況強相關,且進一步優化受限於平臺引擎(比如基於spark開發的代碼,再用simd、GPGPU加速就很困難)。
而Renascence的並行計算框架,意圖將代碼優化與並行計算的實現解耦,以並行腳本代替算法代碼的並行化改造,用並行計算引擎去解析腳本,實現多線程/分佈式的並行計算。這樣,代碼的優化就不依賴於並行計算的平臺,可以同步進行。
這個並行計算框架實現的是比較粗粒度的並行,而GPGPU、SIMD加速中的並行是數據並行,可認爲是對代碼本身的優化。
並行語言規範
參見
https://github.com/jxt1234/Renascence/blob/master/doc/Parallel.txt
文件組成
如下是一套完整的並行框架文件組成示例
so文件:
libGP.so:Renascence框架庫
libmthpGP.so:多線程並行引擎庫
libpics.so:圖像算法庫
xml配置文件:
func.xml:圖像算法列表
Map-Reduce.xml:並行化語言描述
mgpfunc.xml:並行計算引擎的函數列表
並行化實現結果
在sql查詢引擎中,排序的並行描述爲
REDUCE((MAP((x0,x1,x2), Sort(x0,x1,x2), [a0,b0,c0]->[a0], a0==b0),x1,x2), MergeSort(x0, y0, x1, x2), [a0,b0,c0]->[1], a0==b0)
Sort 對一個數組進行排序,使之有序。
MergeSort對兩個有序數組進行歸併,合併成一個有序數組。
這個描述表示:對所有數組執行一次Sort,使各自成爲有序數據,然後進行兩兩合併,執行MergeSort,最終合成一個大的有序數據。
如下爲引入 GP並行機制後,最新的數據(其他算子未完全支持):
語句 | 優化前時間/ms | 優化後時間/ms | 性能提升率% |
---|---|---|---|
select age, SNO, SNAME from TEST order by SNO, SNAME | 29418.7 | 16990.7 | 73.1 |
select age*2, SNO, SNAME from TEST order by SNO, SNAME | 30823.2 | 20118.2 | 53.2 |
基於Renascence架構的查詢引擎設計思想
依賴Renascence組織調用邏輯
查詢引擎分爲公式層與算子層,公式層生成一個模糊的公式,Renascence生成適合的程序邏輯(這裏是有向無環圖DAG),調用算子層進行計算。
性能優化集中於算子/執行層面
查詢引擎的性能優化只需要優化算子,邏輯性的優化由Renascence完成。
代碼不便公開,一些細節也未完全描述,關於並行化計算可參考Renascence中的文檔。