相关
《Postgresql源码(127)投影ExecProject的表达式执行分析》
《Postgresql源码(128)深入分析JIT中的函数内联llvm_inline》
《Postgresql源码(129)JIT函数中如何使用PG的类型llvmjit_types》
表达式计算在之前做过很多相关的分析了,本篇主要关注ExecInterpExpr如何转换为IR。
PG的表达式计算方法在7年前有一次重构,一方面带来了很大的性能提升,一方面为JIT做准备。
1 为什么PG要重构表达式计算逻辑,会带来哪些提升?
重构在这个提交:b8d7f053c5c2bf2a7e8734fe3327f6a8bc711755
先看下原来的表达式计算长什么样子(左侧)
原来的表达式计算根据node类型不同配置了大量处理函数,运行时按类型走自己的evalfunc,比如Const类型就会走ExecEvalConst函数计算。
而优化后大部分类型的evalfunc都只使用一个函数:ExecInterpExpr。且在ExecInterpExpr中使用GOTO替换了应该有的大switch逻辑。
这样做对性能会有比较大的提升的原因from commit message
:
- non-recursive implementation reduces stack usage / overhead
- simple sub-expressions are implemented with a single jump, without
function calls- sharing some state between different sub-expressions
- reduced amount of indirect/hard to predict memory accesses by laying out operation metadata sequentially; including the avoidance of nearly all of the previously used linked lists
- more code has been moved to expression initialization, avoiding constant re-checks at evaluation time
- 非递归实现减少了栈的使用和开销。
- 简单子表达式通过单一跳转实现,无需函数调用。
- 在不同子表达式之间共享一些状态。
- 通过顺序排列操作元数据,减少了间接/难以预测的内存访问;包括避免了几乎所有之前使用的链表
更多的代码已经移动到表达式初始化阶段,避免了在评估时的不断重新检查。
在我看来还有几点也比较重要
- 减少分支预测失败:处理器使用分支预测来猜测程序的控制流路径。switch可能会导致分支预测失败,特别是当有大量的标签时。goto 可以减少分支预测的复杂性,因为控制流更直接。
- 更高的指令缓存效率:连续goto应该更容易被处理器的指令缓存。比如跳转的比较近的时候,局部指令可能都在缓存中。而且switch的指令数比goto要多一些。
- 代码生成优化:编译器看到goto能做出更多的优化,为后续的JIT实现做准备。
2 生成JIT表达式llvm_compile_expr逻辑分析
还是参考这篇中的例子:《Postgresql源码(128)深入分析JIT中的函数内联llvm_inline》
select abs(k),abs(k),abs(k),abs(k),abs(k),exp(k),exp(k),exp(k),exp(k),exp(k) from t1;
表达式计算投影列时,ExecInterpExpr的步骤:
(gdb) p/x state->steps[0]->opcode
$15 = 0x74b0ad EEOP_SCAN_FETCHSOME:取一行
(gdb) p/x state->steps[1]->opcode
$16 = 0x74b1ec EEOP_SCAN_VAR:拿到目标列
(gdb) p/x state->steps[2]->opcode
$17 = 0x74b784 EEOP_FUNCEXPR_STRICT:函数计算
(gdb) p/x state->steps[3]->opcode
$18 = 0x74b591 EEOP_ASSIGN_TMP:暂存结果
(gdb) p/x state->steps[4]->opcode
$19 = 0x74b1ec EEOP_SCAN_VAR:拿到目标列
(gdb) p/x state->steps[5]->opcode
$20 = 0x74b784 EEOP_FUNCEXPR_STRICT:函数计算
(gdb) p/x state->steps[6]->opcode
$21 = 0x74b591 EEOP_ASSIGN_TMP:暂存结果
(gdb) p/x state->steps[7]->opcode
$22 = 0x74b1ec EEOP_SCAN_VAR:拿到目标列
...
...
(gdb) p/x state->steps[34]->opcode
$27 = 0x74b784 EEOP_FUNCEXPR_STRICT:函数计算
(gdb) p/x state->steps[35]->opcode
$28 = 0x74b591 EEOP_ASSIGN_TMP:暂存结果
(gdb) p/x state->steps[36]->opcode
$29 = 0x74b01a EEOP_DONE:计算结束
2.1 计算准备
原函数ExecInterpExpr:
static Datum
ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
{ExprEvalStep *op;TupleTableSlot *resultslot;TupleTableSlot *innerslot;TupleTableSlot *outerslot;TupleTableSlot *scanslot;op = state->steps;resultslot = state->resultslot;innerslot = econtext->ecxt_innertuple;outerslot = econtext->ecxt_outertuple;scanslot = econtext->ecxt_scantuple;EEO_DISPATCH();{EEO_CASE(EEOP_DONE){goto out;}EEO_CASE(EEOP_INNER_FETCHSOME){...EEO_NEXT();}EEO_CASE(EEOP_OUTER_FETCHSOME){...EEO_NEXT();}...
out:*isnull = state->resnull;return state->resvalue;
}
原函数的逻辑可以简单理解为循环执行steps,每一个steps走大switch的一个分支(这里已经优化成了goto)。
注意原函数是执行,到jit逻辑中,这里的执行变成了→BUILD IR。
bool
llvm_compile_expr(ExprState *state)
{...
- 在context中拿到module,用来存放function
- 在context中创建一个builder,用来构造后面的function内容
mod = llvm_mutable_module(context);lc = LLVMGetModuleContext(mod);b = LLVMCreateBuilderInContext(lc);
- 创建函数evalexpr,llvm_pg_var_func_type用来拿到ExecInterpExprStillValid函数的入参
(ExprState *state, ExprContext *econtext, bool *isNull)
。 - 增加编译选项LLVMExternalLinkage,指定当前函数可以被其他编译单元看到,所以在link时其他编译单元可以直接使用这里的代码,类似于extern函数。
- LLVMAddFunction在mod中增加了一个函数声明evalexpr。
- llvm_copy_attributes的功能见《Postgresql源码(129)JIT函数中如何使用PG的类型llvmjit_types》
funcname = llvm_expand_funcname(context, "evalexpr");eval_fn = LLVMAddFunction(mod, funcname,llvm_pg_var_func_type("ExecInterpExprStillValid"));LLVMSetLinkage(eval_fn, LLVMExternalLinkage);LLVMSetVisibility(eval_fn, LLVMDefaultVisibility);llvm_copy_attributes(AttributeTemplate, eval_fn);
- 这里给函数增加了第一个Block,函数定义开始了:
entry = LLVMAppendBasicBlockInContext(lc, eval_fn, "entry");/* build state */v_state = LLVMGetParam(eval_fn, 0);v_econtext = LLVMGetParam(eval_fn, 1);v_isnullp = LLVMGetParam(eval_fn, 2);LLVMPositionBuilderAtEnd(b, entry);
- 下面执行的操作等价与scanslot = econtext->ecxt_scantuple;从结构体中拿一个成员变量的值。
- IR中的结构体是不会记录成员名称的,所以需要告知llvm成员变量在结构体中的偏移位置FIELDNO_EXPRCONTEXT_SCANTUPLE = 1。
- LLVMBuildLoad从内存中加载值。
- LLVMStructGetTypeAtIndex拿到结构体指定位置的类型。
- LLVMBuildStructGEP拿到结构体1位置的成员地址(GEP=GetElementPtr)
- 从API调用的角度等价与:
v_scanslot = l_load_struct_gep(b,StructExprContext,v_econtext,FIELDNO_EXPRCONTEXT_SCANTUPLE,"v_scanslot");...
- 这里为每一个step创建了一个Block。
opblocks = palloc(sizeof(LLVMBasicBlockRef) * state->steps_len);for (int opno = 0; opno < state->steps_len; opno++)opblocks[opno] = l_bb_append_v(eval_fn, "b.op.%d.start", opno);
- 将builder位置调整到第一个block中,开始build。
LLVMBuildBr(b, opblocks[0]);for (int opno = 0; opno < state->steps_len; opno++){...}LLVMDisposeBuilder(b);
2.2 EEOP_SCAN_FETCHSOME计算
EEOP_SCAN_FETCHSOME原函数
非JIT表达式计算EEOP_SCAN_FETCHSOME流程:
- 从econtext中拿到tts赋给scanslot。
- 走EEOP_SCAN_FETCHSOME分支计算econtext。
ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)TupleTableSlot *scanslot;...scanslot = econtext->ecxt_scantuple;...EEO_SWITCH(){...EEO_CASE(EEOP_SCAN_FETCHSOME){...slot_getsomeattrs(scanslot, op->d.fetch.last_var);EEO_NEXT();}...}
EEOP_SCAN_FETCHSOME IR构造
JIT表达式计算EEOP_SCAN_FETCHSOME流程:
/** l_load_struct_gep = * * LLVMBuildLoad(b,* LLVMStructGetTypeAtIndex(StructExprContext, 1),* LLVMBuildStructGEP(b, StructExprContext, v_econtext, 1, "")* "v_scanslot")*/v_scanslot = l_load_struct_gep(b,StructExprContext,v_econtext,FIELDNO_EXPRCONTEXT_SCANTUPLE,"v_scanslot");...case EEOP_SCAN_FETCHSOME:{TupleDesc desc = NULL;LLVMValueRef v_slot;LLVMBasicBlockRef b_fetch;LLVMValueRef v_nvalid;LLVMValueRef l_jit_deform = NULL;const TupleTableSlotOps *tts_ops = NULL;
- 前面已经为每一个case都创建了一个BasicBlock。
- l_bb_before_v在当前switch的BasicBlock前增加了一个新的Block。
- 新的Block的语义:
if (v_nvalid >= op->d.fetch.last_var) // 跳转到下一个case的Block:opblocks[opno + 1]
else // 继续执行 当前Block 中的代码
b_fetch = l_bb_before_v(opblocks[opno + 1],"op.%d.fetch", opno);v_slot = v_scanslot;v_nvalid =l_load_struct_gep(b,StructTupleTableSlot,v_slot,FIELDNO_TUPLETABLESLOT_NVALID,"");LLVMBuildCondBr(b,LLVMBuildICmp(b, LLVMIntUGE, v_nvalid,l_int16_const(lc, op->d.fetch.last_var),""),opblocks[opno + 1], b_fetch);
- 将builder的插入点调整到b_fetch块的末尾,继续在b_fetch中增加代码:
LLVMPositionBuilderAtEnd(b, b_fetch);{LLVMValueRef params[2];params[0] = v_slot;params[1] = l_int32_const(lc, op->d.fetch.last_var);
- 创建一个调用指令,等价与
slot_getsomeattrs(scanslot, op->d.fetch.last_var);
/** API调用:* LLVMBuildCall2(* b, * LLVMGetFunctionType(LLVMGetNamedFunction(llvm_types_module, "slot_getsomeattrs_int")), * LLVMAddFunction(mod, "slot_getsomeattrs_int", LLVMGetFunctionType(LLVMGetNamedFunction(llvm_types_module, "slot_getsomeattrs_int"))), * params, * 2,* "");*/l_call(b,llvm_pg_var_func_type("slot_getsomeattrs_int"),llvm_pg_func(mod, "slot_getsomeattrs_int"),params, lengthof(params), "");}
- 继续到下一个Block执行。
LLVMBuildBr(b, opblocks[opno + 1]);break;}