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);