声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 OpenGauss5.1.0 的开源代码和《OpenGauss数据库源码解析》一书
ExecInitVecGroup 函数
ExecInitVecGroup 函数是一个用于初始化向量化分组操作节点的函数。在执行查询计划的过程中,该函数负责创建和配置 VecGroupState 结构,设置向量化执行的相关属性,并初始化表达式上下文和元组槽。它还处理子节点的初始化、目标列表和资格表达式的向量化,以及准备相等比较函数。通过这些步骤,该函数为后续的分组聚合操作建立了基础,确保能够高效地处理数据分组和聚合的计算。整体上,这个函数是向量化分组操作执行的准备阶段,确保在执行过程中能够有效管理状态和资源。函数源码如下所示:(路径:src\gausskernel\runtime\vecexecutor\vecnode\vecgroup.cpp
)
VecGroupState* ExecInitVecGroup(VecGroup* node, EState* estate, int eflags)
{
VecGroupState* grp_state = NULL; // 声明一个指向 VecGroupState 的指针,用于保存聚合状态
ScalarDesc unknown_desc; // 未使用的标量描述符
// 检查不支持的标志
Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));
// 创建状态结构
grp_state = makeNode(VecGroupState); // 创建 VecGroupState 节点
grp_state->ss.ps.vectorized = true; // 设置为向量化执行
grp_state->ss.ps.plan = (Plan*)node; // 将当前节点赋值给计划属性
grp_state->ss.ps.state = estate; // 将执行状态赋值
grp_state->grp_done = false; // 初始化完成状态为 false
// 创建表达式上下文
ExecAssignExprContext(estate, &grp_state->ss.ps); // 分配表达式上下文
// 初始化元组表
ExecInitResultTupleSlot(estate, &grp_state->ss.ps); // 初始化结果元组槽
// 初始化子表达式
grp_state->ss.ps.targetlist = (List*)ExecInitVecExpr((Expr*)node->plan.targetlist, (PlanState*)grp_state); // 初始化目标列表
grp_state->ss.ps.qual = (List*)ExecInitVecExpr((Expr*)node->plan.qual, (PlanState*)grp_state); // 初始化资格表达式
#ifdef ENABLE_LLVM_COMPILE
/*
* 检查 nlstate->js.joinqual 和 nlstate->js.ps.qual 表达式列表是否可以代码生成。
*/
llvm::Function* grp_vecqual = NULL; // 声明一个 LLVM 函数指针
dorado::GsCodeGen* llvm_code_gen = (dorado::GsCodeGen*)t_thrd.codegen_cxt.thr_codegen_obj; // 获取 LLVM 代码生成器对象
bool consider_codegen =
CodeGenThreadObjectReady() && // 确保代码生成线程对象准备好
CodeGenPassThreshold(((Plan*)node)->plan_rows, estate->es_plannedstmt->num_nodes, ((Plan*)node)->dop); // 检查代码生成阈值
if (consider_codegen) {
grp_vecqual = dorado::VecExprCodeGen::QualCodeGen((List*)grp_state->ss.ps.qual, (PlanState*)grp_state); // 生成资格代码
if (grp_vecqual != NULL)
llvm_code_gen->addFunctionToMCJit(grp_vecqual, reinterpret_cast<void**>(&(grp_state->jitted_vecqual))); // 添加到 JIT 编译器
}
#endif
// 初始化子节点
outerPlanState(grp_state) = ExecInitNode(outerPlan(node), estate, eflags); // 初始化外部计划
/*
* 初始化结果元组类型和投影信息。
* 分组节点的结果元组槽总是保存虚拟元组,因此默认表访问方法类型设置为 HEAP。
*/
ExecAssignResultTypeFromTL(&grp_state->ss.ps); // 从目标列表中分配结果类型
// 为表达式评估分配向量
grp_state->ss.ps.ps_ProjInfo = ExecBuildVecProjectionInfo(grp_state->ss.ps.targetlist,
node->plan.qual,
grp_state->ss.ps.ps_ExprContext,
grp_state->ss.ps.ps_ResultTupleSlot,
NULL); // 构建投影信息
ExecAssignVectorForExprEval(grp_state->ss.ps.ps_ExprContext); // 为表达式评估分配向量
// 预计算用于内循环的 fmgr 查找数据
grp_state->eqfunctions = execTuplesMatchPrepare(node->numCols, node->grpOperators); // 准备相等比较函数
// 初始化向量执行过程中的变量并绑定 FmgrInfo 中的函数
grp_state->cap = (void*)palloc(sizeof(Encap)); // 分配内存用于封装
InitGrpUniq<VecGroupState>(grp_state, node->numCols, node->grpColIdx); // 初始化唯一分组状态
return grp_state; // 返回聚合状态
}
ExecVecGroup 函数
ExecVecGroup 函数用于执行向量化的分组操作。该函数首先检查分组是否已经完成,然后通过循环不断从外部计划获取数据批次,并将其存储在分组容器中。每次获取批次后,它会调用相应的函数将数据填充到容器中。当容器满时,会将其内容转储到扫描批次中,并通过调整索引来管理剩余数据。最后,当没有更多数据可处理时,函数会处理未满的容器内容,并生成最终的结果批次。总体而言,该函数实现了高效的批处理分组功能,确保在向量化环境中可以快速和高效地处理数据分组和聚合的计算。函数源码如下所示:(路径:src\gausskernel\runtime\vecexecutor\vecnode\vecgroup.cpp
)
VectorBatch* ExecVecGroup(VecGroupState* node)
{
void** grp_container = node->container; // 获取分组容器,用于存储当前组的信息
uint16 i = 0; // 初始化循环索引
uint16* idx = &(node->idx); // 获取当前索引,表示分组容器中的有效元素个数
VectorBatch* res_batch = NULL; // 初始化结果批次
Encap* cap = (Encap*)node->cap; // 获取封装对象,用于传递批次和函数参数
cap->eqfunctions = node->eqfunctions; // 设置相等比较函数
errno_t rc; // 错误码
if (node->grp_done) // 如果分组操作已经完成,则返回 NULL
return NULL;
node->bckBuf->Reset(); // 重置备份缓冲区
node->scanBatch->Reset(true); // 重置扫描批次
for (;;) { // 开始一个无限循环
// 获取下一个批次,如果为 NULL 则跳出循环
VectorBatch* batch = VectorEngine(outerPlanState(node)); // 从外部计划中获取向量批次
if (unlikely(BatchIsNull(batch))) // 检查批次是否为空
break; // 如果为空,退出循环
// 调用 buildFunc 函数,将批次读入 grp_container,使用 idx 作为当前插入位置
cap->batch = batch; // 将当前批次赋值给封装对象
(void)FunctionCall2(node->buildFunc, PointerGetDatum(node), PointerGetDatum(cap)); // 调用函数将数据填充到 grp_container
// 当 grp_container 满时,调用 buildScanFunc 将 BatchMaxSize 个单元从 grp_container 转储到 scanBatch
// 用于生成外部批次,然后调整 idx,从剩余单元开始重新计数,并重置尾部单元
if (*idx >= BatchMaxSize) { // 如果索引超过最大批次大小
cap->batch = node->scanBatch; // 将当前批次指向扫描批次
for (i = 0; i < BatchMaxSize; i++) { // 循环处理每个单元
(void)FunctionCall2(node->buildScanFunc, PointerGetDatum(grp_container[i]), PointerGetDatum(cap)); // 调用函数转储数据
}
uint16 remain_grp = *idx - BatchMaxSize + 1; // 计算剩余的组
for (i = 0; i < remain_grp; i++) { // 循环调整 grp_container
grp_container[i] = grp_container[BatchMaxSize + i]; // 将剩余单元移至前面
}
// 将 grp_container 的尾部重置为 NULL
rc = memset_s(&grp_container[remain_grp],
sizeof(GUCell*) * (2 * BatchMaxSize - remain_grp),
0,
sizeof(GUCell*) * (2 * BatchMaxSize - remain_grp)); // 清零尾部
securec_check(rc, "\0", "\0"); // 检查安全性
*idx = remain_grp - 1; // 更新索引
res_batch = ProduceBatch(node); // 生成结果批次
if (unlikely(BatchIsNull(res_batch))) { // 如果结果批次为空
// 释放空间
node->bckBuf->Reset(); // 重置备份缓冲区
// 重置批次
node->scanBatch->Reset(true); // 重置扫描批次
continue; // 继续循环
} else
return res_batch; // 返回结果批次
}
}
// 如果没有数据
if (0 == *idx && NULL == grp_container[0]) { // 如果索引为 0 且容器第一个元素为 NULL
return NULL; // 返回 NULL
}
// 处理剩余的 grp_container 数据
cap->batch = node->scanBatch; // 将批次指向扫描批次
for (i = 0; i <= *idx; i++) { // 遍历 grp_container
(void)FunctionCall2(node->buildScanFunc, PointerGetDatum(grp_container[i]), PointerGetDatum(cap)); // 调用函数处理每个单元
}
node->grp_done = true; // 设置分组完成状态为 true
return ProduceBatch(node); // 返回最终结果批次
}
ProduceBatch 函数
ProduceBatch 函数的主要作用是生成一个包含符合条件的结果的向量批次。函数首先检查当前的扫描批次是否为空,如果为空,则返回 NULL。接着,它重置表达式上下文并设置相关的批次信息。然后,如果存在质检条件(qual),函数会检查是否可以使用 LLVM 优化来生成代码。如果可以,它将调用 JIT 编译的函数来获取符合条件的标量向量;如果不可以,则使用普通的执行函数进行处理。如果获取的标量向量为空,函数将返回 NULL。最后,函数应用选择器并调用投影函数生成最终的结果批次。总体来说,ProduceBatch 函数通过结合选择和投影逻辑,实现了从数据批次中提取符合条件的数据的功能。函数源码如下所示:(路径:src\gausskernel\runtime\vecexecutor\vecnode\vecgroup.cpp
)
static VectorBatch* ProduceBatch(VecGroupState* node)
{
ExprContext* expr_context = NULL; // 声明表达式上下文指针
VectorBatch* p_res = NULL; // 声明结果批次指针
VectorBatch* batch = node->scanBatch; // 获取当前的扫描批次
// 检查是否有输入行,如果没有则返回 NULL
if (batch == NULL)
return NULL; // 如果批次为空,直接返回 NULL
expr_context = node->ss.ps.ps_ExprContext; // 获取当前节点的表达式上下文
ResetExprContext(expr_context); // 重置表达式上下文,以便重新使用
expr_context->ecxt_scanbatch = batch; // 设置扫描批次
expr_context->ecxt_outerbatch = batch; // 设置外部批次为当前扫描批次
// 如果有质检条件
if (list_length((List*)node->ss.ps.qual) != 0) {
ScalarVector* p_vector = NULL; // 声明标量向量指针
/*
* 如果 grp_state->node->ss.ps.qual 可以使用 LLVM 优化生成代码,则先使用 LLVM 优化
* (成本模型已经考虑过)
*/
if (node->jitted_vecqual) {
p_vector = node->jitted_vecqual(expr_context); // 使用 JIT 编译的函数获取标量向量
if (HAS_INSTR(&node->ss, false)) { // 检查是否需要记录指标
node->ss.ps.instrument->isLlvmOpt = true; // 标记为使用 LLVM 优化
}
} else
p_vector = ExecVecQual(node->ss.ps.qual, expr_context, false); // 如果没有 JIT 编译,则使用普通执行函数
if (p_vector == NULL) // 如果标量向量为空
return NULL; // 返回 NULL,表示没有满足条件的行
batch->Pack(expr_context->ecxt_scanbatch->m_sel); // 将扫描批次的选择器应用于当前批次
}
// 设置表达式上下文中的选择使用标志
expr_context->m_fUseSelection = node->ss.ps.ps_ExprContext->m_fUseSelection;
p_res = ExecVecProject(node->ss.ps.ps_ProjInfo); // 执行向量化投影,生成结果批次
return p_res; // 返回结果批次
}