PostgreSQL 11に搭載されるJITコンパイラ機能を動かしてみる

postgresql
この記事は約11分で読めます。

PostgreSQL 11から、JIT機能が追加されました。

PostgreSQL 11 Beta 1 Released!
The PostgreSQL Global Development Group announces that the first beta release of PostgreSQL 11 is now available for down...
PostgreSQL、高層化を実現するLLVMベースのJIT機能を導入開始
ここ数日のコミットで、PostgreSQLにLLVMを使ったJIT機能を取り込むためのコードが追加された(Github.comなど)。追加されたコードは全体の初期段階にあり、今後、関連したコードが随時追加されていくものと見られる。

あまり理解できていないのですが、どんな感じで動くのか、まずは触ってみたいと思います。

導入と動作確認にあたり、以下の資料を参考にさせていただきました。

https://www.sraoss.co.jp/tech-blog/wp-content/uploads/2018/07/pg11_report-6.pdf

また、llvmについては参考リンクのページに事前に目を通しておくと、どんな事をしているのかイメージしやすいと思います。llvmのtutorialは、ざっと目を通しておくと良いかもしれません。

インストール

試してみた環境は以下です。

  • Mac OS X mac High Sierra 10.13.5
  • iMac (27-inch Late 2012) Intel Core i5 3.2GHz

llvmのインストール

PostgreSQLは、JITの実装としてデフォルトでLLVMを使用するようになっているようです。私の環境は、Homebrewを使用していますので、LLVMを以下のようにしてインストールします。

参考 src/backend/jit/jit.c

PostgreSQL11beta2のインストール

自作のpgenv2[1]https://github.com/moritoru81/pgenv2でサクッとインストールします。

まず、configureオプションで--with-llvmを追加します。

続いて11beta2をインストールします。

11beta2をデフォルト使用にします。

JITに関連するGUCs

PostgreSQL 11のソースツリーにあるsrc/backend/jit/READMEを参照すると、以下のGUCがjitの動作に関係してくるようです。テストで動作確認したい場合、以下のパラメータに小さなコスト値を設定することで、JITを発動しやすくできます。

jit jitの有効または無効化。デフォルトON(利用可能な場合)
jit_above_cost jitコンパイル適用のコスト閾値。デフォルト100000
jit_optimize_above_cost jitコンパイルされたプログラムを最適化するコスト閾値。デフォルト500000
jit_inline_above_cost jitコンパイルされたプログラムの関数や演算をインライン化するコスト閾値。デフォルト500000

参考 https://www.postgresql.org/docs/11/static/runtime-config-query.html

参考 

JITを動かしてみる

JITが活きてくるのは、CPUバウンドかつ長時間実行されるような処理、とドキュメントでも説明されています。そうでない場合は、JITコンパイルのオーバヘッドの方が大きくなってしまいます。

まずはJITが動くかクエリを実行してみます。GUCでJITが有効になるよう設定して、適当なクエリを実行してみます。演算式や関数が含まれるクエリが良いでしょう。

上記のクエリを実行してみます。explainすると、JIT…という記述があるのが分かります。

次は、JITコンパイルの効果が出ているのがわかるように、もう少し複雑なクエリを実行してみます。(実行するクエリは、参考資料と同様、実際には大容量のデータの入ったテーブルを準備するのでなく、実行時にタプルを生成するようにしています)

私の環境では、結果は以下のようでした(本来は、複数回の計測を行ないばらつきを考慮すべきですが、以下では簡単のため省略しています)

Test クエリ Execution Time (msec)
JIT OFF JIT ON
1 SELECT i + 1 FROM bigtable() AS i; 49630.957 47463.748
2 SELECT i, random() * pi() * exp(i % 2) + log(i % 100 + random() + 1) + sqrt(round(log(i % 100 + 50)))
FROM bigtable() AS i;
68899.182 60690.048
3 SELECT (i % 3) AS G, sum(random() * pi() * exp(i % 2) + log(i % 100 + random() + 1) + sqrt(round(log(i % 100 + 50))))
FROM bigtable() AS i
WHERE i % 3 = 0
GROUP BY G;

 
54232.246 48204.660 

Test1は、単純な加算処理ですが、演算対象の行数が多いためか、JITがONの場合には多少の改善が見られました。

Test2・3では、関数と演算を組み合わせてみましたが、数秒程度の改善が見られました。

