PostgreSQL 教程: 即时编译 (JIT)

三月 25, 2025

摘要:在本教程中,您将学习如何在 PostgreSQL 中使用即时编译 (JIT) 优化 SQL 查询。

目录

介绍

很早前,PostgreSQL 以提前编译的形式为 PL/pgSQL 函数提供了编译功能,版本 10 引入了表达式编译。不过,这些都不会生成机器码。

要检查 PostgreSQL 二进制程序构建时是否启用了 LLVM 的支持,请使用pg_config命令显示编译选项,并在输出中查找 --with-llvm。下面是一个 PGDG RPM 发行版的示例:

$ /usr/pgsql-15/bin/pg_config --configure
'--enable-rpath' '--prefix=/usr/pgsql-15' '--includedir=/usr/pgsql-15/include' '--mandir=/usr/pgsql-15/share/man' '--datadir=/usr/pgsql-15/share' '--enable-tap-tests' '--with-icu' '--with-llvm' '--with-perl' '--with-python' '--with-tcl' '--with-tclconfig=/usr/lib64' '--with-openssl' '--with-pam' '--with-gssapi' '--with-includes=/usr/include' '--with-libraries=/usr/lib64' '--enable-nls' '--enable-dtrace' '--with-uuid=e2fs' '--with-libxml' '--with-libxslt' '--with-ldap' '--with-selinux' '--with-systemd' '--with-system-tzdata=/usr/share/zoneinfo' '--sysconfdir=/etc/sysconfig/pgsql' '--docdir=/usr/pgsql-15/doc' '--htmldir=/usr/pgsql-15/doc/html' 'CFLAGS=-O2 -g -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fexceptions -fstack-protector-strong -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection' 'PKG_CONFIG_PATH=:/usr/lib64/pkgconfig:/usr/share/pkgconfig'

为什么选择 LLVM JIT?

它始于社区中的一场讨论,当时表达式求值和元组解析的过程被证明是加速大型查询的障碍。据社区成员反馈,在实现 JIT 特性后,“表达式求值本身比以前快了十倍以上”。此外,在帖子结尾的问答部分,解释了之所以选择 LLVM 而不是其他类似组件的原因。

虽然 LLVM 是所选的即时编译组件,但 GUC 参数 jit_provider 也可用于指向其他的 JIT 编译组件。但请注意,由于构建过程的工作方式,仅在使用 LLVM 作为即时编译组件时才能支持内联。

何时采用 JIT?

文档中有清楚的说明:长时间运行且受 CPU 限制的查询可受益于 JIT 编译。此外,本文中引用的邮件列表讨论指出,对于仅执行一次的查询来说,JIT 编译的成本太高。

与编程语言相比,PostgreSQL 的优势在于它依赖查询规划器来“判断”何时使用 JIT。为此,引入了许多了 GUC 参数。为了保护用户在启用 JIT 时免受意外的影响,这些成本相关的参数被有意地设置为相当高的值。请注意,将 JIT 成本参数设置为 “0”,会强制所有查询进行 JIT 编译,进而降低所有查询的速度。

虽然 JIT 编译通常是有益的,但正如邮件列表中所讨论的那样,启用它在某些情况下可能会有害。

如何使用 JIT?

如上所述,RPM 二进制包支持 LLVM。但是,为了让 JIT 编译能正常运行,还需要执行一些额外的步骤:

show server_version;
 server_version
----------------
 15.1
(1 row)

show port;
 port
-------
 54311
(1 row)

create table t1 (id serial);
insert INTO t1 (id) select * from generate_series(1, 10000000);
set jit = 'on';
set jit_above_cost = 10;
set jit_inline_above_cost = 10;
set jit_optimize_above_cost = 10;

