上一篇介紹了第七章執行器決議中“7.1 執行器整體架構及代碼概覽”、“7.2 執行流程”及“7.3 執行算子”的相關內容,本篇將介紹“7.4 運算式計算”及“7.5 編譯執行”的精彩內容,
7.4 運算式計算
運算式計算對應的代碼源檔案是“execQual.cpp”,openGauss處理SQL陳述句中的函式呼叫、計算式和條件運算式時需要用到運算式計算,
運算式的表示方式和查詢計劃樹的計劃節點類似,通過生成運算式計劃來對每個運算式節點進行計算,運算式繼承層次中的公共根類為Expr節點,其他運算式節點都繼承Expr節點,運算式狀態的公共根類為ExprState,記錄了運算式的型別以及實作該運算式節點的函式指標,運算式記憶體背景關系類為ExprContext,ExprContext充當了計劃樹節點中Estate的角色,運算式計算程序中的引數以及運算式所使用的記憶體背景關系都會存放到此結構中,
運算式計算對應的主要結構體代碼如下:
typedef struct Expr {
NodeTag type; /*運算式節點型別*/
} Expr;
struct ExprState {
NodeTag type;
Expr* expr; /*關聯的運算式節點*/
ExprStateEvalFunc evalfunc; /*運算式運算的函式指標*/
VectorExprFun vecExprFun;
exprFakeCodeGenSig exprCodeGen; /*運行LLVM匯編函式的指標*/
ScalarVector tmpVector;
Oid resultType;
};
運算式計算的程序分為3個部分:初始化、執行和清理,初始化的程序使用統一介面ExecInitExpr,根據運算式的型別選擇不同的處理方式,生成運算式節點樹,執行程序使用統一介面宏ExecEvalExpr,執行程序類似于計劃節點的遞回方式,
7.4.1 初始化階段
ExecInitExpr函式的作用是在執行的初始化階段,準備要執行的運算式樹,根據傳入的運算式node tree,來創建并回傳ExprState tree,在真正的執行階段會根據ExprState tree中記錄的處理函式,遞回地執行每個節點,ExecInitExpr函式的核心代碼如下:
if (node == NULL) { /* 判斷輸入是否為空 */
gstrace_exit(GS_TRC_ID_ExecInitExpr);
return NULL;}
switch (nodeTag(node)) { /* 根據節點型別初始化節點內容 */
case T_Var:
case T_Const:
case T_Param:
……
case T_CaseTestExpr:
case T_Aggref:
……
case T_CurrentOfExpr:
case T_TargetEntry:
case T_List:
case T_Rownum:
default:…… }
return state; /* 回傳運算式節點樹 */
ExecInitExpr函式主要執行流程如下,
(1) 判斷輸入的node節點是否為空,若為空,則直接回傳NULL,表示沒有運算式限制,
(2) 根據輸入的node節點的型別初始化變數evalfunc即node節點對應的執行函式,若節點存在引數或者運算式,則遞回呼叫ExecInitExpr函式,最后生成ExprState tree,
(3) 回傳ExprState tree,在執行運算式的時候會根據ExprState tree來遞回執行,
ExecInitExpr函式流程如圖7-12所示,

7.4.2 執行階段
執行階段主要是根據宏定義ExecEvalExpr遞回呼叫執行函式,在計算時的核心函式包括ExecMakeFunctionResult和ExecMakeFunctionResultNoSets,通過這兩個函式計算出運算式的結果并回傳,其他的運算式計算函式還包括ExecEvalFunc、ExecEvalOper、ExecEvalScalarVar、ExecEvalConst、ExecQual、ExecProject等,這些函式分別對應不同的運算式的型別或者引數型別,通過不同的邏輯來處理獲取的計算結果,
執行程序就是上層函式呼叫下層函式,首先下層函式根據引數型別獲取相應的資料,然后上層函式通過處理資料得到最后的結果,最后根據運算式邏輯回傳結果,
通過一個簡單的SQL陳述句介紹一下運算式計算的函式呼叫程序,每種SQL陳述句的執行流程不完全一致,此示例僅供參考,例句:“SELECT * FROM s WHERE s.a<3 or s.b<3;”,具體流程如下,
(1) 根據運算式“s.a<3 or s.b<3”確認第一步呼叫ExecQual函式,
(2) 由于本次運算式是or陳述句,所以需要將運算式傳入到ExecEvalOr函式計算,在ExecEvalOr函式中采用for回圈依次對子運算式“s.a<3”和“s.b<3”計算,將子運算式傳入到下一層函式中,
(3) ExecEvalOper函式根據子運算式的回傳值是否為set集來呼叫下一層函式,計算子運算式的結果,
(4) ExecMakeFunctionResultNoSets函式中獲取子運算式中的引數的值,“s.a”和“3”分別通過ExecEvalScalarVar函式和ExecEvalConst函式來獲取,獲取到引數之后計算運算式結果,若s.a<3本次計算回傳true,否則回傳false,并依次向上層回傳結果,
函式呼叫流程圖如圖7-13所示,

