JIT-компиляция

Что такое JIT-компиляция?

JIT-компиляция (Just-in-time compilation, компиляция «на лету») — это процесс, преобразующий некоторую форму интерпретируемого вычисления программы в программу, оптимизированную для используемого процессора, и делающий это во время выполнения. Например, вместо использования универсального кода, способного вычислять произвольные выражения SQL, для вычисления конкретного условия SQL вроде WHERE a.col = 3, можно сгенерировать функцию, специфичную для этого выражения, которую может выполнять собственно ЦП, тем самым повышая производительность.

В QHB имеется встроенная поддержка JIT-компиляции с использованием LLVM, которая включается, если QHB собирается с ключом --with-llvm.


Операции, ускоряемые JIT-компиляцией

В настоящее время реализация JIT-компиляции в QHB поддерживает ускорение вычисления выражений и переформирование кортежей. В будущем могут быть ускорены и некоторые другие операции.

Вычисление выражений используется для вычисления предложений WHERE, целевых списков, агрегатов и проекций. Его можно ускорить, сгенерировав код, специфичный для каждого отдельного случая.

Переформирование кортежа — это процесс трансформации дискового кортежа (см. подраздел Структура строк таблицы) в его представление в памяти. Его можно ускорить, создав функцию, специфичную для определенной структуры таблицы и количества извлекаемых столбцов.


Встраивание

QHB очень гибка и позволяет определять новые типы данных, функции, операторы и другие объекты базы данных; см. главу Расширение SQL. На самом деле встроенные объекты реализуются с помощью практически тех же механизмов. Такая гибкость подразумевает некоторые издержки, например, вследствие вызовов функций (см. раздел Пользовательские функции). Чтобы снизить эти издержки, JIT-компиляция может встраивать тела маленьких функций в использующие их выражения. Это позволяет оптимизировать значительный процент издержек.


Оптимизация

В LLVM поддерживается оптимизация сгенерированного кода. Некоторые оптимизации достаточно дешевы, чтобы выполняться при каждом использовании JIT-компиляции, тогда как другие выгодны только для длительно выполняющихся запросов. Подробнее об оптимизации рассказывается здесь.



Когда применять JIT-компиляцию?

JIT-компиляция выгодна в первую очередь для длительных запросов, зависящих от быстродействия ЦП. Чаще всего это аналитические запросы. Для коротких запросов дополнительные затраты на выполнение JIT-компиляции зачастую будут превышать сэкономленное этим время.

Определение целесообразности применения JIT-компиляции отталкивается от общей оценочной стоимости запроса (см. главу Как планировщик использует статистику и подраздел Константы стоимости для планировщика). Оценочная стоимость запроса будет сравниваться со значением jit_above_cost. Если стоимость выше, JIT-компиляция будет проведена. Затем нужно принять еще два решения. Во-первых, если оценочная стоимость превышает значение jit_inline_above_cost, используемые в запросе короткие функции и операторы будут встроены в код. Во-вторых, если оценочная стоимость превышает значение jit_optimize_above_cost, для улучшения сгенерированного кода применяются дорогостоящие оптимизации. Каждый из этих вариантов увеличивает издержки JIT-компиляции, но может значительно сократить время выполнения запроса.

Эти основанные на стоимости решения будут приниматься во время планирования, а не выполнения запроса. Это означает, что при использовании подготовленных операторов и общего плана (см. PREPARE) на решения влияют значения конфигурационных параметров, действующие во время подготовки запроса, а не во время выполнения.

Примечание
Если в параметре jit установлено значение off (выключен) или реализация JIT-компиляции недоступна (например, потому что сервер был скомпилирован без --with-llvm), JIT-компиляция выполняться не будет, даже если она была бы полезна, исходя из вышеуказанных критериев. Заданное в jit значение off действует как во время планирования, так и во время выполнения.

Для того чтобы проверить, используется ли JIT-компиляция, можно воспользоваться командой EXPLAIN. Например, ниже приводится запрос, не использующий JIT-компиляцию:

=# EXPLAIN ANALYZE SELECT SUM(relpages) FROM pg_class;
                                                 QUERY PLAN