explain analyze select count(*) from t1;
                                                               QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=97331.43..97331.44 rows=1 width=8) (actual time=647.585..647.585 rows=1 loops=1)
   ->  Gather  (cost=97331.21..97331.42 rows=2 width=8) (actual time=647.484..649.059 rows=3 loops=1)
         Workers Planned: 2
         Workers Launched: 2
         ->  Partial Aggregate  (cost=96331.21..96331.22 rows=1 width=8) (actual time=640.995..640.995 rows=1 loops=3)
               ->  Parallel Seq Scan on t1  (cost=0.00..85914.57 rows=4166657 width=0) (actual time=0.060..397.121 rows=3333333 loops=3)
 Planning Time: 0.182 ms
 Execution Time: 649.170 ms
(8 rows)

请注意,我们在上面启用了 JIT(根据 pgsql-hackers 邮件列表中引用的讨论,默认情况下是禁用的)。然后,我们还根据文档中的建议调整了 JIT 参数的成本

根据 JIT 文档中的引用,可以在 src/backend/jit/README 文件中找到的第一个提示是:

提示:加载哪个共享库是由 GUC 参数 jit_provider 决定的,默认为 “llvmjit”。

由于 RPM 软件包没有自动地纳入 JIT 依赖组件,这是经过广泛讨论后决定的(请参见完整的帖子),我们需要手动来安装它:

dnf install postgresql15-llvmjit

安装完成后,我们可以立即测试了:

explain analyze select count(*) from t1;
                                                               QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=97331.43..97331.44 rows=1 width=8) (actual time=794.998..794.998 rows=1 loops=1)
   ->  Gather  (cost=97331.21..97331.42 rows=2 width=8) (actual time=794.870..803.680 rows=3 loops=1)
         Workers Planned: 2
         Workers Launched: 2
         ->  Partial Aggregate  (cost=96331.21..96331.22 rows=1 width=8) (actual time=689.124..689.125 rows=1 loops=3)
               ->  Parallel Seq Scan on t1  (cost=0.00..85914.57 rows=4166657 width=0) (actual time=0.062..385.278 rows=3333333 loops=3)
 Planning Time: 0.150 ms
 JIT:
   Functions: 4
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 2.146 ms, Inlining 117.725 ms, Optimization 47.928 ms, Emission 69.454 ms, Total 237.252 ms
 Execution Time: 803.789 ms
(12 rows)

我们还可以显示每个工作进程使用 JIT 的详细情况

explain (analyze, verbose, buffers) select count(*) from t1;
                                                                  QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=97331.43..97331.44 rows=1 width=8) (actual time=974.352..974.352 rows=1 loops=1)
   Output: count(*)
   Buffers: shared hit=2592 read=41656
   ->  Gather  (cost=97331.21..97331.42 rows=2 width=8) (actual time=974.166..980.942 rows=3 loops=1)
         Output: (PARTIAL count(*))
         Workers Planned: 2
         Workers Launched: 2
         JIT for worker 0:
         Functions: 2
         Options: Inlining true, Optimization true, Expressions true, Deforming true
         Timing: Generation 0.378 ms, Inlining 74.033 ms, Optimization 11.979 ms, Emission 9.470 ms, Total 95.861 ms
         JIT for worker 1:
         Functions: 2
         Options: Inlining true, Optimization true, Expressions true, Deforming true
         Timing: Generation 0.319 ms, Inlining 68.198 ms, Optimization 8.827 ms, Emission 9.580 ms, Total 86.924 ms
         Buffers: shared hit=2592 read=41656
         ->  Partial Aggregate  (cost=96331.21..96331.22 rows=1 width=8) (actual time=924.936..924.936 rows=1 loops=3)
               Output: PARTIAL count(*)
               Buffers: shared hit=2592 read=41656
               Worker 0: actual time=900.612..900.613 rows=1 loops=1
               Buffers: shared hit=668 read=11419
               Worker 1: actual time=900.763..900.763 rows=1 loops=1
               Buffers: shared hit=679 read=11608
               ->  Parallel Seq Scan on public.t1  (cost=0.00..85914.57 rows=4166657 width=0) (actual time=0.311..558.192 rows=3333333 loops=3)
                     Output: id
                     Buffers: shared hit=2592 read=41656
                     Worker 0: actual time=0.389..539.796 rows=2731662 loops=1
                     Buffers: shared hit=668 read=11419
                     Worker 1: actual time=0.082..548.518 rows=2776862 loops=1
                     Buffers: shared hit=679 read=11608
 Planning Time: 0.207 ms
 JIT:
   Functions: 9
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 8.818 ms, Inlining 153.087 ms, Optimization 77.999 ms, Emission 64.884 ms, Total 304.787 ms
 Execution Time: 989.360 ms
