相关
《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的步骤:
c
(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:
c
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。
c
bool
llvm_compile_expr(ExprState *state)
{
...
- 在context中拿到module,用来存放function
- 在context中创建一个builder,用来构造后面的function内容
c
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》
c
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,函数定义开始了:
c
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调用的角度等价与:
c
v_scanslot = l_load_struct_gep(b,
StructExprContext,
v_econtext,
FIELDNO_EXPRCONTEXT_SCANTUPLE,
"v_scanslot");
...
- 这里为每一个step创建了一个Block。
c
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。
c
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。
c
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流程:
c
/*
* 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 中的代码
c
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中增加代码:
c
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);
c
/*
* 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执行。
c
LLVMBuildBr(b, opblocks[opno + 1]);
break;
}