執行階段所有函式都共享此呼叫約定,相關代碼如下:
輸入:
expression:需要計算的運算式狀態樹,
econtext:評估背景關系資訊,
輸出:
return value:Datum型別的回傳值,
*isNull:如果結果為NULL,則設定為TRUE(實際回傳值無意義);如果結果非空,則設定為FALSE,
*isDone:設定為set-result狀態的指標,
只能接受單例(非集合)結果的呼叫方應該傳遞isDone為NULL,如果運算式計算得到集合結果(set-result),則回傳錯誤將通過ereport報告,如果呼叫者傳遞的isDone指標不為空,需要將*isDone設定為以下3種狀態之一:
(1) ExprSingleResult 單例結果(非集合),
(2) ExprMultipleResult 回傳值是集合的一個元素,
(3) ExprEndResult 集合中沒有其他元素,
當回傳ExprMultipleResult時,呼叫者應該重復呼叫并執行ExecEvalExpr函式,直到回傳ExprEndResult,
表7-30中列舉代碼“execQual.cpp”檔案中的部分主要函式,下面將依次詳細介紹每個函式的功能、核心代碼和執行流程,
主要函式 | 說明 |
ExecMakeFunctionResultNoSets | 運算式計算(非集合) |
ExecMakeFunctionResult | 運算式計算(集合) |
ExecEvalFunc/ExecEvalOper | 呼叫運算式計算函式 |
ExecQual | 檢查條件運算式 |
ExecEvalOr | 處理or運算式 |
ExecTargetList | 計算targetlist中的所有運算式 |
ExecProject | 計算投影資訊 |
ExecEvalParamExec | 獲取Exec型別引數 |
ExecEvalParamExtern | 獲取Extern型別引數 |
ExecMakeFunctionResult函式和ExecMakeFunctionResultNoS函式是運算式計算的核心函式,主要作用是通過獲取運算式的引數來計算出運算式結果,ExecMakeFunctionResultNoSets函式是ExecMakeFunctionResult函式的簡化版,只能處理回傳值是非集合情況,ExecMakeFunctionResult函式核心代碼如下:
fcinfo = &fcache->fcinfo_data; /* 宣告fcinfo */
InitFunctionCallInfoArgs(*fcinfo, list_length(fcache->args), 1); /*初始化fcinfo */
econtext->is_cursor = false;
foreach (arg, fcache->args) { /* 遍歷獲取引數值 */
ExprState* argstate = (ExprState*)lfirst(arg);
fcinfo->argTypes[i] = argstate->resultType;
fcinfo->arg[i] = ExecEvalExpr(argstate, econtext, &fcinfo->argnull[i], NULL);
if (fcache->func.fn_strict) /* 判斷引數是否存在空值 */
……
result = FunctionCallInvoke(fcinfo); /* 計算運算式結果 */
return result;
ExecMakeFunctionResultNoSets函式的執行流程如下,
(1) 宣告fcinfo來存盤運算式需要的引數資訊,通過InitFunctionCallInfoArgs函式初始化fcinfo中的欄位,
(2) 遍歷運算式中的引數args,通過ExecEvalExpr宏呼叫介面獲取每一個引數的值,存盤到“fcinfo->arg[i]”中,
(3) 根據func.fn_strict函式來判斷是否需要檢查引數空值情況,如果不需要檢查,則通過“FunctionCalllv-oke”宏將引數傳入運算式并計算出運算式的結果,否則進行判空處理,若存在空值則直接回傳空,若不存在空值則通過FunctionCalllvoke宏計算運算式結果,
(4) 回傳計算結果,
流程如圖7-14所示,

ExecMakeFunctionResult函式的執行流程如圖7-15所示,
(1) 判斷funcResultStore是否存在,如果存在則從中獲取結果回傳(注:如果下文(3)中的模式是SFRM_Materialize,則會直接跳到此處),
(2) 計算出引數值存入到fcinfo中,
(3) 把引數傳入到運算式函式中計算運算式,首先判斷引數args是否存在空,然后判斷回傳集合的函式的回傳模式,SFRM_ValuePerCall模式是每次呼叫回傳一個值,SFRM_Materialize模式是在Tuplestore中實體化的結果集,
(4) 根據不同的模式進行計算并回傳結果,

ExecEvalFunc和ExecEvalOper這兩個函式的功能類似,通過呼叫結果處理函式來獲取結果,如果函式本身或者它的任何輸入引數都可以回傳一個集合,那么就會調ExecMakeFunctionResult函式來計算結果,否則呼叫ExecMakeFunctionResultNoSets函式來計算結果,核心代碼如下:
init_fcache<false>(func->funcid,func->inputcollid,fcache, econtext->ecxt_per_query_memory, true); /* 初始化fcache */
if (fcache->func.fn_retset) { /* 判斷回傳結果型別 */
……
return ExecMakeFunctionResult<true, true, true>(fcache, econtext, isNull, isDone);
} else if (expression_returns_set((Node*)func->args)) {
……
return ExecMakeFunctionResult<true, true, false>(fcache, econtext, isNull, isDone);
} else {
……
return ExecMakeFunctionResultNoSets<true, true>(fcache, econtext, isNull, isDone);
}
ExecEvalFunc函式的執行流程如下,
(1) 是通過init_fcache函式初始化FuncExprState節點,包括初始化引數、記憶體管理等等,
(2) 根據FuncExprState函式中的資料判斷回傳結果是否為set型別,并呼叫相應的函式計算結果,
ExecEvalFunc函式執行流程如圖7-16所示,

ExecQual函式的作用是檢查slot結果是否滿足運算式中的子運算式,如果子運算式為false,則回傳false否則回傳true,表示該結果符合預期,需要輸出,核心代碼如下:
foreach (l, qual) { /* 遍歷qual中的子運算式并計算 */
expr_value = ExecEvalExpr(clause, econtext, &isNull, NULL);
if (isNull) { /* 判斷計算結果 */
if (resultForNull == false) {
result = false;
break;
}
} else {
if (!DatumGetBool(expr_value)) {
result = false;
……
return result; /* 回傳結果是否滿足運算式 */
ExecQual函式的主要執行流程如下,
(1) 遍歷qual中的子運算式,根據ExecEvalExpr函式計算結果是否滿足該子運算式,若滿足則expr_value為1,否則為0,
(2) 判斷結果是否為空,若為空,則根據resultForNull引數得到回傳值資訊,若不為空,則根據expr_value判斷回傳true或者false,
(3) 回傳result,
ExecQual函式的執行流程如圖7-17所示,

ExecEvalOr函式的作用是計算通過or連接的bool運算式(布爾運算式,最終只有true(真)和false(假)兩個取值),檢查slot結果是否滿足運算式中的or運算式,如果結果符合or運算式中的任何一個子運算式,則直接回傳true,否則回傳false,如果獲取的結果為null,則記錄isNull為true,核心代碼如下:
foreach (clause, clauses) { /* 遍歷子運算式 */
ExprState* clausestate = (ExprState*)lfirst(clause);
Datum clause_value;
clause_value = ExecEvalExpr(clausestate, econtext, isNull, NULL); /* 執行運算式 */
/* 如果得到不空且ture的結果,直接回傳結果 */
if (*isNull)
/* 記錄存在空值 */
AnyNull = true;
else if (DatumGetBool(clause_value))
/* 一次結果為true就回傳 */
return clause_value; /* 回傳執行結果 */
}
*isNull = AnyNull;
return BoolGetDatum(false);
ExecEvalOr函式主要執行流程如下,
(1) 遍歷子運算式clauses,
(2) 通過ExecEvalExpr函式來呼叫clause中的運算式計算函式,計算出結果,
(3) 對結果進行判斷,or運算式中若有一個結果滿足條件,就會跳出回圈直接回傳,
ExecEvalOr函式的執行流程如圖7-18所示,

ExecTargetList函式的作用是根據給定的運算式背景關系計算targetlist中的所有運算式,將計算結果存盤到元組中,主要結構體代碼如下:
typedef struct GenericExprState {
ExprState xprstate;
ExprState* arg; /*子節點的狀態*/
} GenericExprState;
typedef struct TargetEntry {
Expr xpr;
Expr* expr; /*要計算的運算式*/
AttrNumber resno; /*屬性號*/
char* resname; /*列的名稱*/
Index ressortgroupref; /*如果被sort/group子句參考,則為非零*/
Oid resorigtbl; /*列的源表的OID */
AttrNumber resorigcol; /*源表中的列號*/
bool resjunk; /*設定為true可從最終目標串列中洗掉該屬性*/
} TargetEntry;
ExecTargetList函式主要執行流程如下,
(1) 遍歷targetlist中的運算式,
(2) 計算運算式結果,
(3) 判斷結果中itemIsDone[resind]引數并生成最后的元組,
ExecTargetList函式的執行流程如圖7-19所示,

ExecProject函式的作用是進行投影操作,投影操作是一種屬性過濾程序,該操作將對元組的屬性進行精簡,把在上層計劃節點中不需要用的屬性從元組中去掉,從而構造一個精簡版的元組,投影操作中被保留下來的那些屬性被稱為投影屬性,主要結構體代碼如下:
typedef struct ProjectionInfo {
NodeTag type;
List* pi_targetlist; /*目標串列*/
ExprContext* pi_exprContext; /*記憶體背景關系*/
TupleTableSlot* pi_slot; /*投影結果*/
ExprDoneCond* pi_itemIsDone; /*ExecProject的作業區陣列*/
bool pi_directMap;
int pi_numSimpleVars; /*在原始tlist(查詢目標串列)中找到的簡單變數數*/
int* pi_varSlotOffsets; /*指示變數來自哪個slot(槽位)的陣列*/
int* pi_varNumbers; /*包含變數的輸入屬性數的陣列*/
int* pi_varOutputCols; /*包含變數的輸出屬性數的陣列*/
int pi_lastInnerVar; /*內部引數*/
int pi_lastOuterVar; /*外部引數*/
int pi_lastScanVar; /*掃描引數*/
List* pi_acessedVarNumbers;
List* pi_sysAttrList;
List* pi_lateAceessVarNumbers;
List* pi_maxOrmin; /*串列優化,指示獲取此列的最大值還是最小值*/
List* pi_PackTCopyVars; /*記錄需要移動的列*/
List* pi_PackLateAccessVarNumbers; /*記錄cstore(列存盤)掃描中移動的內容的列*/
bool pi_const;
VectorBatch* pi_batch;
vectarget_func jitted_vectarget; /* LLVM函式指標*/
VectorBatch* pi_setFuncBatch;
} ProjectionInfo;
ExecProject函式的主要執行流程如下,
(1) 取ProjectionInfo需要投影的資訊,按照執行的偏移獲取原屬性所在的元組,通過偏移量獲取該屬性,并通過目標屬性的序號找到對應的新元組屬性位置進行賦值,
(2) 對pi_targetlist進行運算,將結果賦值給對應元組中的屬性,
(3)產生一個行記錄結果,對slot做標記處理,slot包含一個有效的虛擬元組,
ExecProject函式的執行流程如圖7-20所示,

ExecEvalParamExec函式的作用是獲取并回傳PARAM_EXEC型別的引數,PARAM_EXEC引數是指內部執行器引數,是需要執行子計劃來獲取的結果,最后需要將結果回傳到上層計劃中,核心代碼如下:
prm = &(econtext->ecxt_param_exec_vals[thisParamId]); /* 獲取econtext中引數 */
if (prm->execPlan != NULL) { /* 判斷是否需要生成引數 */
/* 引數還未計算執行此函式*/
ExecSetParamPlan((SubPlanState*)prm->execPlan, econtext);
/*引數計算完計劃重置為空*/
Assert(prm->execPlan == NULL);
prm->isConst = true;
prm->valueType = expression->paramtype;
}
*isNull = prm->isnull;
prm->isChanged = true;
return prm->value; /* 回傳生成的引數 */
ExecEvalParamExec函式的主要執行流程如下,
(1) 獲取econtext中的ecxt_param_exec_vals引數,
(2) 判斷子計劃是否為空,若不為空則呼叫ExecSetParamPlan函式執行子計劃獲取結果,并把計劃置為空,當再次執行此函式時,不需要重新執行計劃,直接回傳已經獲取過結果,
(3) 將結果prm->value回傳,
ExecEvalParamExec函式的執行流程如圖7-21所示,

ExecEvalParamExtern函式的作用是獲取并回傳PARAM_EXTERN型別的引數,該引數是指外部傳入引數,例如在PBE執行時,PREPARE的陳述句中的引數,在需要execute陳述句執行時傳入,核心代碼如下:
if (paramInfo && thisParamId > 0 && thisParamId <= paramInfo->numParams) {/* 判斷引數 */
ParamExternData* prm = ¶mInfo->params[thisParamId - 1];
if (!OidIsValid(prm->ptype) && paramInfo->paramFetch != NULL) /* 獲取動態引數 */
(*paramInfo->paramFetch)(paramInfo, thisParamId);
if (OidIsValid(prm->ptype)) { /*檢查引數并回傳 */
if (prm->ptype != expression->paramtype)
ereport(……);
*isNull = prm->isnull;
if (econtext->is_cursor && prm->ptype == REFCURSOROID) {
CopyCursorInfoData(&econtext->cursor_data, &prm->cursor_data);
econtext->dno = thisParamId - 1;
}
return prm->value;
}
}
ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT), errmsg("no value found for parameter %d", thisParamId)));
return (Datum)0;
ExecEvalParamExtern函式主要執行流程如下,
(1) 判斷PARAM_EXTERN型別的引數否存在,若存在則從ecxt_param_list_info中獲取該引數,否則直接報錯,
(2) 判斷引數是否是動態的,若是動態的則再次獲取引數,
(3) 判斷引數型別是否符合要求,若符合要求直接回傳該引數,
ExecEvalParamExtern函式的執行流程如圖7-22所示,

7.5 編譯執行
為了提高SQL的執行速度,解決傳統資料處理引擎條件邏輯冗余的問題,openGauss為執行運算式引入了CodeGen技術,其核心思想是為具體的查詢生成定制化的機器碼代替通用的函式實作,并盡可能地將資料存盤在CPU暫存器中,openGauss通過LLVM編譯框架來實作CodeGen,LLVM是“Low Level Virtual Machine”的縮寫,開發之初是想作為一個底層虛擬機,但隨著開發,以及功能的逐漸完善,慢慢變成一個模塊化的編譯系統,并能支持多種語言,LLVM的系統架構如圖7-23所示,

LLVM大體上可以分成3個部分,
(1) 支持多種語言的前端,
(2) 優化器,
(3) 支持多種CPU架構的后端(X86、Aarch64),
LLVM與GCC一樣,都是常用的編譯系統,但是LLVM更加模塊化,從而可以免去每使用一套語言換一套優化器的作業,開發者只要設計相應的前端,并針對各個目標平臺做后端優化,
考慮如下SQL陳述句,
SELECT * FROM dataTable WHRER (x + 2) * 3 > 4;
正常的遞回流程如圖7-24所示,

此類運算式的執行代碼是一套通用的函式實作,每次遞回都有很多冗余判斷,需要依賴上一步的輸出作為當前的輸入,實作如下代碼邏輯:
void MaterializeTuple(char * tuple) {
for (int I = 0; i < num_slots_; i++) {
char* slot = tuple + offsets_[i];
switch(types_[i]) {
case BOOLEAN:
*slot = ParseBoolean();
break;
case INT:
*slot = ParseInt();
Break;
case FLOAT: …
case STRING: …
… …
}
}
}
通過CodeGen可以為運算式構造定制化的實作,如下代碼所示:
void MaterializeTuple(char * tuple) {
*(tuple + 0) = ParseInt();
*(tuple + 4) = ParseBoolean();
*(tuple + 5) = ParseInt();
}
通過減少冗余的判斷分支,極大減少了SQL執行時間,同時也減少大量虛函式的呼叫,為了實作基于LLVM的CodeGen,并方便介面呼叫,openGauss定義了一個GsGodeGen類,GodeGen所有介面都在這個類中實作,主要的成員變數包括:
llvm::Module* m_currentModule; /* 當前query使用的module */
bool m_optimizations_enabled; /* modules是否能優化 */
bool m_llvmIRLoaded; /* IR檔案是否已經載入 */
bool m_isCorrupt; /* 當前query的module是否可用 */
bool m_initialized; /* GsCodeGen 物件是否完成初始化 */
llvm::LLVMContext* m_llvmContext; /* llvm背景關系 */
List* m_machineCodeJitCompiled; /* 保存所有機器碼JIT編譯完成的函式 */
llvm::ExecutionEngine* m_currentEngine; /* 當前query的llvm執行引擎 */
bool m_moduleCompiled; /* module是否編譯完成 */
MemoryContext m_codeGenContext; /* CodeGen記憶體背景關系 */
List* m_cfunction_calls; /* 記錄運算式中呼叫IR的c函式 */
這里涉及一些LLVM的概念,Module是LLVM的一個重要類,可以把Module看作一個容器,每個Moudle以下的元素構成:函式、全域變數、符號表入口、以及LLVM linker(聯系Moudles之間其他模塊的全域變數,函式的前向宣告,以及外部符號表入口);LLVMContext這是一個在執行緒背景關系中使用LLVM的類,它擁有和管理LLVM核心基礎設施的核心“全域”資料,包括型別和常量唯一表,IR檔案是LLVM的中間檔案,前端將用戶代碼(C/C++、python等)轉換成IR檔案,優化器對IR檔案進行優化,openGauss的GodeGen代碼功能之一就是將函式轉換成IR格式的檔案,通常在代碼中將源代碼轉換成IR的方式有多種,openGauss生成IR是使用“llvm::IRBuilder<>”函式,在后面會詳細介紹,如果查詢計劃樹的算子支持CodeGen,那么針對該函式生成“Intermediate Representation”函式(IR 函式),這個IR函式是查詢級別的,即每一個查詢對應的IR函式是不同的,同時對應每一個查詢有多個IR函式,這是因為可以只做區域替換,即只動態生成查詢計劃樹中某個算子或某部分操作函式的IR函式,如只實作投影功能的IR函式,
openGauss GodeGen的整體編譯流程如圖7-25所示,

資料庫啟動后,首先對LLVM初始化,其中CodeGenProcessInitialize函式對LLVM的環境進行初始化,包括通過isCPUFeatureSupportCodegen函式和canInitCodegenInvironment函式檢查CPU是否支持CodeGen、是否能夠進行環境初始化,然后通過“GsCodeGen::InitializeLlvm”函式對本地環境檢查,檢查環境是否為Aarch64或x86架構,并回傳全域變數gscodegen_initialized,
CodeGenThreadInitialize函式在本執行緒中創建一個新的GsCodeGen物件,并創建記憶體,如果創建失敗,要回傳原來的記憶體背景關系給系統,當前執行緒中codegen的部分保存在knl_t_codegen_context中,具體結構代碼為:
typedef struct knl_t_codegen_context {
void* thr_codegen_obj;
bool g_runningInFmgr;
long codegen_IRload_thr_count;
} knl_t_codegen_context;
其中thr_codegen_obj欄位保存代碼中LLVM物件,在初始化和呼叫時通常轉換成GsCodeGen類,GsCodeGen保存了LLVM全部封裝好的LLVM函式、記憶體和成員變數等,g_runningInFmgr欄位表示函式是否運行在function manager中,codegen_IRload_thr_count欄位是IR載入計數,
當所有的LLVM執行環境設定完成后,執行器初始化階段可根據決議器和優化器提供的查詢計劃去檢查當前的計劃是否可以進行LLVM代碼生成優化,以gsql客戶端為例,整個運行程序內嵌在執行引擎運行程序內,函式的呼叫從函式exec_simple_plan函式為入口,LLVM運行的3個階段分別對應executor的3個階段:ExecutorStart、ExecutorRun以及ExecutorEnd(從其他客戶端輸入的查詢,最終也會走到ExecutorStart、ExecutorRun以及ExecutorEnd階段),
(1) ExecutorStart階段:為運行準備階段,初始化查詢級別的GsCodeGen類物件,并在InitPlan階段按照優化器產生的執行計劃遍歷其中各個算子節點初始化函式,生成IR函式,
(2) ExecutorRun階段:為運行階段,若已成功生成LLVM IR函式,則對該IR函式進行編譯,生成可執行的機器碼,并在具體的算子運行階段用機器碼替換到原本的執行函式入口,
(3) ExecutorEnd階段:為運行完清理環境階段,在ExecutorEnd函式中將第一階段生成的LLVMCodeGen物件及其相關資源進行釋放,
GsCodeGen的介面定義在檔案“codegen/gscodegen.h”中,GsCodeGen中介面說明如表7-31所示,
介面名稱 | 介面型別 | 職責描述 |
|---|---|---|
initialize | API | 分配Codegen使用記憶體使用環境 |
InitializeLLVM | API | 初始化LLVM運行環境 |
parseIRFile | API | 決議IR檔案 |
cleanupLlvm | API | 停止LLVM呼叫執行緒 |
createNewModule | API | 創建一個新的LLVM模板 |
compileCurrentModule | API | 編譯當前指定LLVM模塊中的函式 |
compileModule | API | 編譯模板并依據相關選項對模板中未用的IR函式進行優化 |
releaseResource | API | 釋放LLVM模塊占用的系統資源 |
FinalizeFunction | API | 確定最后的IR函式是否可用 |
getType | API | 從openGauss的型別轉換到LLVM內部對應的型別 |
verifyFunction | API | 檢查輸入的LLVM IR函式的有效性 |
getPtrType | API | 從openGauss的型別轉換到LLVM內部對應該型別的指標型別 |
castPtrToLlvmPtr | API | 將openGauss的指標轉換為LLVM的指標 |
getIntConstant | API | 將openGauss對應型別的常數轉換為LLVM對應型別的常數 |
generatePrototype | API | 創建要加入當前LLVM模塊的函式原型 |
replaceCallSites | API | 替換LLVM當前模塊的函式 |
optimizeModule | API | 優化LLVM當前模塊中的函式 |
addFunctionToMCJit | API | 外部函式呼叫介面 |
canInitCodegenInvironment | API | 判斷當前可否初始化CodeGen環境 |
canInitThreadCodeGen | API | 判斷當前可否初始化CodeGen執行緒 |
CodeGenReleaseResource | API | 洗掉當前模板和LLVM執行引擎 |
CodeGenProcessInitialize | API | 初始化LLVM服務行程 |
CodeGenThreadInitilize | API | 初始化LLVM服務執行緒 |
CodeGenThreadRuntimeSetup | API | 初始化LLVM服務物件 |
CodeGenThreadRuntimeCodeGenerate | API | 編譯當前LLVM模板中的IR函式 |
CodeGenThreadTearDown | API | 釋放LLVM模塊占用的系統資源介面 |
CodeGenThreadObjectReady | API | 判斷當前LLVM服務物件是否有效 |
CodeGenThreadReset | API | 清空當前記憶體中的機器碼 |
CodeGenPassThreshold | API | 根據回傳行數判斷是否需要CodeGen |
GsCodeGen提供LLVM環境處理函式和module函式,以及處理IR的函式,另一方面,為了處理算子函式功能,將每個算子涉及的各個運算子封裝在ForeigenScanCodeGen類中,介面定義在“codegen/foreignscancodegen.h”中,各個介面功能如表7-32所示:
介面名稱 | 介面型別 | 職責描述 |
|---|---|---|
ScanCodeGen | API | 生成外表掃描謂詞運算式運算對應的IR函式 |
IsJittableExpr | API | 謂詞中的運算式是否支持LLVM化 |
buildConstValue | API | 獲取謂詞運算式中的常量 |
目前針對不同的運算式,openGauss實作了4個類:
(1) VecExprCodeGen類主要用于處理查詢陳述句中運算式計算的LLVM動態編譯優化,目前主要處理的是過濾條件語法中的運算式,即在ExecVecQual函式中處理的運算式計算,
(2) VecHashAggCodeGen類用于對節點hashagg運算的LLVM動態編譯優化,
(3) VecHashJoinCodeGen類用于對節點hash join運算的LLVM動態編譯優化,
(4) VecSortCodeGen類用于對節點sort運算的LLVM動態編譯優化,
7.5.1 VecExprCode類
VecExprCodeGen類用于支持openGauss設計框架中向量化運算式的動態編譯優化,即生成各類向量化運算式計算的IR函式,VecExprCodeGen類主要針對存在qual的查詢場景,即運算式在WHERE語法中的查詢場景,VecExprCodeGen介面定義在“codegen/vecexprcodegen.h”檔案中,VecExprCode類支持的陳述句場景為:
SELECT targetlist expr FROM table WHERE filter expr…;
其中,對filter expr進行LLVM化處理,
列存盤執行引擎每次處理的為一個VectorBatch,在執行程序中,由于采用迭代計算模型,對于每一個qual,會遍歷整個qual運算式,然后根據遍歷得到的資訊去讀取VectorBatch中的列向量ScalarVector,這樣就會導致需要不停地去替換當前存放在記憶體或暫存器中的資料,為了更好地減少資料讀取,讓資料在計算程序中更久地存放在暫存器中,將ExecVecQual與對VectorBatch進行結合處理:只有當前的資料處理完所有的vecqual時再更新暫存器中的資料,即原本的執行流程,相關代碼如下:
foreach(cell, qual)
{
DealVecQual(batch->m_arr[var->attno-1]);
}
替換為
for(i = 0; i < batch->m_rows; i++)
{
foreach(cell, qual)
{
DealVecQual(batch->m_arr[var->attno-1]->m_vals[i]);
}
}
DealVecQual代表的就是對當前的資料引數進行qual條件處理,可以看到現有的處理方式實際上已經退化為行存盤的形式,即每次只處理batch中的一行資料資訊,但是該資料資訊會一直存放在暫存器中,直至所有的qual條件處理完成,表7-33列出了VecExprCodeGen的所有介面,
介面名稱 | 介面型別 | 職責描述 |
ExprJittable | API | 判斷單個運算式是否支持LLVM化 |
QualJittable | API | 判斷整個qual條件是否支持LLVM化 |
QualCodeGen | API | ExecVecQual的LLVM化,生成的“machine code”用于替換實際執行時的ExecVecQual |
ExprCodeGen | API | ExecInitExpr的LLVM化,目前只支持部分功能和函式的LLVM化 |
OpCodeGen | API | 運算子運算式(算術運算式,比較運算式等)的LLVM化,目前支持的資料型別包括int、float、numeric、text和bpchar等型別 |
ScalarArrayCodeGen | API | ExecEvalScalarArrayOp的LLVM化處理,支持的型別包括text、varchar、bpchar、int和float型別 |
CaseCodeGen | API | ExecEvalVecCase的LLVM化處理,其中“case when”中的選項型別包括int型別和text、bpchar型別,對于復雜運算式的暫只支持substr |
VarCodeGen | API | ExecEvalVecVar的LLVM化處理 |
EvalConstCodeGen | API | ExecEvalConst的LLVM化處理 |
舉例來說,以ExecCStoreScan函式中處理qual運算式來說明,以本次查詢所生成的查詢計劃樹為輸入,編譯得到機器碼,因此實作呼叫需要做到如下兩點,
(1) 結合所實作的函式介面,依據當前查詢計劃樹,生成對應的IR函式,
如提供了ExecVecQual的LLVM化介面,則通過遍歷每一個qual并判斷是否支持LLVM化來判斷當前的ps.qual是否可生成IR函式,如果判斷可生成,則借助IR builder API生成對應于當前quallist的IR函式,相關代碼如下:
if (!codegen_in_up_level) {
consider_codegen = CodeGenThreadObjectReady() &&
CodeGenPassThreshold(((Plan*)node)->plan_rows,
estate->es_plannedstmt->num_nodes, ((Plan*)node)->dop);
if (consider_codegen) {
jitted_vecqual = dorado::VecExprCodeGen::QualCodeGen(scan_stat->ps.qual, (PlanState*)scan_stat);
if (jitted_vecqual != NULL)
llvm_code_gen->addFunctionToMCJit(jitted_vecqual, reinterpret_cast<void**>(&(scan_stat->jitted_vecqual)));
}
}
代碼段顯示了ExecInitCStoreScan函式中對于ps.qual部分的處理,如果存在LLVM環境,則優先去生成ps.qual的IR函式,在QualCodeGen函式中的QualJittable用于判斷當前ps.qual是否可LLVM化,
(2) 將原本的執行函式入口替換成預編譯好的可執行機器碼,
當步驟(1)已經生成IR函式后,則根據如圖7-25中所示那樣會進行編譯(compile IR Function),那么在實際執行過濾的時候就會進行替換,相關代碼如下:
if (node->jitted_vecqual)
p_vector = node->jitted_vecqual(econtext);
else
p_vector = ExecVecQual(qual, econtext, false);
代碼段顯示了如果生成了用于處理CStoreScan函式中plan.qual的機器碼,則直接去呼叫生成的jitted_vecqual函式,如果沒有,則按照原有的執行邏輯去處理,
表7-33中提到OpCodegen是運算子的LLVM化,其支持的資料結構包括了布爾型、浮點型、整型和字符型等,源代碼在“gausskernel/runtime/codegen/codegenutil”目錄中,以boolcodegen.cpp、datecocegen.cpp格式命名,
LLVM提供了很多針對于資料的基本操作,包括基本算術運算和比較運算,由于LLVM最高可支持(223-1)位的整數型別,且資料型別可以進行二進制轉換(延展,擴充都可以),因此LLVM只需要提供整型資料比較和浮點型資料比較即可,一個典型的比較運算子介面代碼如下(以’=’為例):
llvm:Value *CreateICmpEQ(Value *LHS, Value *RHS, const Twine &Name = "")
其中LHS和RHS為參與運算的輸入引數,而Name表示在運算時候的變數名,類似地,LLVM也提供了眾多的基本運算,如兩個整型資料相加的介面代碼為:
llvm:Value *CreateAdd(Value *LHS, Value *RHS, const Twine &Name = "")
通過LLVM提供的這些基本介面就完成一些常用的運算操作,
復雜的運算都是通過回圈結構和條件判斷結構實作的,在LLVM中,回圈結構和條件判斷結構都是基于“IR Builder”類中的BasicBlock結構來實作的,因為回圈結構和條件判斷的執行都可以理解為當滿足某個條件后去執行回圈結構內部或對應條件分支內部的內容,事實上,“Basic Block”也是整個代碼中的控制流,一個簡單的條件判斷呼叫代碼為:
builder.CreateCondBr(Value *cond, BasicBlock *true, BasicBlock *false);
其中cond為條件判斷結果值,如果為true,就進入true-block分支,如果為false,就進入false-block分支,“builder.SetInsertPoint(entry)”表示進入對應的entry-block分支,在這樣的基本設計思想下,如下一個簡單的for回圈結構:
int i = 0;
int b = 0;
for( i = 0; i < 100; i++)
{
b = b + 1;
}
就需要通過如下的LLVM builder偽代碼實作:
Builder.SetInsertPoint(for_begin);
cond=builder.CreateICmpLT(i,100);
builder.CreateCondBr(cond, for_body, for_end);
builder.SetInsertPoint(for_body);
b = builder.CreateAdd(b,1);
buider.CreateBr(for_check);
builder.SetInsertPoint(for_check);
i=builder.CreateAdd(i,1);
builder.CreateBr(for_begin);
builder.SetInsertPoint(for_end);
builder.CreateAlignLoad(b);
builder.CreateRet(b);
其中builder.CreateBr函式表示無條件進入對應的block,實際上是一個控制流,CreateRet(b)表示當前函式結束后回傳相應的值,通過撰寫類似的程式就可以生成如下執行所需的IR函式:
define i32 @main() #0 {
%1 = alloca i32, align 4
%i = alloca i32, align 4
%b = alloca i32, align 4
store i32 0, i32* %1
store i32 0, i32* %i, align 4
store i32 0, i32* %b, align 4
store i32 0, i32* %i, align 4
br label %2
; <label>:2 ; preds = %8, %0
%3 = load i32* %i, align 4
%4 = icmp slt i32 %3, 100
br i1 %4, label %5, label %11
; <label>:5 ; preds = %2
%6 = load i32* %b, align 4
%7 = add nsw i32 %6, 1
store i32 %7, i32* %b, align 4
br label %8
; <label>:8 ; preds = %5
%9 = load i32* %i, align 4
%10 = add nsw i32 %9, 1
store i32 %10, i32* %i, align 4
br label %2
; <label>:11 ; preds = %2
%12 = load i32* %b, align 4
ret i32 %12
}
上述的IR函式經過編譯后就可以直接在執行階段被呼叫,從而提升執行效率,而后續OLAP-LLVM層的代碼設計都基于上述的基本資料結構,資料型別和BasicBlock控制流結構,一個完整的生成IR函式的構建代碼結構如下:
llvm::Function *func(InputArg[計劃樹資訊])
{
定義資料型別,變數值;
申明動態引數[帶有實際資料的引數];
控制流主體;
回傳結果值,
}
因此后續單個LLVM函式的具體的設計和實作都將依賴于本節所介紹的基本框架,
7.5.2 VecHashAggCodeGen類
對于hash聚合來說,資料庫會根據“GROUP BY”欄位后面的值算出哈希值,并根據前面使用的聚合函式在記憶體中維護對應的串列,VecHashAggCodeGen類的介面實作在“codegen/vechashaggcodegen.h”檔案中,介面的說明如表7-34所示,
介面名稱 | 介面型別 | 職責描述 |
GetAlignedScale | API | 計算當前運算式scale |
AggRefJittable | API | 判斷運算式是否支持LLVM化 |
AggRefFastJittable | API | 判斷當前運算式是否能用快速CodeGen |
AgghashingJittable | API | 判斷Agg節點是否能LLVM化 |
HashAggCodeGen | API | HashAgg節點構建IR函式的主函式 |
SonicHashAggCodeGen | API | Sonic hashagg節點構建IR函式的主函式 |
HashBatchCodeGen | API | 為“hashBatch”函式生成LLVM函式指標 |
MatchOneKeyCodeGen | API | 為“match_key”函式生成LLVM函式指標 |
BatchAggJittable | API | 判斷當前batch aggregation節點是否支持LLVM化 |
BatchAggregationCodeGen | API | 為BatchAggregation節點生成LLVM函式指標 |
SonicBatchAggregationCodeGen | API | 為SonicBatchAggregation節點生成LLVM函式指標 |
openGauss內核在處理Agg節點時,首先在ExecInitVecAggregation函式中判斷是否進行CodeGen,如果行數大于codegen_cost_threshold引數那么可以進行CodeGen,
bool consider_codegen =
CodeGenThreadObjectReady() &&CodeGenPassThreshold(((Plan*)outer_plan)->plan_rows,
estate->es_plannedstmt->num_nodes, ((Plan*)outer_plan)->dop);
if (consider_codegen) {
if (node->aggstrategy == AGG_HASHED && node->is_sonichash) {
dorado::VecHashAggCodeGen::SonicHashAggCodeGen(aggstate);
} else if (node->aggstrategy == AGG_HASHED) {
dorado::VecHashAggCodeGen::HashAggCodeGen(aggstate);
}
}
如果輸出行數小于codegen_cost_threshold,那么codegen的成本要大于執行優化的成本,如果節點是sonic型別,執行SonicHashAggCodeGen函式;一般的HashAgg節點執行HashAggCodeGen函式,SonicHashAggCodeGen函式和HashAggCodeGen函式的執行流程如圖7-26所示,

