自 11 版本以来,PostgreSQL 增加了即时编译(JIT)。PostgreSQL 的默认 JIT 提供者基于 LLVM。用户可以通过设置 jit_provider
GUC 参数1 来使用替代的 JIT 提供者。可插拔的 JIT 接口非常易于使用,我成功构建了两个原型,一个发出 C 代码2,另一个发出汇编代码3,使用了 AsmJit 库4。在这篇文章中,我将简要介绍现有的 LLVM JIT 提供者,并向您展示如何实现发出 C 代码的原型。这既有趣又简单。
LLVM JIT 提供者简介#
启用 LLVM JIT#
内置的 JIT 提供者可以通过在构建 PostgreSQL 时将 --with-llvm
附加到配置标志来启用。如果您的系统上安装了多个 LLVM 工具链,您可能需要指定 CLANG
和 LLVM_CONFIG
环境变量,以确保它们来自同一 LLVM 工具链集。否则,会出现 ABI 不兼容问题。
CC=/<path>/<to>/clang CXX=/<path>/<to>/clang++ CLANG=/<path>/<to>/clang LLVM_CONFIG=/<path>/<to>/llvm-config \
./configure --with-llvm <other-configure-flags>
构建完成后,输入以下命令以验证您的服务器中是否启用了 LLVM JIT。
postgres=# SHOW jit;
jit
-----
on
(1 row)
postgres=# SHOW jit_provider;
jit_provider
--------------
llvmjit
(1 row)
将 jit_above_cost
设置为 0
以强制服务器对查询进行 JIT。
postgres=# SET jit_above_cost=0;
SET
postgres=# EXPLAIN (ANALYZE) SELECT 1;
QUERY PLAN
------------------------------------------------------------------------------------------------------------
Result (cost=0.00..0.01 rows=1 width=4) (actual time=3.945..3.946 rows=1 loops=1)
Planning Time: 0.039 ms
JIT:
Functions: 1
Options: Inlining false, Optimization false, Expressions true, Deforming true
Timing: Generation 0.181 ms, Inlining 0.000 ms, Optimization 0.216 ms, Emission 3.721 ms, Total 4.117 ms
Execution Time: 4.218 ms
(7 rows)
从 JIT 统计信息中,我们了解到 LLVM JIT 支持从多个方面加速查询,例如内联函数、JIT 表达式和 JIT 元组变形过程。
JIT 表达式#
在 PostgreSQL 中,SQL 查询中的表达式最终被转换为低级操作符,其结果可以通过解释这些操作符来计算。这些操作符在头文件 src/include/executor/execExpr.h
中定义。
typedef enum ExprEvalOp
{
/* 整个表达式已完全评估,返回 */
EEOP_DONE,
/* 在相应的元组槽上应用 slot_getsomeattrs */
EEOP_INNER_FETCHSOME,
EEOP_OUTER_FETCHSOME,
EEOP_SCAN_FETCHSOME,
...
};
当 JIT 未启用时,解释它们的主要入口是 src/backend/executor/execExprInterp.c:ExecInterpExpr
,表达式的结果可以通过迭代 ExprState::steps
数组来计算。
/*
* ExecInterpExpr 使用“直接线程”实现表达式评估
* 以提高性能。为了使解释器易于理解,
* 我将其重写为使用 for 循环。
*/
static Datum
ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
{
...
for (int opno = 0; opno < state->steps_len; ++opno)
{
ExprEvalStep *op = &state->steps[opno];
switch ((ExprEvalOp) op->opcode)
{
case EEOP_DONE:
{
*isnull = state->resnull;
return state->resvalue;
}
case EEOP_INNER_FETCHSOME:
{
CheckOpSlotCompatibility(op, innerslot);
slot_getsomeattrs(innerslot, op->d.fetch.last_var);
break;
}
/* 其他操作符... */
}
}
...
}
当启用 LLVM JIT 时,在解释操作符之前,LLVM JIT 提供者将这些操作符编译为 LLVM IR,编译操作符的主要入口是 src/backend/jit/llvm/llvmjit_expr.c:llvm_compile_expr
。
bool
llvm_compile_expr(ExprState *state)
{
/*
* 发出一个与
* Datum ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) 相似的函数。
*
* 注意:LLVM 从未有过这样的 API,这只是为了易于理解!
*/
Func = LLVMIRBuilder.newFunc("Datum JittedExecInterpExpr(ExprState *, ExprContext *, bool *)");
for (int opno = 0; opno < state->steps_len; ++op)
{
switch ((ExprEvalOp) op->opcode)
{
case EEOP_DONE:
{
/* 为 EEOP_DONE 操作符发出 LLVM IR */
Func.emit("Assign state->resnull to *isnull");
Func.emit("Return state->resvalue");
break;
}
/* 为其他操作符发出 LLVM IR... */
}
}
...
/*
* 将发出的函数添加到 LLVM JIT 运行时。
* EmittedFunc 是发出的 JIT 函数的地址。
*/
EmittedFunc = LLVMRuntime.add(Func);
...
/*
* 将发出的函数的地址存储到 state->evalfunc,以便
* 调用者将调用 JIT 函数。
*/
state->evalfunc = EmittedFunc;
...
}
JIT 元组变形过程#
元组变形过程在 3 个操作符中被调用:EEOP_INNER_FETCHSOME
、EEOP_OUTER_FETCHSOME
和 EEOP_SCAN_FETCHSOME
。也就是说,如果我们为这 3 个操作符添加代码生成支持,那么就支持 JIT 元组变形。
内联函数#
如果我们使用 LLVM JIT 支持安装 PostgreSQL 服务器,则会有一个特殊目录 <prefix>/lib/postgresql/bitcode/
。
$ ls -al <prefix>/lib/postgresql/bitcode/
total 2068
drwxr-xr-x 3 v users 4096 Nov 6 23:24 .
drwxr-xr-x 4 v users 4096 Nov 5 09:03 ..
drwxr-xr-x 28 v users 4096 Oct 22 10:22 postgres
-rw-r--r-- 1 v users 2104036 Nov 1 21:25 postgres.index.bc
它包含整个服务器的 LLVM 位码。当 JIT 表达式调用其他函数时,服务器进程将从位码中查找函数定义。如果该函数能够被内联,则该函数将从位码中提取并放置在 JIT 函数体中。我们的原型将不支持内联函数,因为我还没有找到在没有 LLVM 的情况下实现它的方法。
实现我们自己的 JIT 提供者原型#
通过上述分析,即使我们不是执行器的专家,我们仍然能够为 PostgreSQL 实现替代的 JIT 提供者。由于发出的函数与 src/backend/executor/execExprInterp.c:ExecInterpExpr
相同。
可插拔的 JIT 接口#
PostgreSQL 提供了实现 JIT 提供者的接口。
struct JitProviderCallbacks
{
// 用于将操作符编译为机器代码。
JitProviderCompileExprCB compile_expr;
// 在完成执行 JIT 代码后释放资源。
JitProviderReleaseContextCB release_context;
// 当在编译操作符或执行 JIT 代码期间发生错误时重置某些状态。
JitProviderResetAfterErrorCB reset_after_error;
};
extern void _PG_jit_provider_init(JitProviderCallbacks *cb);
现在,我们对 PostgreSQL 的 JIT 提供者有了基本的了解。让我们开始吧!
pg_slowjit
的基本设置#
我们将实现我们的 JIT 提供者作为一个扩展,因为 PostgreSQL 的扩展构建框架5 非常方便使用。让我们创建一个目录 pg_slowjit
,其中包含 3 个文件:Makefile
、slowjit.control
和 slowjit.c
pg_slowjit/Makefile (点击我查看内容)
MODULE_big = slowjit
EXTENSION = slowjit
OBJS = slowjit.o
# 禁用 LLVM 位码生成。
override with_llvm = no
PG_CONFIG := pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)
pg_slowjit/slowjit.control (点击我查看内容)
comment = '一个非常低效的 JIT 提供者。'
default_version = '1.0.0'
module_pathname = '$libdir/slowjit'
relocatable = true
pg_slowjit/slowjit.c (点击我查看内容)
/* 一堆头文件。 */
#include "postgres.h"
#include "c.h"
#include "executor/execExpr.h"
#include "fmgr.h"
#include "jit/jit.h"
#include "lib/stringinfo.h"
#include "miscadmin.h"
#include "nodes/execnodes.h"
#include "nodes/pg_list.h"
#include "pg_config_manual.h"
#include "utils/elog.h"
#include "utils/memutils.h"
#include "utils/palloc.h"
#include "utils/resowner.h"
#include "utils/resowner_private.h"
#include <dlfcn.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
PG_MODULE_MAGIC;
extern void _PG_jit_provider_init(JitProviderCallbacks *cb);
/* JIT 编译的函数原型。 */
static bool slowjit_compile_expr(ExprState *state) {
/*
* 发出一条通知消息,以便我们可以检查 JIT 提供者是否成功加载。
*/
elog(NOTICE, "slowjit_compile_expr");
/* 返回 'false' 表示我们不会对当前表达式进行 JIT。 */
return false;
}
static void slowjit_release_context(JitContext *ctx) {
elog(NOTICE, "slowjit_release_context");
}
static void slowjit_reset_after_error(void) {
elog(NOTICE, "slowjit_reset_after_error");
}
/* 初始化 JIT 编译回调的函数。 */
void _PG_jit_provider_init(JitProviderCallbacks *cb) {
cb->compile_expr = slowjit_compile_expr;
cb->release_context = slowjit_release_context;
cb->reset_after_error = slowjit_reset_after_error;
}
测试我们是否能够编译我们的扩展。
$ make PG_CONFIG=/<path>/<to>/pg_config install
确保 PostgreSQL 可以加载我们的 JIT 提供者。
-
编辑
/<path>/<to>/<DataDir>/postgresql.conf
并添加以下行。+ jit_provider='slowjit' # 告诉 PostgreSQL 使用我们的 JIT 提供者 + jit_above_cost=0 # 强制 PostgreSQL 对表达式进行 JIT
-
重启 PostgreSQL 服务器。
$ pg_ctl -D <path>/<to>/<DataDir> -l <path>/<to>/logfile restart
-
打开
psql
客户端。postgres=# EXPLAIN SELECT 1; NOTICE: slowjit_compile_expr QUERY PLAN ------------------------------------------ Result (cost=0.00..0.01 rows=1 width=4) (1 row)
您会发现通知消息打印到我们的终端。我们的 JIT 提供者已成功加载!🎉
pg_slowjit
的上下文管理#
您可能注意到有一个名为 JitContext
的特殊数据结构,它跟踪分配的资源并记录当前 JIT 编译的重要信息。JitContext::flags
控制是否对元组变形过程进行 JIT(flags & PGJIT_DEFORM
)、是否对 JIT 代码进行激进优化(flags & PGJIT_OPT3
)等。JitContext::resowner
记录当前资源所有者。JitContext::instr
记录有关当前 JIT 查询的一些统计信息,例如,在元组变形过程、代码优化、函数内联等中消耗的时间。
typedef struct JitContext
{
int flags;
ResourceOwner resowner;
JitInstrumentation instr;
} JitContext;
不同的 JIT 提供者可以跟踪不同的资源,我们可以为 SlowJitContext
继承 JitContext
。
typedef struct SlowJitContext {
JitContext base;
/* 待实现的字段。 */
} SlowJitContext;
回调函数 cb->compile_expr
可以为单个查询多次调用。当第一次调用 cb->compile_expr
时,JitContext
数据结构会被初始化。现在,让我们初始化我们的 SlowJitContext
。
pg_slowjit/slowjit.c (点击我查看差异)
/* 一堆头文件。 */
#include "postgres.h"
#include "c.h"
#include "executor/execExpr.h"
#include "fmgr.h"
#include "jit/jit.h"
#include "lib/stringinfo.h"
#include "miscadmin.h"
#include "nodes/execnodes.h"
#include "nodes/pg_list.h"
#include "pg_config_manual.h"
#include "utils/elog.h"
#include "utils/memutils.h"
#include "utils/palloc.h"
#include "utils/resowner.h"
#include "utils/resowner_private.h"
#include <dlfcn.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
PG_MODULE_MAGIC;
extern void _PG_jit_provider_init(JitProviderCallbacks *cb);
+ typedef struct SlowJitContext {
+ JitContext base;
+ /* 待实现的字段。 */
+ } SlowJitContext;
+
/* JIT 编译的函数原型。 */
static bool slowjit_compile_expr(ExprState *state) {
+ PlanState *parent = state->parent;
+ SlowJitContext *jit_ctx = NULL;
+
+ /* parent 不应该为 NULL。 */
+ Assert(parent != NULL);
+
/*
* 发出一条通知消息,以便我们可以检查 JIT 提供者是否成功加载。
*/
elog(NOTICE, "slowjit_compile_expr");
+ /* 初始化上下文。 */
+ if (parent->state->es_jit) {
+ /*
+ * 我们可以重用 JIT 上下文。
+ */
+ jit_ctx = (SlowJitContext *)parent->state->es_jit;
+ } else {
+ ResourceOwnerEnlargeJIT(CurrentResourceOwner);
+
+ jit_ctx = (SlowJitContext *)MemoryContextAllocZero(TopMemoryContext,
+ sizeof(SlowJitContext));
+ jit_ctx->base.flags = parent->state->es_jit_flags;
+
+ /* 确保清理 */
+ jit_ctx->base.resowner = CurrentResourceOwner;
+ ResourceOwnerRememberJIT(CurrentResourceOwner, PointerGetDatum(jit_ctx));
+
+ /* 用于重用 JIT 上下文。 */
+ parent->state->es_jit = &jit_ctx->base;
+ }
/* 返回 'false' 表示我们不会对当前表达式进行 JIT。 */
return false;
}
static void slowjit_release_context(JitContext *ctx) {
elog(NOTICE, "slowjit_release_context");
}
static void slowjit_reset_after_error(void) {
elog(NOTICE, "slowjit_reset_after_error");
}
/* 初始化 JIT 编译回调的函数。 */
void _PG_jit_provider_init(JitProviderCallbacks *cb) {
cb->compile_expr = slowjit_compile_expr;
cb->release_context = slowjit_release_context;
cb->reset_after_error = slowjit_reset_after_error;
}
重新编译我们的 JIT 提供者。
$ make PG_CONFIG=/<path>/<to>/pg_config install
重新运行查询,您会发现 slowjit_release_context
被调用!也就是说,SlowJitContext
中跟踪的资源可以在 slowjit_release_context
中释放。
postgres=# EXPLAIN SELECT 1;
NOTICE: slowjit_compile_expr
NOTICE: slowjit_release_context
QUERY PLAN
------------------------------------------
Result (cost=0.00..0.01 rows=1 width=4)
(1 row)
代码生成#
正如我们上面提到的,pg_slowjit
发出 C 代码并将 C 代码编译为共享库以对表达式进行 JIT。我从 Andy Pavlo 的数据库讲座中学到了这一点6。这很容易实现,而且非常有趣。在观看讲座之前,我甚至没有意识到 C 编译器可以是 JIT 编译器。在本节中,我们将发出一个名为 slowjit_eval_expr_<MyProcPid>_<module_generation>
的函数,其中 MyProcPid
是当前服务器进程的进程 ID,module_generation
是发出函数的数量。我们将这两个变量添加到发出的函数中,以避免符号冲突,因为可能会为单个查询发出多个函数。到目前为止,我们没有任何要发出的内容,只是一些注释,例如:“// OP(<opcode>) 待实现
”。
pg_slowjit/slowjit.c (点击我查看差异)
/* 一堆头文件。 */
#include "postgres.h"
#include "c.h"
#include "executor/execExpr.h"
#include "fmgr.h"
#include "jit/jit.h"
#include "lib/stringinfo.h"
#include "miscadmin.h"
#include "nodes/execnodes.h"
#include "nodes/pg_list.h"
#include "pg_config_manual.h"
#include "utils/elog.h"
#include "utils/memutils.h"
#include "utils/palloc.h"
#include "utils/resowner.h"
#include "utils/resowner_private.h"
#include <dlfcn.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
PG_MODULE_MAGIC;
+/*
+ * 为了避免符号名称冲突,我们使用这个变量来计算发出函数的数量
+ * 并将其用作发出函数名称的一部分。
+ */
+static int module_generation = 0;
+
extern void _PG_jit_provider_init(JitProviderCallbacks *cb);
typedef struct SlowJitContext {
JitContext base;
/* 待实现的字段。 */
} SlowJitContext;
/* JIT 编译的函数原型。 */
static bool slowjit_compile_expr(ExprState *state) {
PlanState *parent = state->parent;
SlowJitContext *jit_ctx = NULL;
+ /* 发出的函数的名称。 */
+ char symbol_name[MAXPGPATH];
+ /* 用于保存发出 C 代码的缓冲区。 */
+ StringInfoData code_holder;
/* parent 不应该为 NULL。 */
Assert(parent != NULL);
/*
* 发出一条通知消息,以便我们可以检查 JIT 提供者是否成功加载。
*/
elog(NOTICE, "slowjit_compile_expr");
/* 初始化上下文。 */
if (parent->state->es_jit) {
/*
* 我们可以重用 JIT 上下文。
*/
jit_ctx = (SlowJitContext *)parent->state->es_jit;
} else {
ResourceOwnerEnlargeJIT(CurrentResourceOwner);
jit_ctx = (SlowJitContext *)MemoryContextAllocZero(TopMemoryContext,
sizeof(SlowJitContext));
jit_ctx->base.flags = parent->state->es_jit_flags;
/* 确保清理 */
jit_ctx->base.resowner = CurrentResourceOwner;
ResourceOwnerRememberJIT(CurrentResourceOwner, PointerGetDatum(jit_ctx));
/* 用于重用 JIT 上下文。 */
parent->state->es_jit = &jit_ctx->base;
}
+ initStringInfo(&code_holder);
+
+#define emit_line(...) \
+ do { \
+ appendStringInfo(&code_holder, __VA_ARGS__); \
+ appendStringInfoChar(&code_holder, '\n'); \
+ } while (0)
+
+#define emit_include(header) emit_line("#include \"%s\"", header)
+
+ emit_include("postgres.h");
+ emit_include("nodes/execnodes.h");
+
+ /*
+ * 发出 JIT 函数签名。
+ * 我们使用 MyProcPid 和 module_generation 来避免符号名称冲突。
+ */
+ snprintf(symbol_name, MAXPGPATH, "slowjit_eval_expr_%d_%d", MyProcPid,
+ module_generation);
+ emit_line("Datum %s(ExprState *state, ExprContext *econtext, bool *isnull)",
+ symbol_name);
+
+ /* 打开函数体。 */
+ emit_line("{");
+
+ for (int opno = 0; opno < state->steps_len; ++opno) {
+ ExprEvalStep *op;
+ ExprEvalOp opcode;
+
+ op = &state->steps[opno];
+ opcode = ExecEvalStepOp(state, op);
+
+ switch (opcode) {
+ default: {
+ emit_line("// OP(%d) 待实现", opcode);
+ }
+ }
+ }
+
+ /* 关闭函数体。 */
+ emit_line("}");
+
+ /* 将发出的函数打印到 psql 控制台。 */
+ elog(NOTICE, "\n%s", code_holder.data);
+
/* 返回 'false' 表示我们不会对当前表达式进行 JIT。 */
return false;
}
static void slowjit_release_context(JitContext *ctx) {
elog(NOTICE, "slowjit_release_context");
}
static void slowjit_reset_after_error(void) {
elog(NOTICE, "slowjit_reset_after_error");
}
/* 初始化 JIT 编译回调的函数。 */
void _PG_jit_provider_init(JitProviderCallbacks *cb) {
cb->compile_expr = slowjit_compile_expr;
cb->release_context = slowjit_release_context;
cb->reset_after_error = slowjit_reset_after_error;
}
再次编译模块并重新运行查询 SELECT 1
。
postgres=# EXPLAIN SELECT 1;
NOTICE: slowjit_compile_expr
NOTICE:
#include "postgres.h"
#include "nodes/execnodes.h"
Datum slowjit_eval_expr_89791_0(ExprState *state, ExprContext *econtext, bool *isnull)
{
// OP(16) 待实现
// OP(14) 待实现
// OP(0) 待实现
}
NOTICE: slowjit_release_context
QUERY PLAN
------------------------------------------
Result (cost=0.00..0.01 rows=1 width=4)
(1 row)
为了对最简单的查询 SELECT 1
进行 JIT,我们需要实现 3 个操作符:EEOP_CONST (16)
、EEOP_ASSIGN_TMP (14)
、EEOP_DONE (0)
。请记住我们在本章开头提到的内容:
发出的函数与
src/backend/executor/execExprInterp.c:ExecInterpExpr
相同。
这 3 个操作符的实现如下所示:
EEO_CASE(EEOP_DONE)
{
goto out;
}
...
EEO_CASE(EEOP_ASSIGN_TMP)
{
int resultnum = op->d.assign_tmp.resultnum;
Assert(resultnum >= 0 && resultnum < resultslot->tts_tupleDescriptor->natts);
resultslot->tts_values[resultnum] = state->resvalue;
resultslot->tts_isnull[resultnum] = state->resnull;
EEO_NEXT();
}
...
EEO_CASE(EEOP_CONST)
{
*op->resnull = op->d.constval.isnull;
*op->resvalue = op->d.constval.value;
EEO_NEXT();
}
...
out:
*isnull = state->resnull;
return state->resvalue;
我们可以将逻辑复制并粘贴到 slowjit_compile_expr
中。
pg_slowjit/slowjit.c (点击我查看差异)
/* 一堆头文件。 */
#include "postgres.h"
#include "c.h"
#include "executor/execExpr.h"
#include "fmgr.h"
#include "jit/jit.h"
#include "lib/stringinfo.h"
#include "miscadmin.h"
#include "nodes/execnodes.h"
#include "nodes/pg_list.h"
#include "pg_config_manual.h"
#include "utils/elog.h"
#include "utils/memutils.h"
#include "utils/palloc.h"
#include "utils/resowner.h"
#include "utils/resowner_private.h"
#include <dlfcn.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
PG_MODULE_MAGIC;
/*
* 为了避免符号名称冲突,我们使用这个变量来计算发出函数的数量
* 并将其用作发出函数名称的一部分。
*/
static int module_generation = 0;
extern void _PG_jit_provider_init(JitProviderCallbacks *cb);
typedef struct SlowJitContext {
JitContext base;
/* 待实现的字段。 */
} SlowJitContext;
/* JIT 编译的函数原型。 */
static bool slowjit_compile_expr(ExprState *state) {
PlanState *parent = state->parent;
SlowJitContext *jit_ctx = NULL;
/* 发出的函数的名称。 */
char symbol_name[MAXPGPATH];
/* 用于保存发出 C 代码的缓冲区。 */
StringInfoData code_holder;
/* parent 不应该为 NULL。 */
Assert(parent != NULL);
/*
* 发出一条通知消息,以便我们可以检查 JIT 提供者是否成功加载。
*/
elog(NOTICE, "slowjit_compile_expr");
/* 初始化上下文。 */
if (parent->state->es_jit) {
/*
* 我们可以重用 JIT 上下文。
*/
jit_ctx = (SlowJitContext *)parent->state->es_jit;
} else {
ResourceOwnerEnlargeJIT(CurrentResourceOwner);
jit_ctx = (SlowJitContext *)MemoryContextAllocZero(TopMemoryContext,
sizeof(SlowJitContext));
jit_ctx->base.flags = parent->state->es_jit_flags;
/* 确保清理 */
jit_ctx->base.resowner = CurrentResourceOwner;
ResourceOwnerRememberJIT(CurrentResourceOwner, PointerGetDatum(jit_ctx));
/* 用于重用 JIT 上下文。 */
parent->state->es_jit = &jit_ctx->base;
}
initStringInfo(&code_holder);
#define emit_line(...) \
do { \
appendStringInfo(&code_holder, __VA_ARGS__); \
appendStringInfoChar(&code_holder, '\n'); \
} while (0)
#define emit_include(header) emit_line("#include \"%s\"", header)
emit_include("postgres.h");
emit_include("nodes/execnodes.h");
/*
* 发出 JIT 函数签名。
* 我们使用 MyProcPid 和 module_generation 来避免符号名称冲突。
*/
snprintf(symbol_name, MAXPGPATH, "slowjit_eval_expr_%d_%d", MyProcPid,
module_generation);
emit_line("Datum %s(ExprState *state, ExprContext *econtext, bool *isnull)",
symbol_name);
/* 打开函数体。 */
emit_line("{");
for (int opno = 0; opno < state->steps_len; ++opno) {
ExprEvalStep *op;
ExprEvalOp opcode;
op = &state->steps[opno];
opcode = ExecEvalStepOp(state, op);
switch (opcode) {
+ case EEOP_DONE: {
+ emit_line(" { // EEOP_DONE");
+ emit_line(" *isnull = state->resnull;");
+ emit_line(" }");
+ emit_line(" return state->resvalue;");
+
+ /* 关闭函数体。 */
+ emit_line("}");
+ break;
+ }
+ case EEOP_ASSIGN_TMP: {
+ int resultnum = op->d.assign_tmp.resultnum;
+ emit_line(" { // EEOP_ASSIGN_TMP");
+ emit_line(" TupleTableSlot *resultslot = state->resultslot;");
+ emit_line(" resultslot->tts_values[%d] = state->resvalue;", resultnum);
+ emit_line(" resultslot->tts_isnull[%d] = state->resnull;", resultnum);
+ emit_line(" }");
+ break;
+ }
+ case EEOP_CONST: {
+ emit_line(" { // EEOP_CONST");
+ emit_line(" bool *resnull = (bool *) %lu;", (uint64_t)op->resnull);
+ emit_line(" Datum *resvalue = (Datum *) %lu;", (uint64_t)op->resvalue);
+ emit_line(" *resnull = (bool) %d;", op->d.constval.isnull);
+ emit_line(" *resvalue = (Datum) %luull;", op->d.constval.value);
+ emit_line(" }");
+ break;
+ }
default: {
emit_line("// OP(%d) 待实现", opcode);
}
}
}
- /* 关闭函数体。 */
- emit_line("}");
-
/* 将发出的函数打印到 psql 控制台。 */
elog(NOTICE, "\n%s", code_holder.data);
/* 返回 'false' 表示我们不会对当前表达式进行 JIT。 */
return false;
}
static void slowjit_release_context(JitContext *ctx) {
elog(NOTICE, "slowjit_release_context");
}
static void slowjit_reset_after_error(void) {
elog(NOTICE, "slowjit_reset_after_error");
}
/* 初始化 JIT 编译回调的函数。 */
void _PG_jit_provider_init(JitProviderCallbacks *cb) {
cb->compile_expr = slowjit_compile_expr;
cb->release_context = slowjit_release_context;
cb->reset_after_error = slowjit_reset_after_error;
}
再次编译模块并重新运行查询 SELECT 1
。
postgres=# EXPLAIN SELECT 1;
NOTICE: slowjit_compile_expr
NOTICE:
#include "postgres.h"
#include "nodes/execnodes.h"
Datum slowjit_eval_expr_113916_0(ExprState *state, ExprContext *econtext, bool *isnull)
{
{ // EEOP_CONST
bool *resnull = (bool *) 94251888729381;
Datum *resvalue = (Datum *) 94251888729384;
*resnull = (bool) 0;
*resvalue = (Datum) 1ull;
}
{ // EEOP_ASSIGN_TMP
TupleTableSlot *resultslot = state->resultslot;
resultslot->tts_values[0] = state->resvalue;
resultslot->tts_isnull[0] = state->resnull;
}
{ // EEOP_DONE
*isnull = state->resnull;
}
return state->resvalue;
}
NOTICE: slowjit_release_context
QUERY PLAN
------------------------------------------
Result (cost=0.00..0.01 rows=1 width=4)
(1 row)
编译和加载发出的函数#
为了完成我们的 JIT 提供者,我们需要将执行低级操作码的函数替换为我们发出的函数。基本思路是将发出的函数编译为共享库,并通过 dlopen()
和 dlsym()
从库中加载该函数。
pg_slowjit/slowjit.c (点击我查看差异)
/* 一堆头文件。 */
#include "postgres.h"
#include "c.h"
#include "executor/execExpr.h"
#include "fmgr.h"
#include "jit/jit.h"
#include "lib/stringinfo.h"
#include "miscadmin.h"
#include "nodes/execnodes.h"
#include "nodes/pg_list.h"
#include "pg_config_manual.h"
#include "utils/elog.h"
#include "utils/memutils.h"
#include "utils/palloc.h"
#include "utils/resowner.h"
#include "utils/resowner_private.h"
#include <dlfcn.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
PG_MODULE_MAGIC;
/*
* 为了避免符号名称冲突,我们使用这个变量来计算发出函数的数量
* 并将其用作发出函数名称的一部分。
*/
static int module_generation = 0;
extern void _PG_jit_provider_init(JitProviderCallbacks *cb);
typedef struct SlowJitContext {
JitContext base;
/* 待实现的字段。 */
} SlowJitContext;
/* JIT 编译的函数原型。 */
static bool slowjit_compile_expr(ExprState *state) {
PlanState *parent = state->parent;
SlowJitContext *jit_ctx = NULL;
/* 发出的函数的名称。 */
char symbol_name[MAXPGPATH];
/* 用于保存发出 C 代码的缓冲区。 */
StringInfoData code_holder;
/* parent 不应该为 NULL。 */
Assert(parent != NULL);
/*
* 发出一条通知消息,以便我们可以检查 JIT 提供者是否成功加载。
*/
elog(NOTICE, "slowjit_compile_expr");
/* 初始化上下文。 */
if (parent->state->es_jit) {
/*
* 我们可以重用 JIT 上下文。
*/
jit_ctx = (SlowJitContext *)parent->state->es_jit;
} else {
ResourceOwnerEnlargeJIT(CurrentResourceOwner);
jit_ctx = (SlowJitContext *)MemoryContextAllocZero(TopMemoryContext,
sizeof(SlowJitContext));
jit_ctx->base.flags = parent->state->es_jit_flags;
/* 确保清理 */
jit_ctx->base.resowner = CurrentResourceOwner;
ResourceOwnerRememberJIT(CurrentResourceOwner, PointerGetDatum(jit_ctx));
/* 用于重用 JIT 上下文。 */
parent->state->es_jit = &jit_ctx->base;
}
initStringInfo(&code_holder);
#define emit_line(...) \
do { \
appendStringInfo(&code_holder, __VA_ARGS__); \
appendStringInfoChar(&code_holder, '\n'); \
} while (0)
#define emit_include(header) emit_line("#include \"%s\"", header)
emit_include("postgres.h");
emit_include("nodes/execnodes.h");
/*
* 发出 JIT 函数签名。
* 我们使用 MyProcPid 和 module_generation 来避免符号名称冲突。
*/
snprintf(symbol_name, MAXPGPATH, "slowjit_eval_expr_%d_%d", MyProcPid,
module_generation);
emit_line("Datum %s(ExprState *state, ExprContext *econtext, bool *isnull)",
symbol_name);
/* 打开函数体。 */
emit_line("{");
for (int opno = 0; opno < state->steps_len; ++opno) {
ExprEvalStep *op;
ExprEvalOp opcode;
op = &state->steps[opno];
opcode = ExecEvalStepOp(state, op);
switch (opcode) {
case EEOP_DONE: {
emit_line(" { // EEOP_DONE");
emit_line(" *isnull = state->resnull;");
emit_line(" }");
emit_line(" return state->resvalue;");
/* 关闭函数体。 */
emit_line("}");
break;
}
case EEOP_ASSIGN_TMP: {
int resultnum = op->d.assign_tmp.resultnum;
emit_line(" { // EEOP_ASSIGN_TMP");
emit_line(" TupleTableSlot *resultslot = state->resultslot;");
emit_line(" resultslot->tts_values[%d] = state->resvalue;", resultnum);
emit_line(" resultslot->tts_isnull[%d] = state->resnull;", resultnum);
emit_line(" }");
break;
}
case EEOP_CONST: {
emit_line(" { // EEOP_CONST");
emit_line(" bool *resnull = (bool *) %lu;", (uint64_t)op->resnull);
emit_line(" Datum *resvalue = (Datum *) %lu;", (uint64_t)op->resvalue);
emit_line(" *resnull = (bool) %d;", op->d.constval.isnull);
emit_line(" *resvalue = (Datum) %luull;", op->d.constval.value);
emit_line(" }");
break;
}
default: {
emit_line("// OP(%d) 待实现", opcode);
}
}
}
{
char c_src_path[MAXPGPATH];
char shared_library_path[MAXPGPATH];
char include_server_path[MAXPGPATH];
char compile_command[MAXPGPATH];
FILE *c_src_file;
void *handle;
void *jitted_func;
/* 将发出的 C 代码写入文件。 */
snprintf(c_src_path, MAXPGPATH, "/tmp/%d.%d.c", MyProcPid,
module_generation);
c_src_file = fopen(c_src_path, "w+");
if (c_src_file == NULL) {
ereport(ERROR, (errmsg("无法打开文件 '%s' 进行写入", c_src_path)));
}
fwrite(code_holder.data, 1, code_holder.len, c_src_file);
fclose(c_src_file);
resetStringInfo(&code_holder);
pfree(code_holder.data);
/* 准备编译命令。 */
snprintf(shared_library_path, MAXPGPATH, "/tmp/%d.%d.so", MyProcPid,
module_generation);
get_includeserver_path(my_exec_path, include_server_path);
snprintf(compile_command, MAXPGPATH, "cc -fPIC -I%s -shared -O3 -o %s %s",
include_server_path, shared_library_path, c_src_path);
/* 编译代码 */
if (system(compile_command) != 0) {
ereport(ERROR, (errmsg("无法执行命令:%s", compile_command)));
}
/* 将共享库加载到当前进程。 */
handle = dlopen(shared_library_path, RTLD_LAZY);
if (handle == NULL) {
char *err = dlerror();
ereport(ERROR,
(errmsg("无法 dlopen '%s':%s", shared_library_path, err)));
}
/* 找到函数指针并将其保存到 state->evalfunc */
jitted_func = dlsym(handle, symbol_name);
if (jitted_func == NULL) {
char *err = dlerror();
ereport(ERROR, (errmsg("无法从 '%s' 找到符号 '%s':%s",
shared_library_path, symbol_name, err)));
}
state->evalfunc = jitted_func;
state->evalfunc_private = NULL;
module_generation++;
}
return true;
}
static void slowjit_release_context(JitContext *ctx) {
elog(NOTICE, "slowjit_release_context");
}
static void slowjit_reset_after_error(void) {
elog(NOTICE, "slowjit_reset_after_error");
}
/* 初始化 JIT 编译回调的函数。 */
void _PG_jit_provider_init(JitProviderCallbacks *cb) {
cb->compile_expr = slowjit_compile_expr;
cb->release_context = slowjit_release_context;
cb->reset_after_error = slowjit_reset_after_error;
}
现在,我们可以对最简单的查询进行 JIT 了!!但仍然存在一些问题。在加载共享库后,我们失去了对句柄的跟踪。我们需要在查询完成后关闭共享库的句柄。
pg_slowjit/slowjit.c (点击我查看差异)
/* 一堆头文件。 */
#include "postgres.h"
#include "c.h"
#include "executor/execExpr.h"
#include "fmgr.h"
#include "jit/jit.h"
#include "lib/stringinfo.h"
#include "miscadmin.h"
#include "nodes/execnodes.h"
#include "nodes/pg_list.h"
#include "pg_config_manual.h"
#include "utils/elog.h"
#include "utils/memutils.h"
#include "utils/palloc.h"
#include "utils/resowner.h"
#include "utils/resowner_private.h"
#include <dlfcn.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
PG_MODULE_MAGIC;
/*
* 为了避免符号名称冲突,我们使用这个变量来计算发出函数的数量
* 并将其用作发出函数名称的一部分。
*/
static int module_generation = 0;
extern void _PG_jit_provider_init(JitProviderCallbacks *cb);
typedef struct SlowJitContext {
JitContext base;
- /* 待实现的字段。 */
+ List *handles;
} SlowJitContext;
/* JIT 编译的函数原型。 */
static bool slowjit_compile_expr(ExprState *state) {
PlanState *parent = state->parent;
SlowJitContext *jit_ctx = NULL;
/* 发出的函数的名称。 */
char symbol_name[MAXPGPATH];
/* 用于保存发出 C 代码的缓冲区。 */
StringInfoData code_holder;
/* parent 不应该为 NULL。 */
Assert(parent != NULL);
/*
* 发出一条通知消息,以便我们可以检查 JIT 提供者是否成功加载。
*/
elog(NOTICE, "slowjit_compile_expr");
/* 初始化上下文。 */
if (parent->state->es_jit) {
/*
* 我们可以重用 JIT 上下文。
*/
jit_ctx = (SlowJitContext *)parent->state->es_jit;
} else {
ResourceOwnerEnlargeJIT(CurrentResourceOwner);
jit_ctx = (SlowJitContext *)MemoryContextAllocZero(TopMemoryContext,
sizeof(SlowJitContext));
jit_ctx->base.flags = parent->state->es_jit_flags;
/* 确保清理 */
jit_ctx->base.resowner = CurrentResourceOwner;
ResourceOwnerRememberJIT(CurrentResourceOwner, PointerGetDatum(jit_ctx));
/* 用于重用 JIT 上下文。 */
parent->state->es_jit = &jit_ctx->base;
}
initStringInfo(&code_holder);
#define emit_line(...) \
do { \
appendStringInfo(&code_holder, __VA_ARGS__); \
appendStringInfoChar(&code_holder, '\n'); \
} while (0)
#define emit_include(header) emit_line("#include \"%s\"", header)
emit_include("postgres.h");
emit_include("nodes/execnodes.h");
/*
* 发出 JIT 函数签名。
* 我们使用 MyProcPid 和 module_generation 来避免符号名称冲突。
*/
snprintf(symbol_name, MAXPGPATH, "slowjit_eval_expr_%d_%d", MyProcPid,
module_generation);
emit_line("Datum %s(ExprState *state, ExprContext *econtext, bool *isnull)",
symbol_name);
/* 打开函数体。 */
emit_line("{");
for (int opno = 0; opno < state->steps_len; ++opno) {
ExprEvalStep *op;
ExprEvalOp opcode;
op = &state->steps[opno];
opcode = ExecEvalStepOp(state, op);
switch (opcode) {
case EEOP_DONE: {
emit_line(" { // EEOP_DONE");
emit_line(" *isnull = state->resnull;");
emit_line(" }");
emit_line(" return state->resvalue;");
/* 关闭函数体。 */
emit_line("}");
break;
}
case EEOP_ASSIGN_TMP: {
int resultnum = op->d.assign_tmp.resultnum;
emit_line(" { // EEOP_ASSIGN_TMP");
emit_line(" TupleTableSlot *resultslot = state->resultslot;");
emit_line(" resultslot->tts_values[%d] = state->resvalue;", resultnum);
emit_line(" resultslot->tts_isnull[%d] = state->resnull;", resultnum);
emit_line(" }");
break;
}
case EEOP_CONST: {
emit_line(" { // EEOP_CONST");
emit_line(" bool *resnull = (bool *) %lu;", (uint64_t)op->resnull);
emit_line(" Datum *resvalue = (Datum *) %lu;", (uint64_t)op->resvalue);
emit_line(" *resnull = (bool) %d;", op->d.constval.isnull);
emit_line(" *resvalue = (Datum) %luull;", op->d.constval.value);
emit_line(" }");
break;
}
default: {
emit_line("// OP(%d) 待实现", opcode);
}
}
}
{
char c_src_path[MAXPGPATH];
char shared_library_path[MAXPGPATH];
char include_server_path[MAXPGPATH];
char compile_command[MAXPGPATH];
FILE *c_src_file;
void *handle;
void *jitted_func;
+ MemoryContext oldctx;
/* 将发出的 C 代码写入文件。 */
snprintf(c_src_path, MAXPGPATH, "/tmp/%d.%d.c", MyProcPid,
module_generation);
c_src_file = fopen(c_src_path, "w+");
if (c_src_file == NULL) {
ereport(ERROR, (errmsg("无法打开文件 '%s' 进行写入", c_src_path)));
}
fwrite(code_holder.data, 1, code_holder.len, c_src_file);
fclose(c_src_file);
resetStringInfo(&code_holder);
pfree(code_holder.data);
/* 准备编译命令。 */
snprintf(shared_library_path, MAXPGPATH, "/tmp/%d.%d.so", MyProcPid,
module_generation);
get_includeserver_path(my_exec_path, include_server_path);
snprintf(compile_command, MAXPGPATH, "cc -fPIC -I%s -shared -O3 -o %s %s",
include_server_path, shared_library_path, c_src_path);
/* 编译代码 */
if (system(compile_command) != 0) {
ereport(ERROR, (errmsg("无法执行命令:%s", compile_command)));
}
/* 将共享库加载到当前进程。 */
handle = dlopen(shared_library_path, RTLD_LAZY);
if (handle == NULL) {
char *err = dlerror();
ereport(ERROR,
(errmsg("无法 dlopen '%s':%s", shared_library_path, err)));
}
+ /*
+ * 跟踪共享库的句柄,以便我们可以稍后释放它。
+ */
+ oldctx = MemoryContextSwitchTo(TopMemoryContext);
+ jit_ctx->handles = lappend(jit_ctx->handles, handle);
+ MemoryContextSwitchTo(oldctx);
/* 找到函数指针并将其保存到 state->evalfunc */
jitted_func = dlsym(handle, symbol_name);
if (jitted_func == NULL) {
char *err = dlerror();
ereport(ERROR, (errmsg("无法从 '%s' 找到符号 '%s':%s",
shared_library_path, symbol_name, err)));
}
state->evalfunc = jitted_func;
state->evalfunc_private = NULL;
module_generation++;
}
return true;
}
static void slowjit_release_context(JitContext *ctx) {
- elog(NOTICE, "slowjit_release_context");
+ SlowJitContext *jit_ctx = (SlowJitContext *)ctx;
+ ListCell *lc;
+
+ foreach (lc, jit_ctx->handles) {
+ void *handle = (void *)lfirst(lc);
+ dlclose(handle);
+ }
+ list_free(jit_ctx->handles);
+ jit_ctx->handles = NIL;
}
static void slowjit_reset_after_error(void) {
elog(NOTICE, "slowjit_reset_after_error");
}
/* 初始化 JIT 编译回调的函数。 */
void _PG_jit_provider_init(JitProviderCallbacks *cb) {
cb->compile_expr = slowjit_compile_expr;
cb->release_context = slowjit_release_context;
cb->reset_after_error = slowjit_reset_after_error;
}
仪器统计#
我们的 JIT 提供者仍然缺少一些内容。那就是仪器统计信息。LLVM JIT 提供者能够报告有关 JIT 编译的一些统计信息,例如 JIT 函数的数量、代码生成时间等。
postgres=# EXPLAIN (ANALYZE) SELECT 1;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------
Result (cost=0.00..0.01 rows=1 width=4) (actual time=11.966..11.967 rows=1 loops=1)
Planning Time: 0.031 ms
JIT:
Functions: 1
Options: Inlining false, Optimization false, Expressions true, Deforming true
Timing: Generation 0.075 ms (Deform 0.000 ms), Inlining 0.000 ms, Optimization 7.857 ms, Emission 4.099 ms, Total 12.031 ms
Execution Time: 12.113 ms
(7 rows)
有关 JIT 编译的各种信息记录在 JitContext::instr
字段中。
typedef struct JitInstrumentation
{
/* 发出的函数数量 */
size_t created_functions;
/* 生成代码的累计时间 */
instr_time generation_counter;
/* 包含在 generation_counter 中的变形元组的累计时间 */
instr_time deform_counter;
/* 内联的累计时间 */
instr_time inlining_counter;
/* 优化的累计时间 */
instr_time optimization_counter;
/* 代码发出的累计时间 */
instr_time emission_counter;
} JitInstrumentation;
让我们实现其中一些内容,以完成我们的原型。以下差异添加了对创建函数计数和总生成时间的支持。
pg_slowjit/slowjit.c (点击我查看差异)
/* 一堆头文件。 */
#include "postgres.h"
#include "c.h"
#include "executor/execExpr.h"
#include "fmgr.h"
#include "jit/jit.h"
#include "lib/stringinfo.h"
#include "miscadmin.h"
#include "nodes/execnodes.h"
#include "nodes/pg_list.h"
#include "pg_config_manual.h"
+#include "portability/instr_time.h"
#include "utils/elog.h"
#include "utils/memutils.h"
#include "utils/palloc.h"
#include "utils/resowner.h"
#include "utils/resowner_private.h"
#include <dlfcn.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
PG_MODULE_MAGIC;
/*
* 为了避免符号名称冲突,我们使用这个变量来计算发出函数的数量
* 并将其用作发出函数名称的一部分。
*/
static int module_generation = 0;
extern void _PG_jit_provider_init(JitProviderCallbacks *cb);
typedef struct SlowJitContext {
JitContext base;
List *handles;
} SlowJitContext;
/* JIT 编译的函数原型。 */
static bool slowjit_compile_expr(ExprState *state) {
PlanState *parent = state->parent;
SlowJitContext *jit_ctx = NULL;
/* 发出的函数的名称。 */
char symbol_name[MAXPGPATH];
/* 用于保存发出 C 代码的缓冲区。 */
StringInfoData code_holder;
+ /* 一些仪器统计信息。 */
+ instr_time starttime;
+ instr_time endtime;
/* parent 不应该为 NULL。 */
Assert(parent != NULL);
/*
* 发出一条通知消息,以便我们可以检查 JIT 提供者是否成功加载。
*/
elog(NOTICE, "slowjit_compile_expr");
/* 初始化上下文。 */
if (parent->state->es_jit) {
/*
* 我们可以重用 JIT 上下文。
*/
jit_ctx = (SlowJitContext *)parent->state->es_jit;
} else {
ResourceOwnerEnlargeJIT(CurrentResourceOwner);
jit_ctx = (SlowJitContext *)MemoryContextAllocZero(TopMemoryContext,
sizeof(SlowJitContext));
jit_ctx->base.flags = parent->state->es_jit_flags;
/* 确保清理 */
jit_ctx->base.resowner = CurrentResourceOwner;
ResourceOwnerRememberJIT(CurrentResourceOwner, PointerGetDatum(jit_ctx));
/* 用于重用 JIT 上下文。 */
parent->state->es_jit = &jit_ctx->base;
}
+ INSTR_TIME_SET_CURRENT(starttime);
+
initStringInfo(&code_holder);
#define emit_line(...) \
do { \
appendStringInfo(&code_holder, __VA_ARGS__); \
appendStringInfoChar(&code_holder, '\n'); \
} while (0)
#define emit_include(header) emit_line("#include \"%s\"", header)
emit_include("postgres.h");
emit_include("nodes/execnodes.h");
/*
* 发出 JIT 函数签名。
* 我们使用 MyProcPid 和 module_generation 来避免符号名称冲突。
*/
snprintf(symbol_name, MAXPGPATH, "slowjit_eval_expr_%d_%d", MyProcPid,
module_generation);
emit_line("Datum %s(ExprState *state, ExprContext *econtext, bool *isnull)",
symbol_name);
/* 打开函数体。 */
emit_line("{");
for (int opno = 0; opno < state->steps_len; ++opno) {
ExprEvalStep *op;
ExprEvalOp opcode;
op = &state->steps[opno];
opcode = ExecEvalStepOp(state, op);
switch (opcode) {
+ case EEOP_DONE: {
+ emit_line(" { // EEOP_DONE");
+ emit_line(" *isnull = state->resnull;");
+ emit_line(" }");
+ emit_line(" return state->resvalue;");
+
+ /* 关闭函数体。 */
+ emit_line("}");
+ break;
+ }
+ case EEOP_ASSIGN_TMP: {
+ int resultnum = op->d.assign_tmp.resultnum;
+ emit_line(" { // EEOP_ASSIGN_TMP");
+ emit_line(" TupleTableSlot *resultslot = state->resultslot;");
+ emit_line(" resultslot->tts_values[%d] = state->resvalue;", resultnum);
+ emit_line(" resultslot->tts_isnull[%d] = state->resnull;", resultnum);
+ emit_line(" }");
+ break;
+ }
+ case EEOP_CONST: {
+ emit_line(" { // EEOP_CONST");
+ emit_line(" bool *resnull = (bool *) %lu;", (uint64_t)op->resnull);
+ emit_line(" Datum *resvalue = (Datum *) %lu;", (uint64_t)op->resvalue);
+ emit_line(" *resnull = (bool) %d;", op->d.constval.isnull);
+ emit_line(" *resvalue = (Datum) %luull;", op->d.constval.value);
+ emit_line(" }");
+ break;
+ }
default: {
emit_line("// OP(%d) 待实现", opcode);
}
}
}
{
char c_src_path[MAXPGPATH];
char shared_library_path[MAXPGPATH];
char include_server_path[MAXPGPATH];
char compile_command[MAXPGPATH];
FILE *c_src_file;
void *handle;
void *jitted_func;
MemoryContext oldctx;
/* 将发出的 C 代码写入文件。 */
snprintf(c_src_path, MAXPGPATH, "/tmp/%d.%d.c", MyProcPid,
module_generation);
c_src_file = fopen(c_src_path, "w+");
if (c_src_file == NULL) {
ereport(ERROR, (errmsg("无法打开文件 '%s' 进行写入", c_src_path)));
}
fwrite(code_holder.data, 1, code_holder.len, c_src_file);
fclose(c_src_file);
resetStringInfo(&code_holder);
pfree(code_holder.data);
/* 准备编译命令。 */
snprintf(shared_library_path, MAXPGPATH, "/tmp/%d.%d.so", MyProcPid,
module_generation);
get_includeserver_path(my_exec_path, include_server_path);
snprintf(compile_command, MAXPGPATH, "cc -fPIC -I%s -shared -O3 -o %s %s",
include_server_path, shared_library_path, c_src_path);
/* 编译代码 */
if (system(compile_command) != 0) {
ereport(ERROR, (errmsg("无法执行命令:%s", compile_command)));
}
/* 将共享库加载到当前进程。 */
handle = dlopen(shared_library_path, RTLD_LAZY);
if (handle == NULL) {
char *err = dlerror();
ereport(ERROR,
(errmsg("无法 dlopen '%s':%s", shared_library_path, err)));
}
/*
* 跟踪共享库的句柄,以便我们可以稍后释放它。
*/
oldctx = MemoryContextSwitchTo(TopMemoryContext);
jit_ctx->handles = lappend(jit_ctx->handles, handle);
MemoryContextSwitchTo(oldctx);
/* 找到函数指针并将其保存到 state->evalfunc */
jitted_func = dlsym(handle, symbol_name);
if (jitted_func == NULL) {
char *err = dlerror();
ereport(ERROR, (errmsg("无法从 '%s' 找到符号 '%s':%s",
shared_library_path, symbol_name, err)));
}
state->evalfunc = jitted_func;
state->evalfunc_private = NULL;
module_generation++;
+ jit_ctx->base.instr.created_functions++;
}
+ INSTR_TIME_SET_CURRENT(endtime);
+ INSTR_TIME_ACCUM_DIFF(jit_ctx->base.instr.generation_counter, endtime,
+ starttime);
+
return true;
}
static void slowjit_release_context(JitContext *ctx) {
SlowJitContext *jit_ctx = (SlowJitContext *)ctx;
ListCell *lc;
foreach (lc, jit_ctx->handles) {
void *handle = (void *)lfirst(lc);
dlclose(handle);
}
list_free(jit_ctx->handles);
jit_ctx->handles = NIL;
}
static void slowjit_reset_after_error(void) {
elog(NOTICE, "slowjit_reset_after_error");
}
/* 初始化 JIT 编译回调的函数。 */
void _PG_jit_provider_init(JitProviderCallbacks *cb) {
cb->compile_expr = slowjit_compile_expr;
cb->release_context = slowjit_release_context;
cb->reset_after_error = slowjit_reset_after_error;
}
我们的原型能够报告一些统计信息!
postgres=# EXPLAIN (ANALYZE) SELECT 1;
NOTICE: slowjit_compile_expr
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------
Result (cost=0.00..0.01 rows=1 width=4) (actual time=0.002..0.002 rows=1 loops=1)
Planning Time: 0.125 ms
JIT:
Functions: 1
Options: Inlining false, Optimization false, Expressions true, Deforming true
Timing: Generation 71.044 ms (Deform 0.000 ms), Inlining 0.000 ms, Optimization 0.000 ms, Emission 0.000 ms, Total 71.044 ms
Execution Time: 71.358 ms
(7 rows)
结论#
在这篇博客文章中,我们实现了一个简单且效率低下的 JIT 提供者原型。还有几个方面可以改进。
-
每个共享库只包含一个函数。有时我们需要编译多个共享库以对单个查询进行 JIT。PostgreSQL 的 LLVM JIT 提供者可以一次发出多个函数。这可以节省编译共享库和加载函数的时间。
-
为了使这篇文章易于理解,一些代码是不正确的。例如,代码生成的 switch 子句的
default
分支应该返回 false,以停止对不支持的查询进行 JIT,否则将产生不正确的结果,服务器可能会崩溃。 -
JIT 提供者的测试用例缺失。我通常通过运行 PostgreSQL 回归测试套件来测试它,同时加载 JIT 提供者。
本文的完整代码可以在 higuoxing/pg_slowjit 的 blog
分支中找到,改进版本在 main
分支中。