openGauss源码解析(139)
openGauss源码解析:执行器解析(32)
举例来说,以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函数的具体的设计和实现都将依赖于本节所介绍的基本框架。