higuoxing

higuoxing

github
twitter

為 PostgreSQL 實現替代的 JIT 提供者

即時編譯自 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 工具鏈,您可能需要指定 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 的擴展構建框架4 非常方便使用。讓我們創建一個目錄 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 提供者可以有不同的資源來跟踪,我們可以繼承 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 提供者原型。還有幾個方面可以改進。

  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. 擴展構建基礎設施

  5. 查詢編譯與 JIT 代碼生成(CMU 高級數據庫 / 2023 年春季)

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。