HashAggCodeGen函式是HashAgg節點LLVM化的主入口,openGauss在結構體VecAggState中定義哈希策略的Agg節點,openGauss針對LLVM化Agg節點增加了5個引數用來保存codegen后的函式指標:jitted_hashing、jitted_sglhashing、jitted_batchagg、jitted_sonicbatchagg以及jitted_SortAggMatchKey,而且openGauss在addFunctionToMCJit函式中用生成的IR函式與節點對應的函式指標構造一個鏈表,
7.5.3 VecHashJoinCodeGen類
VecHashAggCodeGen類的定義在“codegen/vechashjoincodegen.h”檔案中,介面說明如表7-35所示,
介面名稱 | 介面型別 | 職責描述 |
|---|---|---|
GetSimpHashCondExpr | API | 回傳var運算式 |
JittableHashJoin | API | 判斷當前hash join節點是否支持LLVM化 |
JittableHashJoin_buildandprobe | API | 判斷buildHashTable/probeHashTable是否可以LLVM化 |
JittableHashJoin_bloomfilter | API | 判斷bloom filter(布隆過濾器)函式是否能LLVM化 |
HashJoinCodeGen | API | hash join節點構建IR函式的主函式 |
HashJoinCodeGen_fastpath | API | hash join節點生成快速IR函式 |
KeyMatchCodeGen | API | keyMatch函式生成LLVM函式 |
HashJoinCodeGen_buildHashTable | API | 為buildHashTable函式生成LLVM函式 |
HashJoinCodeGen_buildHashTable_NeedCopy | API | 磁區表中buildHashTable函式生成LLVM函式 |
HashJoinCodeGen_probeHashTable | API | probeHashTable生成LLVM函式 |
在函式ExecInitVecHashJoin中,為hash join節點進行CodeGen的代碼為:
if (consider_codegen && !node->isSonicHash) {
dorado::VecHashJoinCodeGen::HashJoinCodeGen(hash_state);
}
其中consider_codegen是根據行數判斷是否進行CodeGen,HashJoinCodeGen是hash join節點LLVM化的主入口,與其他可LLVM化的節點一樣,生成IR函式后,將IR函式與節點結構體中對應變數系結,如圖7-27所示,

