openGauss数据库源码解析系列文章—— 执行器解析(二)_param_extern param_exec param_sublink

现在能在网上找到很多很多的学习资源,有免费的也有收费的,当我拿到1套比较全的学习资源之前,我并没着急去看第1节,我而是去审视这套资源是否值得学习,有时候也会去问一些学长的意见,如果可以之后,我会对这套学习资源做1个学习计划,我的学习计划主要包括规划图和学习进度表。

分享给大家这份我薅到的免费视频资料,质量还不错,大家可以跟着学习

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!


ExecQual函数的主要执行流程如下。  
 (1) 遍历qual中的子表达式,根据ExecEvalExpr函数计算结果是否满足该子表达式,若满足则expr\_value为1,否则为0。  
 (2) 判断结果是否为空,若为空,则根据resultForNull参数得到返回值信息。若不为空,则根据expr\_value判断返回true或者false。  
 (3) 返回result。  
 ExecQual函数的执行流程如图7-17所示。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/cee0b62d4da84061987276a9cf1068e7.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0dhdXNzREI=,size_16,color_FFFFFF,t_70#pic_center)



 图7-17 ExecQual函数执行流程 

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所示。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/77ee2039f12b47a980c80dec5170b2d3.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0dhdXNzREI=,size_16,color_FFFFFF,t_70#pic_center)



 图7-18 ExecEvalOr函数执行流程 

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所示。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/2eee6cc7d7424c5a8ac6b03275eccac1.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0dhdXNzREI=,size_16,color_FFFFFF,t_70#pic_center)



 图7-19 ExecTargetList函数执行流程 

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所示。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/46c1ebc9ab2a4863a73f7a4575f04cc2.png#pic_center)



 图7-20 ExecProject函数执行流程 

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所示。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/ef7faaae6b294bd19eb08323d53465b6.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0dhdXNzREI=,size_16,color_FFFFFF,t_70#pic_center)



 图7-21 ExecEvalParamExec函数执行流程 

ExecEvalParamExtern函数的作用是获取并返回PARAM\_EXTERN类型的参数。该参数是指外部传入参数,例如在PBE执行时,PREPARE的语句中的参数,在需要execute语句执行时传入。核心代码如下:



if (paramInfo && thisParamId > 0 && thisParamId <= paramInfo->numParams) {/* 判断参数 /
ParamExternData
prm = &paramInfo->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所示。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/108a90d7a0304658b46c189ccc126025.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0dhdXNzREI=,size_16,color_FFFFFF,t_70#pic_center)



 图7-22 ExecEvalParamExtern函数执行流程 

## 7.5 编译执行


为了提高SQL的执行速度,解决传统数据处理引擎条件逻辑冗余的问题,openGauss为执行表达式引入了CodeGen技术,其核心思想是为具体的查询生成定制化的机器码代替通用的函数实现,并尽可能地将数据存储在CPU寄存器中。openGauss通过LLVM编译框架来实现CodeGen,LLVM是“Low Level Virtual Machine”的缩写,开发之初是想作为一个底层虚拟机,但随着开发,以及功能的逐渐完善,慢慢变成一个模块化的编译系统,并能支持多种语言。LLVM的系统架构如图7-23所示。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/08a17a8f7f3144f396e63f0378857c69.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0dhdXNzREI=,size_16,color_FFFFFF,t_70#pic_center)



 图7-23 LLVM系统架构 

LLVM大体上可以分成3个部分。  
 (1) 支持多种语言的前端。  
 (2) 优化器。  
 (3) 支持多种CPU架构的后端(X86、Aarch64)。  
 LLVM与GCC一样,都是常用的编译系统,但是LLVM更加模块化,从而可以免去每使用一套语言换一套优化器的工作,开发者只要设计相应的前端,并针对各个目标平台做后端优化。  
 考虑如下SQL语句。



SELECT * FROM dataTable WHRER (x + 2) * 3 > 4;


正常的递归流程如图7-24所示。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/a24ebf92c2454d05a889fd1b6b7029ea.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0dhdXNzREI=,size_16,color_FFFFFF,t_70#pic_center)



 图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所示。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/5eaff1ceaca34f62b572ca1531a51829.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0dhdXNzREI=,size_16,color_FFFFFF,t_70#pic_center)



 图7-25 openGauss CodeGen编译执行流程 

数据库启动后,首先对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所示。



 表7-31 GsCodeGen接口汇总 
 
 

| 接口名称 | 接口类型 | 职责描述 |
| --- | --- | --- |
| 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所示:



 表7-32 ForeigenScanCodeGen接口汇总 




| 接口名称 | 接口类型 | 职责描述 |
| --- | --- | --- |
| 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的所有接口。



 表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
;


上述的IR函数经过编译后就可以直接在执行阶段被调用。从而提升执行效率。而后续OLAP-LLVM层的代码设计都基于上述的基本数据结构,数据类型和BasicBlock控制流结构。一个完整的生成IR函数的构建代码结构如下:



llvm::Function *func(InputArg[计划树信息])
{
定义数据类型,变量值;
申明动态参数[带有实际数据的参数];
控制流主体;
返回结果值。
}


因此后续单个LLVM函数的具体的设计和实现都将依赖于本节所介绍的基本框架。  
 7.5.2 VecHashAggCodeGen类  
 对于hash聚合来说,数据库会根据“GROUP BY”字段后面的值算出哈希值,并根据前面使用的聚合函数在内存中维护对应的列表。VecHashAggCodeGen类的接口实现在“codegen/vechashaggcodegen.h”文件中,接口的说明如表7-34所示。



 表7-34 VecHashAggCodeGen接口汇总 
 
 

|  |  |  |
| --- | --- | --- |
| 接口名称 | 接口类型 | 职责描述 |
| 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所示。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/8cc8e5ab22704d2d82e2b8bcaa031b06.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0dhdXNzREI=,size_16,color_FFFFFF,t_70#pic_center)



 图7-26 HashAgg节点CodeGen流程 

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所示。



 表7-35 VecHashAggCodeGen接口汇总 
 
 

| 接口名称 | 接口类型 | 职责描述 |
| --- | --- | --- |
| 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所示。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/cd805377b2724d7d8fab2389b24cf81b.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0dhdXNzREI=,size_16,color_FFFFFF,t_70#pic_center)



 图7-27 HashJoinCodeGen函数执行流程 

图中所有CodeGen函数返回都是“LLVM::Function”类型的IR函数指针,其中值得注意的是当enable\_fast\_keyMatch的值等于0时,是正常的“key match”;等于2时,所有key值类型都是int4或者int8并且不为NULL,这时候可使用更少的内存和更少的分支,所以叫作“fast path”。


### 7.5.4 VecSortCodeGen类


学好 Python 不论是就业还是做副业赚钱都不错,但要学会 Python 还是要有一个学习规划。最后大家分享一份全套的 Python 学习资料,给那些想学习 Python 的小伙伴们一点帮助!



### 一、Python所有方向的学习路线



Python所有方向路线就是把Python常用的技术点做整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。



![](https://img-blog.csdnimg.cn/img_convert/9f49b566129f47b8a67243c1008edf79.png)



### 二、学习软件

工欲善其事必先利其器。学习Python常用的开发软件都在这里了,给大家节省了很多时间。



![](https://img-blog.csdnimg.cn/img_convert/8c4513c1a906b72cbf93031e6781512b.png)



### 三、全套PDF电子书



书籍的好处就在于权威和体系健全,刚开始学习的时候你可以只看视频或者听某个人讲课,但等你学完之后,你觉得你掌握了,这时候建议还是得去看一下书籍,看权威技术书籍也是每个程序员必经之路。

![](https://img-blog.csdnimg.cn/img_convert/46506ae54be168b93cf63939786134ca.png)



### 四、入门学习视频

我们在看视频学习的时候,不能光动眼动脑不动手,比较科学的学习方法是在理解之后运用它们,这时候练手项目就很适合了。



![](https://img-blog.csdnimg.cn/afc935d834c5452090670f48eda180e0.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA56iL5bqP5aqb56eD56eD,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center)



### 五、实战案例



光学理论是没用的,要学会跟着一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。



![](https://img-blog.csdnimg.cn/img_convert/252731a671c1fb70aad5355a2c5eeff0.png)



### 六、面试资料

我们学习Python必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有阿里大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。



![](https://img-blog.csdnimg.cn/img_convert/6c361282296f86381401c05e862fe4e9.png)  

![](https://img-blog.csdnimg.cn/img_convert/d2d978bb523c810abca3abe69e09bc1a.png)




**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化学习资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618317507)**

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值