厳密なベンチではないので適正な評価はできませんが、どのようなケースでJITが有効か、ざっと雰囲気は掴めたような気がします。

JIT機能を少し追ってみる

『PostgreSQLのJITは、どのような仕組みなのだろうか?』、ざっと雰囲気が掴めたら、次は内部を知りたくなってきます。あまり深いところまで追いきれていませんが、入り口部分を覗いて見ました。

JITプロバイダ

PostgreSQLでは、JIT機能のデフォルト実装はLLVMとなっているが、他のJIT実装も可能なよう設計されている(といっても、私はLLVM以外であまり知りません。。)。

参考 src/backend/jit/jit.c

変数jit_providerで指定される動的ライブラリをロードし、実装ごとの_PG_jit_provider_init関数内でJitProviderCallbacks構造体オブジェクトの関数ポインタに実装ごとの関数のアドレスが登録される。デフォルト実装はLLVMであるが、LLVMの場合はllvmjit.cに_PG_jit_provider_init定義がある。

参考 src/backend/jit/llvm/llvmjit.c

JITプロバイダの初期化は、SQL関数を呼ぶことによって明示的に初期化することもできる。以下を実行すると、backendプロセスで、jit_providerで指定される動的モジュールをロードすることができる。

pg_jit_available関数の呼び出し前後で、lldbで動的ライブラリの適当なシンボルが見えるか確認してみる。以下は、LLVMCreateBuilder関数が見えるかbackendプロセスにアタッチして調べた例である。pg_jit_available関数からprovider_init関数が呼ばれ、llvmjit.soがロードされたことがわかる。

JITの対象

PostgreSQLでは、JIT機能はbackendプロセスで動作し、JITコンパイルの対象は以下となっている。

  1. expression evaluation
    1. WHERE句、ターゲットリスト、集約、射影などの式評価
  2. tuple deforming
    1. ディスク上のタプル表現のインメモリ表現への変換

参考 https://www.postgresql.org/docs/11/static/jit-reason.html#JIT-ACCELERATED-OPERATIONS

JIT Contextと生成されるコード

以降はLLVMを前提で記述する。

JITコンパイルのエントリーポイントは、jit_compile_expr関数である。

1.expression evaluationでは、LLVM IRでevalexpr_<module_generation>_<counter>という関数が生成される。module_generationは、LLVMモジュールの生成数(名前はpg)、counterはオブジェクトの出力回数であり、これらの変数はLLVMJitContext構造体で定義されている。

参考 src/backend/jit/llvm/llvmjit_expr.c

2. tuple deformingでは、LLVM IRでdeform_<module_generation>_<counter>という関数が生成される。

参考 src/backend/jit/llvm/llvmjit_deform.c

LLVMJitContextは、plan実行時に作成され、トランザクションの終わりで解放される。

参考 src/backend/executor/execMain.c

1、2で生成されるLLVM IRをみるには、jit_dump_bitcodeパラメータをonにすればよい。パラメータをonにして、JITが働くクエリを実行すると、pgdata下に.bcファイルが生成される。.llで人が読める形式に変換するにはllvm-disを実行する。

試しに以下のようなクエリを実行してみる。

私の実行した環境では、以下のファイルが生成された。

  • 90645.14.bc
  • 90645.14.optimized.bc

optimizedは最適化されたビットコードである。90645.14.bcをllvm-disでテキストとして読めるLLVM IR形式に変換してみた結果が以下である。

最初の%で始まる定義は、型定義である。LLVM IRでは、%で始まる名前はローカル識別子、@で始まる名前はグローバル識別子を示している[2]https://llvm.org/docs/LangRef.html#identifiers

71行目付近に関数定義があり、evalexpr_xx_xという名前になっていることがわかる。

LLVM IRを生成しているllvm_compile_expr関数をみると、LLVMのIRの構造に沿って、コード作成されていくことがわかる。

LLVMModuleCreateWithNameでModuleを作成し、LLVMAddFunctionでModuleにFunctionを追加し、LLVMAppendBasicBlockでBasicBlockを追加、BuilderでInstructionの構築・・・といった感じである。

参考リンク

 

 

きつねさんでもわかるLLVM ~コンパイラを自作するためのガイドブック~
柏木 餅子 風薬
インプレス
売り上げランキング: 87,944

コメント

タイトルとURLをコピーしました