-------------------------------------------------------------------​------------------------------------------
 Aggregate  (cost=16.27..16.29 rows=1 width=8) (actual time=0.303..0.303 rows=1 loops=1)
   ->  Seq Scan on pg_class  (cost=0.00..15.42 rows=342 width=4) (actual time=0.017..0.111 rows=356 loops=1)
 Planning Time: 0.116 ms
 Execution Time: 0.365 ms
(4 rows)

Учитывая стоимость планирования, отказ от JIT был вполне обоснован; стоимость JIT-компиляции была бы выше потенциальной экономии ресурсов. Если скорректировать ограничение стоимости, JIT-компиляция будет использоваться:

=# SET jit_above_cost = 10;
SET
=# EXPLAIN ANALYZE SELECT SUM(relpages) FROM pg_class;
                                                 QUERY PLAN
-------------------------------------------------------------------​------------------------------------------
 Aggregate  (cost=16.27..16.29 rows=1 width=8) (actual time=6.049..6.049 rows=1 loops=1)
   ->  Seq Scan on pg_class  (cost=0.00..15.42 rows=342 width=4) (actual time=0.019..0.052 rows=356 loops=1)
 Planning Time: 0.133 ms
 JIT:
   Functions: 3
   Options: Inlining false, Optimization false, Expressions true, Deforming true
   Timing: Generation 1.259 ms, Inlining 0.000 ms, Optimization 0.797 ms, Emission 5.048 ms, Total 7.104 ms
 Execution Time: 7.416 ms

Как видно в этом примере, JIT-компиляция была использована, а встраивание и дорогостоящая оптимизация — нет. Они бы использовались, если бы параметры jit_inline_above_cost или jit_optimize_above_cost тоже были уменьшены.



Конфигурация

Переменная конфигурации jit определяет, активна или неактивна JIT-компиляция. Если она включена, конфигурационные переменные jit_above_cost, jit_inline_above_cost и jit_optimize_above_cost определяют, будет ли JIT-компиляция выполняться для запрос, и сколько на это будет затрачено ресурсов.

Параметр jit_provider определяет, какая реализация JIT-компиляции будет использоваться. Необходимость изменять его возникает редко. См. подраздел Подключаемые провайдеры JIT-компиляции.

Существует также несколько дополнительных параметров конфигурации, предназначенных для целей разработки и отладки; они описаны в разделе Параметры для разработчиков.



Расширяемость

Поддержка встраивания для расширений

Реализация JIT-компиляции в QHB может встраивать в код тела функций типов C и internal, а также операторов на основе этих функций. Чтобы это работало для функций в расширениях, нужно сделать доступными определения этих функций. При использовании PGXS для сборки расширения для сервера, скомпилированного с JIT-поддержкой LLVM, соответствующие файлы будут собираться и устанавливаться автоматически.

Соответствующие файлы должны устанавливаться в $pkglibdir/bitcode/$extension/, а краткая информация о них должна вноситься в $pkglibdir/bitcode/ $extension.index.bc, где $pkglibdir — это каталог, возвращаемый командой qhb_config --pkglibdir, а $extension — это базовое имя разделяемой библиотеки расширения.

Примечание
Для функций, встроенных в саму QHB, двоичный код устанавливается в $pkglibdir/bitcode/qhb.


Подключаемые провайдеры JIT-компиляции

QHB предоставляет реализацию JIT-компиляции на базе LLVM. Интерфейс провайдера JIT-компиляции позволяет динамически подключать его и менять без перекомпиляции (хотя в настоящее время процесс сборки предоставляет данные для поддержки встраивания только для LLVM). Активный провайдер выбирается посредством параметра jit_provider.

Интерфейс провайдера JIT-компиляции

Провайдер JIT-компиляции загружается путем динамической загрузки указанной разделяемой библиотеки. Для ее нахождения используется обычный путь поиска библиотек. Чтобы предоставить требуемые функции обратного вызова провайдера JIT-компиляции и показать, что эта библиотека действительно является провайдером JIT, она должна иметь функцию на C с именем _PG_jit_provider_init. Этой функции передается структура, которую нужно заполнить указателями на функции обратного вызова для определенных действий:

struct JitProviderCallbacks
{
    JitProviderResetAfterErrorCB reset_after_error;
    JitProviderReleaseContextCB release_context;
    JitProviderCompileExprCB compile_expr;
};

extern void _PG_jit_provider_init(JitProviderCallbacks *cb);