PostgreSQL11の新機能JITコンパイルを試してみた

# はじめに

2018/10/18、PostgreSQL 11がリリースされました。

* PostgreSQL 11 Released<br>
https://www.postgresql.org/about/news/1894/

パラレルクエリやパーティショニングの強化など、大規模データベース向けの様々な機能が強化されています。
今回はその中でも個人的に注目している&なにげに構築が難しい、JITコンパイル機能について検証した内容を共有しようと思います。

本記事を執筆するに辺り、様々な情報を参考にし、そのリンク先を載せています。
<font color="Red">**しかし一部のリンク先は、PostgreSQL 11ベータ版における検証情報です。**</font>
正式にリリースされたPostgreSQL 11とは結果が異なる可能性があるためご注意ください。


# JITコンパイルの導入

PostgreSQL 11を普通に導入しても、JITコンパイルが使えるとは限りません。
デフォルトでは未導入のライブラリ等をインストールする必要があります。


## ソースコードからPostgreSQLを構築する場合

ソースコードからPostgreSQLを構築する場合、事前にLLVMを導入する必要があります。
(CentOS7等はLLVMがデフォルトで導入されていますが、バージョンが古いため対応が必要です)

またコンパイル時に、LLVMを使用することをオプションで明示的にする必要があります。

ソースコードからの構築＋LLVMの導入は[参考にしたサイトの構築・検証系](#構築検証系)に詳しく載っています。
ただし、下記の情報はPostgreSQL 11のベータ版における情報です。
<font color="Red">**現在は、必要なLLVMのバージョンが上がっている可能性があるため注意ください。**</font>


## RPMからPostgreSQLを構築する場合

RPMからのPostgreSQL導入については以前、記事を書いています(ダイマ)。
詳細な導入手順についてはこの記事をご参照ください。
[初心者からこだわりのある人向けRPMからPostgreSQL導入](https://qiita.com/mimitaro/items/de3c0ab6ba9139f8093a)


### オンライン環境の場合

上記記事で紹介しているRPMからのPostgreSQL導入により、JIT対応のビルドを利用できます。
ただしOS側のLLVMのバージョンが、適切できない可能性があるため、注意が必要です。

LLVMのバージョンアップ方法は[参考にしたサイトの構築・検証系](#構築検証系)で紹介したリンク先に案内されています。


### オフライン環境の場合

LLVMはEPEL(Linux用拡張パッケージ)から入手できます。
今回は5.0以上のバージョンを入手します。

* EPEL/ja<br>
https://fedoraproject.org/wiki/EPEL/ja

オフライン環境の場合、直接必要なRPMを上記のサイトから入手し、導入先の環境に配置します。

例)CentOS 7の場合
https://dl.fedoraproject.org/pub/epel/7/x86_64/Packages/l/
↑から「llvm5.0-libs-5.0.1-7.el7.x86_64.rpm」と「llvm5.0-5.0.1-7.el7.x86_64.rpm」を入手する。

```bash:
yum install llvm5.0-libs-5.0.1-7.el7.x86_64.rpm
yum install llvm5.0-5.0.1-7.el7.x86_64.rpm
```

これによりLLVMの導入が完了しました。
次はPostgreSQLのLLVM用のRPMを導入します。
以下のサイトから、適切なOS行におけるバージョンの数字をクリックすることで、PostgreSQL関連の全てのRPMが確認できます。
そこからLLVM関連のRPMを入手します。

* PostgreSQL RPM Chart<br>
https://yum.postgresql.org/rpmchart.php

例)CentOS 7環境におけるPostgreSQL 11の場合
https://download.postgresql.org/pub/repos/yum/11/redhat/rhel-7.4-x86_64/
↑から「postgresql11-llvmjit-11.0-1PGDG.rhel7.x86_64.rpm」を入手する。


```bash:
yum install postgresql11-llvmjit-11.0-1PGDG.rhel7.x86_64.rpm
```

これによりJITコンパイルが利用可能な環境になりました。


# 実際に使ってみよう

JITコンパイルが利用可能な状態になっても、すぐ使える訳ではありません。
関連するパラメータを調整する必要があります。

パラメータについてはマニュアルと[PostgreSQL11がやってくる！(1) - パラメータの差異](https://qiita.com/nuko_yokohama/items/aa597ae8f3b2d650b8da)の記事を参考にさせていただきました。
JIT関連以外にも、PostgreSQL 11で追加/変更されたパラメータの説明があるため、PostgreSQL関係者は必見です。
また(1)とあるように、執筆者である、ぬこ＠横浜様は11に関する様々な記事を投稿しています。
私自身、PostgreSQL 11検証で参考にしているのでオススメです(ダイマその2)。

## JIT関連のパラメータ

JITコンパイルを使用するために必要なパラメータ群です。
特にjit_above_cost、jit_inline_above_cost、jit_optimize_above_costの3つは関連性が強いです。
これらのパラメータを変更し、JITコンパイルを適切に使用します。

|パラメータ名|デフォルト値|説明|
|---|---|---|
|jit|off|JITコンパイルの使用可能にするかを決めます。<br>**ベータ版ではデフォルトでonでしたが、正式版ではoffがデフォルトです。**|
|jit_above_cost|100000|JITコンパイルが使用されるかを決めるコストの閾値です。<br>クエリの推定コストが本値を超えると使用されます。<br>値が-1の場合、無効化されます。|
|jit_inline_above_cost|500000|JITコンパイル使用後に、インライン展開するかを決める閾値です。<br>JITコンパイルのオーバヘッドは増加しますが、クエリ実行時間は減ります。<br>クエリの推定コストが本値を超えると使用されます。<br>値が-1の場合、無効化されます。|
|jit_optimize_above_cost|500000|インライン展開後、高価な最適化をするか決める閾値です。<br>JITコンパイルのオーバヘッドは増加しますが、クエリ実行時間は減ります。<br>クエリの推定コストが本値を超えると使用されます。<br>値が-1の場合、無効化されます。|
|jit_provider|llvmjit|使用するJITプロパイダを決定します。<br>現在はLLVMのみ対応しているため変更の必要はありません。|

この他にも開発者向けのパラメータがありますが、通常運用する際には特に気にする必要はないと思います。


## パラメータを変更しJITコンパイルを使ってみる

[JIT関連のパラメータ](#jit関連のパラメータ)を見れば分かる通り、JIT使用可否の閾値を下げればJITコンパイルをとりあえず使うことはできます。

まずJITコンパイルを使用するテスト用のテーブルを作成します。

```SQL:テスト用テーブル作成
CREATE TABLE jit_test (a INTEGER, b TEXT);

INSERT INTO jit_test (a, b)
SELECT g, md5(g::text) FROM generate_series(1, 50000) AS g;
```

次にJITコンパイルを無理やり実行するために、JITコンパイルを有効化し、関連パラメータの閾値を下げます。

```SQL:パラメータを調整
SET jit=on;
SET jit_above_cost=10;
SET jit_inline_above_cost=10;
SET jit_optimize_above_cost=10;
```

これによりJITコンパイルを使用する準備が出来ました。
PostgreSQL 11現在、JITコンパイルはWHERE句や集計等のために使用されます。
(詳細はマニュアルの以下の部分をご参照ください)

* 32.1.1. JIT Accelerated Operations<br>
https://www.postgresql.org/docs/11/static/jit-reason.html#JIT-ACCELERATED-OPERATIONS

そのため適当なWHERE句を付けて、SQL文を実行し、その実行計画を見てみます。

```SQL:JITコンパイル対象のSQL文
EXPLAIN ANALYZE SELECT * FROM jit_test WHERE a%2 = 1;
```

```SQL:SQL文の実行結果
                                                  QUERY PLAN
---------------------------------------------------------------------------------------------------------------
 Seq Scan on jit_test  (cost=0.00..1167.00 rows=250 width=37) (actual time=71.657..77.408 rows=25000 loops=1)
   Filter: ((a % 2) = 1)
   Rows Removed by Filter: 25000
 Planning Time: 0.044 ms
 JIT:
   Functions: 2
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 0.443 ms, Inlining 50.772 ms, Optimization 12.781 ms, Emission 7.972 ms, Total 71.969 ms
 Execution Time: 86.891 ms
(9 行)
```

「JIT:」以降の行を見れば分かるように、JITコンパイルが使用されています。
ただし今回は閾値を下げて、無理やり使用したため、JITコンパイル未使用時と比べてパフォーマンスは低下しています。
これはJITコンパイルによる高速化よりも、オーバーヘッドによるコストが上回ってしまったためです。
では実際にどのような状況でJITコンパイルが有効であるかを次項で調査・検証してみます。


# 性能を検証してみた

JITコンパイルによるクエリ実行は、通常のクエリ実行と比べ必ずしも高速になるとは限りません。
JITコンパイルによるオーバーヘッドによるコストが高まってしまえば、通常のクエリより遅くなります。
JITコンパイルにより高速化する主なクエリは、I/Oは問題ないが、CPUがボトルネックとなるSQLです。
言い換えると、複雑な集計や計算処理が含まれるSQL処理に適しています。

なので今回はTPC-Hベンチマークテストを使用しました。

## TPC-Hベンチマークテストの導入

TPC-HベンチマークテストはTPCのサイトから入手可能です。
ただしPostgreSQL向けには設定されていないため、調整が必要です。
本記事では導入手順については割愛させていただきます。
(いつか別記事として書くかも)
詳細を知りたい方は、[参考にしたサイトのTPC-HのPostgreSQL導入](#tpc-hのpostgresql導入)のリンク先をご参照ください。

今回は10GBのテストデータを用いて検証を実施しています。

```bash:テスト用データ生成
./dbgen -s 10
```

また検証用の環境として以下を作成しております。
(TPC-Hの用意されたSQLはTPCDスキーマ上にオブジェクトがあることが前提のため)

```sql:ロールの作成
=# CREATE ROLE tpcd;
=# ALTER ROLE tpcd LOGIN SUPERUSER CREATEROLE CREATEDB CONNECTION LIMIT -1 PASSWORD 'password';
```

```sql:データベースの作成
=# CREATE DATABASE tpcd OWNER tpcd;
```

```sql:スキーマの作成
=# \c tpcd tpcd
=# CREATE SCHEMA tpcd;
```

なお、検証には以下の仮想環境を用いています。

|OS|メモリ|CPU|
|---|---|---|
|CentOS 7.4|4GB|2コア|

## JITコンパイルの有無による性能比較

今回はTPC-Hのクエリ1を用いて、JITコンパイル使用有無における性能を比較します。
まずはJITコンパイルを使用しなかった場合のEXPLAN ANALYZEの結果です。
デフォルトでJITの使用はOFFですが、分かりやすいように敢えてJIT=OFFの設定をしています。

```sql:JITコンパイル未使用時のTPC-Hクエリ1の実行結果
=# SET jit=off;
=# explain analyze
select
  l_returnflag,
  l_linestatus,
  sum(l_quantity) as sum_qty,
  sum(l_extendedprice) as sum_base_price,
  sum(l_extendedprice * (1 - l_discount)) as sum_disc_price,
  sum(l_extendedprice * (1 - l_discount) * (1 + l_tax)) as sum_charge,
  avg(l_quantity) as avg_qty,
  avg(l_extendedprice) as avg_price,
  avg(l_discount) as avg_disc,
  count(*) as count_order
from
  lineitem
where
  l_shipdate <= date '1998-12-01' - interval ':1' day
group by
  l_returnflag,
  l_linestatus
order by
  l_returnflag,
  l_linestatus
LIMIT 1;

                                                                            QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=2437688.60..2437688.94 rows=1 width=236) (actual time=68884.525..68885.261 rows=1 loops=1)
   ->  Finalize GroupAggregate  (cost=2437688.60..2437690.67 rows=6 width=236) (actual time=68884.524..68884.525 rows=1 loops=1)
         Group Key: l_returnflag, l_linestatus
         ->  Gather Merge  (cost=2437688.60..2437690.00 rows=12 width=236) (actual time=68884.493..68885.228 rows=4 loops=1)
               Workers Planned: 2
               Workers Launched: 2
               ->  Sort  (cost=2436688.57..2436688.59 rows=6 width=236) (actual time=68879.669..68879.670 rows=3 loops=3)
                     Sort Key: l_returnflag, l_linestatus
                     Sort Method: quicksort  Memory: 27kB
                     Worker 0:  Sort Method: quicksort  Memory: 27kB
                     Worker 1:  Sort Method: quicksort  Memory: 27kB
                     ->  Partial HashAggregate  (cost=2436688.33..2436688.50 rows=6 width=236) (actual time=68879.633..68879.641 rows=4 loops=3)
                           Group Key: l_returnflag, l_linestatus
                           ->  Parallel Seq Scan on lineitem  (cost=0.00..1437019.25 rows=24991727 width=25) (actual time=2.905..13461.643 rows=19995278 loops=3)
                                 Filter: (l_shipdate <= '1998-11-30 00:00:00'::timestamp without time zone)
                                 Rows Removed by Filter: 73
 Planning Time: 0.201 ms
 Execution Time: 68885.341 ms
(18 行)
```



```sql:JITコンパイル未使用時のTPC-Hクエリ1の実行結果
=# SET jit=on;
=# explain analyze
select
  l_returnflag,
  l_linestatus,
  sum(l_quantity) as sum_qty,
  sum(l_extendedprice) as sum_base_price,
  sum(l_extendedprice * (1 - l_discount)) as sum_disc_price,
  sum(l_extendedprice * (1 - l_discount) * (1 + l_tax)) as sum_charge,
  avg(l_quantity) as avg_qty,
  avg(l_extendedprice) as avg_price,
  avg(l_discount) as avg_disc,
  count(*) as count_order
from
  lineitem
where
  l_shipdate <= date '1998-12-01' - interval ':1' day
group by
  l_returnflag,
  l_linestatus
order by
  l_returnflag,
  l_linestatus
LIMIT 1;

                                                                            QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=2437688.60..2437688.94 rows=1 width=236) (actual time=55654.800..55658.101 rows=1 loops=1)
   ->  Finalize GroupAggregate  (cost=2437688.60..2437690.67 rows=6 width=236) (actual time=55278.626..55278.627 rows=1 loops=1)
         Group Key: l_returnflag, l_linestatus
         ->  Gather Merge  (cost=2437688.60..2437690.00 rows=12 width=236) (actual time=55278.556..55281.857 rows=4 loops=1)
               Workers Planned: 2
               Workers Launched: 2
               ->  Sort  (cost=2436688.57..2436688.59 rows=6 width=236) (actual time=55262.481..55262.482 rows=3 loops=3)
                     Sort Key: l_returnflag, l_linestatus
                     Sort Method: quicksort  Memory: 27kB
                     Worker 0:  Sort Method: quicksort  Memory: 27kB
                     Worker 1:  Sort Method: quicksort  Memory: 27kB
                     ->  Partial HashAggregate  (cost=2436688.33..2436688.50 rows=6 width=236) (actual time=55262.405..55262.413 rows=4 loops=3)
                           Group Key: l_returnflag, l_linestatus
                           ->  Parallel Seq Scan on lineitem  (cost=0.00..1437019.25 rows=24991727 width=25) (actual time=343.737..9535.259 rows=19995278 loops=3)
                                 Filter: (l_shipdate <= '1998-11-30 00:00:00'::timestamp without time zone)
                                 Rows Removed by Filter: 73
 Planning Time: 0.415 ms
 JIT:
   Functions: 35
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 8.490 ms, Inlining 323.187 ms, Optimization 678.813 ms, Emission 392.651 ms, Total 1403.141 ms
 Execution Time: 55708.158 ms
(22 行)
```

上記の通り、JITコンパイルにより性能が改善していることが分かります。
(環境が環境なので、目に見えて早くなってはいませんが…)
それぞれ5回ずつ実施した場合の比較は次の通りです。

|実行時間(ms)|1回目|2回目|3回目|4回目|5回目|平均|
|---|---|---|---|---|---|---|
|JITコンパイルなし|62161.412|63427.404|65189.339|74441.315|62692.632|65582.42|
|JITコンパイルあり|56242.278|57135.679|58272.949|68390.395|57561.058|59520.47|

このようにJITコンパイルを用いることで、複雑な計算を実施するSQLが高速化することが分かります。
よりメモリやCPUが大きい環境ならば、JITコンパイルによる影響はさらに増すと思います。


# 参考にしたサイト

## JITコンパイルの詳細・ベンチマークテスト

* **PGCon 2017 (JIT-Compiling SQL Queries in PostgreSQL Using LLVM)**
https://www.pgcon.org/2017/schedule/events/1092.en.html
PGConf 2017におけるJITコンパイルの講演です。
従来との違いや、JITコンパイルの目的など詳しい情報が載っています。
<br>

* **PostgreSQL 11 and Just In Time Compilation of Queries**
https://www.citusdata.com/blog/2018/09/11/postgresql-11-just-in-time/
JITコンパイルによるベンチマークテストの結果が載っています。

## 構築・検証系

* **PostgreSQL 11 検証報告**
https://www.sraoss.co.jp/tech-blog/pgsql/pg11report/
(→3.3.1. JIT コンパイルを使うためのビルド使うためのビルドうためのビルド)
PostgreSQL 11の様々な検証結果が載っています。
とりあえず11で何かできるのか、を知りたい方にオススメです。
<br>

* **KKIDA-GALAXY [PostgreSQL11のJITコンパイリングを試す]**
http://kkida-galaxy.blogspot.com/2018/04/postgresql11-with-jit-01.html
JITコンパイル環境を構築手順として、実行コマンドとその結果など詳細に説明されています。
環境構築が上手くいかない方は、こちらをじっくりと参照すると良いと思います。
<br>

* **How to compile PostgreSQL 11 with support for JIT compilation on RHEL/CentOS 7**
https://blog.dbi-services.com/how-to-compile-postgresql-11-with-support-for-jit-compilation-on-rhelcentos-7/
海外のサイトですが、今回の記事を執筆するに辺り、大変参考になりました。
導入手順からテスト方法まで丁寧に記載されています。
重要なコマンドとその結果は載っているため、英語が苦手な方もGoogle翻訳を利用すれば簡単に理解可能です。
<br>

* **PostgreSQL 11に搭載されるJITコンパイラ機能を動かしてみる**
https://debug-life.net/entry/2928
構築から検証まで丁寧に記載されており、なにより参考情報が豊富です。
(参考にしたPostgreSQLのソース先も紹介されています)

## TPC-HのPostgreSQL導入

* **TPC**
http://www.tpc.org/default.asp
TPCの公式ページです。TPC-Hをはじめとしたベンチマークテストの入手やその説明文書が手に入ります。
<br>

* **PostgreSQL 9.6 with Parallel Query vs. TPC-H**
http://rhaas.blogspot.com/2016/04/postgresql-96-with-parallel-query-vs.html
PostgreSQL 9.6上でTPC-Hの実施検証が載っています。リンク先のGitにPostgreSQL向けのTPC-Hクエリや、PostgreSQLへの導入方法など載っています。
<br>

* **DBGENを使って、PostgreSQLにTPC-H測定用データを格納**
http://someone620.hatenablog.com/entry/2014/10/27/025535
TPC-HをPostgreSQL上で実施する方法について、日本語で提供された情報です。
<br>

* **TPC-H Queries on PostgreSQL**
http://myfpgablog.blogspot.com/2016/08/tpc-h-queries-on-postgresql.html
PostgreSQL上でTPC-Hを実施するための手順について記載されています。PostgreSQL向けに変換したSQLも載っています。
<br>