higuoxing

higuoxing

github
twitter

为PostgreSQL实现替代JIT提供程序

自 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 工具链,您可能需要指定 CLANGLLVM_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_FETCHSOMEEEOP_OUTER_FETCHSOMEEEOP_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 个文件:Makefileslowjit.controlslowjit.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 提供者。

  1. 编辑 /<path>/<to>/<DataDir>/postgresql.conf 并添加以下行。

    + jit_provider='slowjit' # 告诉 PostgreSQL 使用我们的 JIT 提供者
    + jit_above_cost=0       # 强制 PostgreSQL 对表达式进行 JIT
    
  2. 重启 PostgreSQL 服务器。

    $ pg_ctl -D <path>/<to>/<DataDir> -l <path>/<to>/logfile restart
    
  3. 打开 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 提供者原型。还有几个方面可以改进。

  1. 每个共享库只包含一个函数。有时我们需要编译多个共享库以对单个查询进行 JIT。PostgreSQL 的 LLVM JIT 提供者可以一次发出多个函数。这可以节省编译共享库和加载函数的时间。

  2. 为了使这篇文章易于理解,一些代码是不正确的。例如,代码生成的 switch 子句的 default 分支应该返回 false,以停止对不支持的查询进行 JIT,否则将产生不正确的结果,服务器可能会崩溃。

  3. JIT 提供者的测试用例缺失。我通常通过运行 PostgreSQL 回归测试套件来测试它,同时加载 JIT 提供者。

本文的完整代码可以在 higuoxing/pg_slowjitblog 分支中找到,改进版本在 main 分支中。

Footnotes#

  1. 可插拔 JIT 提供者。

  2. pg_slowjit - 一个简单的演示,说明如何为 PostgreSQL 实现 JIT 提供者。

  3. pg_asmjit - 一个基于 asmjit 的替代 x86_64 JIT 提供者。

  4. AsmJit -- 一个用 C++ 编写的低延迟机器代码生成库。

  5. 扩展构建基础设施

  6. 查询编译与 JIT 代码生成(CMU 高级数据库 / 2023 年春季)

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。