(36 rows)

JIT 的实现还可以利用并行查询执行的能力。为了举例说明,首先让我们禁用并行查询:

set max_parallel_workers_per_gather = 0;

explain analyze select count(*) from t1;
                                                      QUERY PLAN
----------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=169247.71..169247.72 rows=1 width=8) (actual time=1447.315..1447.315 rows=1 loops=1)
   ->  Seq Scan on t1  (cost=0.00..144247.77 rows=9999977 width=0) (actual time=0.064..957.563 rows=10000000 loops=1)
 Planning Time: 0.053 ms
 JIT:
   Functions: 2
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 0.388 ms, Inlining 1.359 ms, Optimization 7.626 ms, Emission 7.963 ms, Total 17.335 ms
 Execution Time: 1447.783 ms
(8 rows)

在启用并行查询的情况下,相同的 SQL 可以在一半的时间内完成:

reset max_parallel_workers_per_gather;

explain analyze select count(*) from t1;
                                                               QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=97331.43..97331.44 rows=1 width=8) (actual time=707.126..707.126 rows=1 loops=1)
   ->  Gather  (cost=97331.21..97331.42 rows=2 width=8) (actual time=706.971..712.199 rows=3 loops=1)
         Workers Planned: 2
         Workers Launched: 2
         ->  Partial Aggregate  (cost=96331.21..96331.22 rows=1 width=8) (actual time=656.102..656.103 rows=1 loops=3)
               ->  Parallel Seq Scan on t1  (cost=0.00..85914.57 rows=4166657 width=0) (actual time=0.067..384.207 rows=3333333 loops=3)
 Planning Time: 0.158 ms
 JIT:
   Functions: 9
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 3.709 ms, Inlining 142.150 ms, Optimization 50.983 ms, Emission 33.792 ms, Total 230.634 ms
 Execution Time: 715.226 ms
(12 rows)

下面让我们来看看 JIT 即时编译的效果。在禁用 JIT 的情况下运行测试:

set jit = off;

select sum(id) from t1;
      sum
----------------
 50000005000000
(1 row)
Time: 1036.231 ms

接下来在启用 JIT 的情况下运行测试:

set jit = on;
set jit_above_cost = 10;
set jit_inline_above_cost = 10;
set jit_optimize_above_cost = 10;

select sum(id) from t1;
      sum
----------------
 50000005000000
(1 row)
Time: 795.746 ms

对于该测试用例,它提升了大约 25% 的性能!

最后,需要记住的是,对于预备语句,JIT 编译是在第一次执行函数时发生的。

结论

默认情况下,JIT 编译是禁用的,对于采用 RPM 软件包的系统,安装程序不会告诉你,需要安装 JIT 组件包作为默认 LLVM 编译组件,为程序生成机器码。

从源代码构建 PostgreSQL 时,请注意编译选项以避免性能问题,例如是否启用了 LLVM 断言

正如 pgsql-hackers 列表中所讨论的,启用 JIT 带来的影响尚未完全了解,因此在集群范围内启用该特性之前需要仔细规划,因为原本可以从编译中受益的查询实际上可能会运行得更慢。但是,可以针对每个查询来启用 JIT。

有关 JIT 编译实现的详细信息,请查看项目的 Git 日志Commitfestpgsql-hackers 邮件讨论。

了解更多

PostgreSQL 优化