前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Postgresql源码(130)ExecInterpExpr转换为IR的流程

Postgresql源码(130)ExecInterpExpr转换为IR的流程

作者头像
mingjie
发布2024-05-24 18:06:53
670
发布2024-05-24 18:06:53
举报
文章被收录于专栏:Postgresql源码分析

表达式计算在之前做过很多相关的分析了,本篇主要关注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
  • 非递归实现减少了栈的使用和开销。
  • 简单子表达式通过单一跳转实现,无需函数调用。
  • 在不同子表达式之间共享一些状态。
  • 通过顺序排列操作元数据,减少了间接/难以预测的内存访问;包括避免了几乎所有之前使用的链表 更多的代码已经移动到表达式初始化阶段,避免了在评估时的不断重新检查。

在我看来还有几点也比较重要

  1. 减少分支预测失败:处理器使用分支预测来猜测程序的控制流路径。switch可能会导致分支预测失败,特别是当有大量的标签时。goto 可以减少分支预测的复杂性,因为控制流更直接。
  2. 更高的指令缓存效率:连续goto应该更容易被处理器的指令缓存。比如跳转的比较近的时候,局部指令可能都在缓存中。而且switch的指令数比goto要多一些。
  3. 代码生成优化:编译器看到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的步骤:

代码语言:javascript
复制
(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:

代码语言:javascript
复制
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。

代码语言:javascript
复制
bool
llvm_compile_expr(ExprState *state)
{
	...
  1. 在context中拿到module,用来存放function
  2. 在context中创建一个builder,用来构造后面的function内容
代码语言:javascript
复制
	mod = llvm_mutable_module(context);
	lc = LLVMGetModuleContext(mod);
	b = LLVMCreateBuilderInContext(lc);
  1. 创建函数evalexpr,llvm_pg_var_func_type用来拿到ExecInterpExprStillValid函数的入参(ExprState *state, ExprContext *econtext, bool *isNull)
  2. 增加编译选项LLVMExternalLinkage,指定当前函数可以被其他编译单元看到,所以在link时其他编译单元可以直接使用这里的代码,类似于extern函数。
  3. LLVMAddFunction在mod中增加了一个函数声明evalexpr。
  4. llvm_copy_attributes的功能见《Postgresql源码(129)JIT函数中如何使用PG的类型llvmjit_types》
代码语言:javascript
复制
	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,函数定义开始了:
代码语言:javascript
复制
	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调用的角度等价与:
在这里插入图片描述
在这里插入图片描述
代码语言:javascript
复制
	v_scanslot = l_load_struct_gep(b,
								   StructExprContext,
								   v_econtext,
								   FIELDNO_EXPRCONTEXT_SCANTUPLE,
								   "v_scanslot");
	...
  • 这里为每一个step创建了一个Block。
在这里插入图片描述
在这里插入图片描述
代码语言:javascript
复制
	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。
代码语言:javascript
复制
	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流程:

  1. 从econtext中拿到tts赋给scanslot。
  2. 走EEOP_SCAN_FETCHSOME分支计算econtext。
代码语言:javascript
复制
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流程:

代码语言:javascript
复制
	/*
	 * 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 中的代码
代码语言:javascript
复制
		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中增加代码:
代码语言:javascript
复制
		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);
代码语言:javascript
复制
/*
 * 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执行。
代码语言:javascript
复制
		LLVMBuildBr(b, opblocks[opno + 1]);
		break;
	}
2.3 EEOP_SCAN_VAR计算
在这里插入图片描述
在这里插入图片描述
2.4 EEOP_FUNCEXPR_STRICT计算
在这里插入图片描述
在这里插入图片描述
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-05-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 为什么PG要重构表达式计算逻辑,会带来哪些提升?
  • 2 生成JIT表达式llvm_compile_expr逻辑分析
    • 2.1 计算准备
      • 2.2 EEOP_SCAN_FETCHSOME计算
        • EEOP_SCAN_FETCHSOME原函数
        • EEOP_SCAN_FETCHSOME IR构造
      • 2.3 EEOP_SCAN_VAR计算
        • 2.4 EEOP_FUNCEXPR_STRICT计算
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档