Historicamente, o PostgreSQL forneceu recursos de compilação na forma de compilação antecipada para funções PL/pgSQL e a versão 10 introduziu a compilação de expressões. Nenhum deles gera código de máquina.
JIT para SQL foi discutido muitos anos atrás, e para PostgreSQL o recurso é o resultado de uma mudança substancial no código.
Para verificar se o binário do PostgreSQL foi construído com suporte a LLVM, use o comando pg_configure para exibir os sinalizadores de compilação e procure por –with-llvm na saída. Exemplo para a distribuição PGDG RPM:
omiday ~ $ /usr/pgsql-11/bin/pg_config --configure
'--enable-rpath' '--prefix=/usr/pgsql-11' '--includedir=/usr/pgsql-11/include' '--mandir=/usr/pgsql-11/share/man' '--datadir=/usr/pgsql-11/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-11/doc' '--htmldir=/usr/pgsql-11/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'
Por que LLVM JIT?
Tudo começou há cerca de dois anos, conforme explicado no post de Adres Freund, quando a avaliação de expressão e a deformação de tupla provaram ser os obstáculos na aceleração de grandes consultas. Depois de adicionar a implementação do JIT “a avaliação da expressão em si é mais de dez vezes mais rápida do que antes” nas palavras de Andres. Além disso, a seção de perguntas e respostas que encerra sua postagem explica a escolha do LLVM em relação a outras implementações.
Enquanto LLVM foi o provedor escolhido, o parâmetro GUC jit_provider pode ser usado para apontar para outro provedor JIT. Observe, porém, que o suporte inlining só está disponível ao usar o provedor LLVM, devido à maneira como o processo de compilação funciona.
Quando fazer JIT?
A documentação é clara:consultas de longa duração vinculadas à CPU se beneficiarão da compilação JIT. Além disso, as discussões da lista de discussão mencionadas neste blog apontam que o JIT é muito caro para consultas que são executadas apenas uma vez.
Comparado às linguagens de programação, o PostgreSQL tem a vantagem de “saber” quando fazer JIT, contando com o planejador de consultas. Para esse efeito, foram introduzidos vários parâmetros GUC. Para proteger os usuários de surpresas negativas ao habilitar o JIT, os parâmetros relacionados ao custo são definidos intencionalmente para valores razoavelmente altos. Observe que definir os parâmetros de custo JIT como '0' forçará todas as consultas a serem compiladas por JIT e, como resultado, diminuirá a velocidade de todas as suas consultas.
Embora o JIT possa ser geralmente benéfico, há casos em que habilitá-lo pode ser prejudicial, conforme discutido no commit b9f2d4d3.
Como JIT?
Como mencionado acima, os pacotes binários RPM são habilitados para LLVM. No entanto, para que a compilação JIT funcione, algumas etapas adicionais são necessárias:
A saber:
[email protected][local]:54311 test# show server_version;
server_version
----------------
11.1
(1 row)
[email protected][local]:54311 test# show port;
port
-------
54311
(1 row)
[email protected][local]:54311 test# create table t1 (id serial);
CREATE TABLE
[email protected][local]:54311 test# insert INTO t1 (id) select * from generate_series(1, 10000000);
INSERT 0 10000000
[email protected][local]:54311 test# set jit = 'on';
SET
[email protected][local]:54311 test# set jit_above_cost = 10; set jit_inline_above_cost = 10; set jit_optimize_above_cost = 10;
SET
SET
SET
[email protected][local]:54311 test# 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)
Observe que habilitei o JIT (que é desabilitado por padrão após a discussão pgsql-hackers referenciada no commit b9f2d4d3). Também ajustei o custo dos parâmetros JIT conforme sugerido na documentação.
A primeira dica é encontrada no arquivo src/backend/jit/README referenciado na documentação JIT:
Which shared library is loaded is determined by the jit_provider GUC, defaulting to "llvmjit".
Como o pacote RPM não está puxando a dependência JIT automaticamente — como foi decidido após extensas discussões (veja o tópico completo) — precisamos instalá-lo manualmente:
[[email protected] ~]# dnf install postgresql11-llvmjit
Quando a instalação estiver concluída, podemos testar imediatamente:
[email protected][local]:54311 test# 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)
Também podemos exibir os detalhes do JIT por trabalhador:
[email protected][local]:54311 test# 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)
A implementação JIT também pode aproveitar o recurso de execução de consultas paralelas. Para exemplificar, primeiro vamos desabilitar a paralelização:
[email protected][local]:54311 test# set max_parallel_workers_per_gather = 0;
SET
[email protected][local]:54311 test# 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)
O mesmo comando com consultas paralelas habilitadas é concluído na metade do tempo:
[email protected][local]:54311 test# reset max_parallel_workers_per_gather ;
RESET
[email protected][local]:54311 test# 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)
Achei interessante comparar os resultados dos testes discutidos neste post, durante os estágios iniciais de implementação do JIT versus a versão final. Primeiro, certifique-se de que as condições do teste original sejam atendidas, ou seja, o banco de dados deve caber na memória:
[email protected][local]:54311 test# \l+
postgres | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | | 8027 kB | pg_default | default administrative connection database
template0 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/postgres +| 7889 kB | pg_default | unmodifiable empty database
| | | | | postgres=CTc/postgres | | |
template1 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/postgres +| 7889 kB | pg_default | default template for new databases
| | | | | postgres=CTc/postgres | | |
test | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | | 2763 MB | pg_default |
[email protected][local]:54311 test# show shared_buffers ;
3GB
Time: 0.485 ms
Baixe o whitepaper hoje PostgreSQL Management &Automation with ClusterControlSaiba o que você precisa saber para implantar, monitorar, gerenciar e dimensionar o PostgreSQLBaixe o whitepaper Execute os testes com o JIT desabilitado:
[email protected][local]:54311 test# set jit = off;
SET
Time: 0.483 ms
[email protected][local]:54311 test# select sum(c8) from t1;
0
Time: 1036.231 ms (00:01.036)
[email protected][local]:54311 test# select sum(c2), sum(c3), sum(c4), sum(c5),
sum(c6), sum(c7), sum(c8) from t1;
0 | 0 | 0 | 0 | 0 | 0 | 0
Time: 1793.502 ms (00:01.794)
Em seguida, execute os testes com o JIT ativado:
[email protected][local]:54311 test# set jit = on; set jit_above_cost = 10; set
jit_inline_above_cost = 10; set jit_optimize_above_cost = 10;
SET
Time: 0.473 ms
SET
Time: 0.267 ms
SET
Time: 0.204 ms
SET
Time: 0.162 ms
[email protected][local]:54311 test# select sum(c8) from t1;
0
Time: 795.746 ms
[email protected][local]:54311 test# select sum(c2), sum(c3), sum(c4), sum(c5),
sum(c6), sum(c7), sum(c8) from t1;
0 | 0 | 0 | 0 | 0 | 0 | 0
Time: 1080.446 ms (00:01.080)
Isso é uma aceleração de cerca de 25% para o primeiro caso de teste e 40% para o segundo!
Por fim, é importante lembrar que, para instruções preparadas, a compilação JIT é realizada quando a função é executada pela primeira vez.
Conclusão
Por padrão, a compilação JIT está desabilitada e, para sistemas baseados em RPM, o instalador não indicará a necessidade de instalar o pacote JIT fornecendo o bitcode para o provedor padrão LLVM.
Ao compilar a partir de fontes, preste atenção aos sinalizadores de compilação para evitar problemas de desempenho, por exemplo, se as asserções do LLVM estiverem habilitadas.
Conforme discutido na lista de hackers do pgsql, o impacto do JIT no custo ainda não é totalmente compreendido, portanto, é necessário um planejamento cuidadoso antes de habilitar o cluster de recursos em todo o cluster, pois as consultas que poderiam se beneficiar da compilação podem realmente ficar mais lentas. No entanto, o JIT pode ser ativado por consulta.
Para obter informações detalhadas sobre a implementação da compilação JIT, revise os logs do Git do projeto, os Commitfests e o thread de correio pgsql-hackers.
Feliz JIT!