higuoxing

higuoxing

github
twitter

PostgreSQLの代替JITプロバイダーの実装

PostgreSQL には 11 バージョンからジャストインタイムコンパイルが追加されました。PostgreSQL のデフォルトの JIT プロバイダは LLVM に基づいています。PostgreSQL は、jit_provider GUC パラメータを設定することで、ユーザーが代替の JIT プロバイダを使用できるようにしています1。プラグイン可能な JIT インターフェースは非常に使いやすく、私は C コードを出力するプロトタイプとアセンブリコードを出力するプロトタイプの 2 つを成功裏に構築しました23。この投稿では、既存の LLVM JIT プロバイダについて簡単に紹介し、C コードを出力するプロトタイプの実装方法を示します。楽しく簡単です。

LLVM JIT プロバイダの紹介#

LLVM JIT の有効化#

ビルトイン JIT プロバイダは、PostgreSQL をビルドする際に構成フラグに--with-llvmを追加することで有効化できます。システムに複数の LLVM ツールチェーンがインストールされている場合は、同じ LLVM ツールチェーンセットからのものであることを確認するために、CLANGおよびLLVM_CONFIG環境変数を指定する必要があります。そうしないと、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_cost0に設定して、サーバーにクエリを 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は、出力されたjitted関数のアドレスです。
	 */
	EmittedFunc = LLVMRuntime.add(Func);
	...
	/*
	 * 出力された関数のアドレスをstate->evalfuncに保存し、
	 * 呼び出し元がjitted関数を呼び出せるようにします。
	 */
	state->evalfunc = EmittedFunc;
	...
}

タプル変形プロセスの jit#

タプル変形プロセスは、EEOP_INNER_FETCHSOMEEEOP_OUTER_FETCHSOME、およびEEOP_SCAN_FETCHSOMEの 3 つの演算子で呼び出されます。つまり、これらの 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 ビットコードが含まれています。jitted 式が他の関数を呼び出す場合、サーバープロセスはビットコードから関数定義を検索します。関数がインライン化可能な場合、その関数はビットコードから抽出され、jitted 関数本体に配置されます。私たちのプロトタイプは、LLVM なしで実装する方法を見つけられなかったため、関数のインライン化をサポートしません。

独自の JIT プロバイダプロトタイプを実装する#

上記の分析から、実行者の専門家でなくても、PostgreSQL の代替 JIT プロバイダを実装できることがわかります。出力された関数はsrc/backend/executor/execExprInterp.c:ExecInterpExprと同一です。

プラグイン可能な JIT インターフェース#

PostgreSQL は JIT プロバイダを実装するためのインターフェースを提供しています。

struct JitProviderCallbacks
{
  // 演算子を機械コードにコンパイルするためのコールバック。
  JitProviderCompileExprCB compile_expr;
  // jittedコードの実行が終了した後にリソースを解放するためのコールバック。
  JitProviderReleaseContextCB release_context;
  // 演算子のコンパイルまたはjittedコードの実行中にエラーが発生した場合に
  // 一部の状態をリセットするためのコールバック。
  JitProviderResetAfterErrorCB reset_after_error;
};

extern void _PG_jit_provider_init(JitProviderCallbacks *cb);

これで、PostgreSQL の JIT プロバイダについての基本的な知識を得ました。始めましょう!

pg_slowjitの基本設定#

JIT プロバイダを拡張機能として実装します。PostgreSQL の拡張機能ビルディングフレームワーク [^5] は非常に便利です。Makefileslowjit.controlslowjit.cの 3 つのファイルを持つディレクトリpg_slowjitを作成します。

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 (クリックして内容を表示)
/* 一 bunch of ヘッダーファイル。 */
#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は、現在の jitted クエリに関する統計情報を記録します。たとえば、タプル変形プロセス、コード最適化、関数インライン化にかかる時間などです。

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 (クリックしてdiffを表示)
 /* 一 bunch of ヘッダーファイル。 */
 #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は出力された関数の数です。これら 2 つの変数を出力された関数に追加して、シンボルの衝突を回避します。今のところ、出力するものは、"// OP(<opcode>)を実装する" のようなコメントだけです。

pg_slowjit/slowjit.c (クリックしてdiffを表示)
 /* 一 bunch of ヘッダーファイル。 */
 #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");
+
+  /*
+   * 出力された関数のシグネチャを出力します。
+   * 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)を実装する必要があります。これら 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 (クリックしてdiffを表示)
 /* 一 bunch of ヘッダーファイル。 */
 #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");
 
   /*
    * 出力された関数のシグネチャを出力します。
    * 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 (クリックしてdiffを表示)
 /* 一 bunch of ヘッダーファイル。 */
 #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");
 
   /*
    * 出力された関数のシグネチャを出力します。
    * 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("'%s'をdlopenできません: %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) {
   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 (クリックしてdiffを表示)
 /* 一 bunch of ヘッダーファイル。 */
 #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");
 
   /*
    * 出力された関数のシグネチャを出力します。
    * 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("'%s'をdlopenできません: %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) {
   SlowJitContext *jit_ctx = (SlowJitContext *)ctx;
-  /* 後で実装されるフィールド。 */
+  ListCell *lc;
 
-  elog(NOTICE, "slowjit_release_context");
+  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 コンパイルに関するいくつかの統計を報告することができます。たとえば、jitted 関数の数、コード生成時間などです。

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)

JitContext::instrフィールドには、JIT コンパイルに関するさまざまな情報が記録されています。

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;

これらのいくつかを実装して、プロトタイプを完成させましょう。以下の diff は、作成された関数のカウントと合計生成時間のサポートを追加します。

pg_slowjit/slowjit.c (クリックしてdiffを表示)
 /* 一 bunch of ヘッダーファイル。 */
 #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");
 
   /*
    * 出力された関数のシグネチャを出力します。
    * 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("'%s'をdlopenできません: %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++;
   }
 
+  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. 各共有ライブラリには 1 つの関数しか含まれていません。時には、単一のクエリを jit するために複数の共有ライブラリをコンパイルする必要があります。PostgreSQL の LLVM JIT プロバイダは、一度に複数の関数を出力できます。これにより、共有ライブラリのコンパイルと関数の読み込みにかかる時間を節約できます。

  2. この記事を理解しやすくするために、一部のコードが間違っています。たとえば、コード生成の switch 文のdefaultブランチは、サポートされていないクエリの jit を停止するために false を返すべきです。そうしないと、誤った結果が生成され、サーバーがクラッシュする可能性があります。

  3. JIT プロバイダのテストケースが欠けています。通常、JIT プロバイダを読み込んだ状態で PostgreSQL の回帰テストスイートを実行することでテストします。

この投稿の完全なコードは、higuoxing/pg_slowjitblogブランチにあり、改善されたバージョンはmainブランチにあります。

Footnotes#

  1. プラグイン可能な JIT プロバイダ。

  2. pg_slowjit - PostgreSQL の JIT プロバイダを実装する方法を示すシンプルなデモ。

  3. pg_asmjit - PostgreSQL のための代替 x86_64 JIT プロバイダ(asmjit に基づく)。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。