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

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

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

  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的步骤:

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)
{
	...
  1. 在context中拿到module,用来存放function
  2. 在context中创建一个builder,用来构造后面的function内容
c 复制代码
	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》
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流程:

  1. 从econtext中拿到tts赋给scanslot。
  2. 走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;
	}

2.3 EEOP_SCAN_VAR计算

2.4 EEOP_FUNCEXPR_STRICT计算

相关推荐
CoderIsArt1 小时前
Redis的三种模式:主从模式,哨兵与集群模式
数据库·redis·缓存
师太,答应老衲吧3 小时前
SQL实战训练之,力扣:2020. 无流量的帐户数(递归)
数据库·sql·leetcode
Channing Lewis4 小时前
salesforce case可以新建一个roll up 字段,统计出这个case下的email数量吗
数据库·salesforce
毕业设计制作和分享5 小时前
ssm《数据库系统原理》课程平台的设计与实现+vue
前端·数据库·vue.js·oracle·mybatis
ketil275 小时前
Redis - String 字符串
数据库·redis·缓存
Hsu_kk6 小时前
MySQL 批量删除海量数据的几种方法
数据库·mysql
编程学无止境6 小时前
第02章 MySQL环境搭建
数据库·mysql
knight-n6 小时前
MYSQL库的操作
数据库·mysql
包饭厅咸鱼7 小时前
QML----复制指定下标的ListModel数据
开发语言·数据库
生命几十年3万天7 小时前
redis时间优化
数据库·redis·缓存