圖中所有CodeGen函式回傳都是“LLVM::Function”型別的IR函式指標,其中值得注意的是當enable_fast_keyMatch的值等于0時,是正常的“key match”;等于2時,所有key值型別都是int4或者int8并且不為NULL,這時候可使用更少的記憶體和更少的分支,所以叫作“fast path”,
7.5.4 VecSortCodeGen類
VecSortCodeGen是為sort節點LLVM化定義的一個類,類中的介面宣告在“codegen/vecsortcodegen.h”檔案中,介面描述如表7-36所示,
介面名稱 | 介面型別 | 職責描述 |
JittableCompareMultiColumn | API | 判斷sort node節點是否支持LLVM |
CompareMultiColumnCodeGen | API | 為CompareMultiColumn函式生成LLVM函式 |
CompareMultiColumnCodeGen_TOPN | API | 在Top N sort場景下為CompareMultiColumn函式生成LLVM函式 |
bpcharcmpCodeGen_long(short) | API | 為bpcharcmp函式生成LLVM函式 |
LLVMIRmemcmp_CMC_CodeGen | API | 為memcmp函式生成LLVM函式 |
textcmpCodeGen | API | 為text_cmp函式生成LLVM函式 |
numericcmpCodeGen | API | 為numeric_cmp函式生成LLVM函式 |
JittableSortAggMatchKey | API | 判斷sort aggregation中match_key函式是否支持LLVM |
SortAggMatchKeyCodeGen | API | 為sort aggregation中match_key函式生成LLVM函式 |
SortAggBpchareqCodeGen | API | 為Bpchareq函式生成LLVM函式 |
SortAggMemcmpCodeGen_long(short) | API | match_key中為memcmp函式生成LLVM函式 |
if (consider_codegen) {
/* 根據行數判斷是否使用codegen,如果使用則開始codegen */
jitted_comparecol = dorado::VecSortCodeGen::CompareMultiColumnCodeGen(sort_stat, use_prefetch); /* 為sort操作進行codegen */
if (jitted_comparecol != NULL) {
/* 如果生成了llvm函式則加到MCJIT LIST中 */
llvm_codegen->addFunctionToMCJit(jitted_comparecol, reinterpret_cast<void**>(&(sort_stat->jitted_CompareMultiColumn)));
}
Plan* plan_tree = estate->es_plannedstmt->planTree;
/* 如果sort節點包含“limit”父節點則繼續呼叫相應codegen函式 */
bool has_topn = MatchLimitNode(node, plan_tree);
if (has_topn && (jitted_comparecol != NULL)) {
jitted_comparecol_topn= dorado::VecSortCodeGen::CompareMultiColumnCodeGen_TOPN(sort_stat,
use_prefetch);
if (jitted_comparecol_topn != NULL) {
llvm_codegen->addFunctionToMCJit(jitted_comparecol_topn, reinterpret_cast<void**>(&(sort_stat->jitted_CompareMultiColumn_TOPN)));
}
}
}
在呼叫時,與其他類一樣,首先判斷節點是否LLVM化,沒有LLVM化則進行非codegen的處理,
if (jitted_CompareMultiColumn) /* 如果有codegen則使用jit */
compareMultiColumn = ((LLVM_CMC_func)(jitted_CompareMultiColumn));
else
compareMultiColumn = CompareMultiColumn<false>;
感謝大家學習第七章執行器決議中“7.4 運算式計算”及“7.5 編譯執行”的精彩內容,下一篇我們開啟“7.6 向量化引擎”、“7.7 小結”的相關內容的介紹,
敬請期待,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/295138.html
標籤:其他
下一篇:Vue從開發到部署
