即時編譯自 PostgreSQL 11 版本開始被添加。PostgreSQL 的預設 JIT 提供者基於 LLVM。PostgreSQL 允許用戶通過設置 jit_provider
GUC 參數來使用替代的 JIT 提供者1。可插拔的 JIT 接口非常易於使用,我已成功構建了兩個原型,一個生成 C 代碼2,另一個生成使用 AsmJit 庫的組合代碼3。在這篇文章中,我將簡要介紹現有的 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 的擴展構建框架4 非常方便使用。讓我們創建一個目錄 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 提供者可以有不同的資源來跟踪,我們可以繼承 JitContext
來創建 SlowJitContext
。
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 的數據庫講座中學到了這一點5。這很容易實現且相當有趣。在觀看講座之前,我甚至沒有意識到 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)
為了 JIT 最簡單的查詢 SELECT 1
,我們需要實現 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);
}
}
}
/* 打印發出的函數到 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;
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;
/* 將發出的 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)));
}
/*
* 跟踪共享庫的句柄,以便我們可以稍後釋放它。
*/
jit_ctx->handles = lappend(jit_ctx->handles, handle);
/* 查找函數指針並將其保存到 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) {
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 最簡單的查詢!!但仍然存在一些問題。在加載共享庫後,我們失去了對句柄的跟踪。我們需要在查詢結束後關閉共享庫的句柄。
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;
/* 將發出的 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)));
}
/*
* 跟踪共享庫的句柄,以便我們可以稍後釋放它。
*/
jit_ctx->handles = lappend(jit_ctx->handles, handle);
/* 查找函數指針並將其保存到 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) {
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;
/* 將發出的 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)));
}
/*
* 跟踪共享庫的句柄,以便我們可以稍後釋放它。
*/
jit_ctx->handles = lappend(jit_ctx->handles, handle);
/* 查找函數指針並將其保存到 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
分支中。