LibJIT

Материал из Национальной библиотеки им. Н. Э. Баумана
Последнее изменение этой страницы: 11:21, 18 мая 2017.
LibJIT
Создатели: Риз Везерли, Норберт Боллоу
Разработчики: Открытое программное обеспечение
Постоянный выпуск: 0.1.2 / 12 December 2008 года; 13 years ago (2008-12-12)
Платформа: Кросс-платформенное программное обеспечение
Тип ПО: Библиотека для JIT-компиляции
Лицензия: GNU LGPL
Веб-сайт gnu.org/software/libjit/

LibJIT — библиотека для JIT-компиляции, позволяет компилировать фрагменты байт кода машинный код во время исполнения программ.

Первоначально создана Ризом Везерли и Норбертом Боллоу для Фонда Свободного Программного Обеспечения в рамках проекта DotGNU. Позже Libjit разрабатывался Кириллом Кононенко, Клаусом Трейчелом, Алексеем Демаковым. Дизайн библиотеки Libjit содержит обширный набор средств, которые заботятся о процессе компиляции во время выполнения программы, не связывая программиста с языком или специфическими особенностями байт-кода.

Введение

JIT-компиляция становится все более популярным средством для выполнения кода, написанного на динамических языках, таких как Perl и Python, и полудинамических языках, таких как Java и C#. Исследования показали, что JIT-компиляция по скорости вплотную приближается к производительности скомпилированного кода, а иногда и превосходит её.

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

JIT-компиляция полезна не только в сфере интерпретируемых языков. Её преимущества могут быть задействованы во многих других задачах. Графические приложения могут повысить производительность, если они будут "на лету" компилировать специальные методы, необходимые в лданный момент, а отказавшись от статических. Кроме того, таким приложениям не требуются модели объектов, сборщики мусора или огромный класс динамических библиотек.

Большая часть работы над JIT-компиляцией связана с арифметическими преобразованиями, числовыми преобразованиями, записью в память и чтением из памяти, работой с циклами, анализом потока данных, установкой регистров и генерированием исполняемого машинного код. Только очень небольшая часть работы связана с особенностями языка.

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

В отличие от других систем, таких как Java Virtual Machine (JVM), .Net и Parrot, libjit — это не ещё одна собственная виртуальная машина. Это фундамент, на котором может быть построено большое количество различных виртуальная машина, динамических скриптовых языков или собственных настраиваемых методов для рендеринга.

Проект llvm имеет схожие черты с libjit в том, что его промежуточный формат является универсальным для фронтэнд языков. Он написан на C++ и предоставляет большой набор компонентов для создания и оптимизации компилятора; гораздо больше, чем предоставляет собственно libjit. По словам его автора, Крис Латтнер, часть его возможностей может быть использована и для создания JIT-компиляторов.

Libjit должен позволить разработчикам думать о создании фронтэнда, не погружаясь в детали исполнения кода. Вместе с этим, эксперты в области проектирования и внедрения JIT-компиляторов могут сосредоточиться на решении проблем в исполнении кода.

Особенности

  1. Основной интерфейс библиотеки реализован на языке C для максимального облегчения повторного использования кода. Для программистов на C++ доступен интерфейс с классами.
  2. Переносимость на все основные 32-битные и 64-битные платформы.
  3. Трехадресный API для пользователей библиотеки. Он обеспечивает достаточную непрозрачность, благодаря чему остальные представления могут быть использованы в будущем внутри библиотеки, не завтрагивая существующих пользователей.
  4. Статическая или динамическая компиляция любой функции.
  5. Встроенная поддержка перекомпиляции функций для улучшения оптимизации. Вызовы автоматически перенаправляются на новую версию.
  6. Альтернативный интерпретатор для запуска кода на платформах, на которых в данный момент не доступен нативный генератор кода. Это снижает необходимость писать собственный интерпритатор для таких платформ.
  7. Для множества платформ поддерживаются арифметические, побитовые операторы, операторы преобразования типов для 8-битных, 16-битных, 32-битных и 64-битных целых и 32-битных и 64-битных или более чисел с плавающей точкой.
  8. Большой набор математических и тригонометрических операций (sqrt, sin, cos, min, abs и т.д.) для встраивания библиотечных функций с плавающей точкой.
  9. Упрощенная схема типов и механизмы обработки исключений, благодаря чему может быть построено множество различных объектов.
  10. Поддерживаются вложенные функции, предоставляется доступ к локальным переменным родителей (для обеспечения выполнения Паскаль-подобных языков).

Пример использования

В данном примере мы создадим и скомпилируем следующую функцию:

int mul_add(int x, int y, int z)
{
    return x * y + z;
}

Чтобы использовать libjit, в первую очередь объявим соответствующие заголовочные файлы <jit/jit.h>:

#include <jit/jit.h>

Все заголовочные файлы расположены в поддиректории jit, чтобы их можно было легко отличить от обычных системных заголовочных файлов. После установки libjit заголовочные файлы обычно можно найти в /usr/local/include/jit или /usr/include/jit, в зависимости от вашей системы. Кроме того, мы должны использовать линковщик с флагом -ljit.

Каждая программа, использующая libjit, должна вызывать jit_context_create:

jit_context_t context;
...
context = jit_context_create();

Практически всё, что делается с помощью libjit, делается в некотором контексте. В частности, в контексте содержатся все функции, которые мы написали и скомпилировали.

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

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

jit_context_build_start(context);

В качестве следующего шага мы должны создать объект функции, которая представляет нашу функцию mul_add.

jit_function_t function;
...
function = jit_function_create(context, signature);

В качестве сигнатуры используется объект jit_type_t, который описывает параметры функции и возвращаемое значение. Она указывает libjit, как генерировать необходимый порядок вызывов для этой функции.

jit_type_t params[3];
jit_type_t signature;
...
params[0] = jit_type_int;
params[1] = jit_type_int;
params[2] = jit_type_int;
signature = jit_type_create_signature
    (jit_abi_cdecl, jit_type_int, params, 3, 1);

В коде выше декларируется функция, которая принимает три параметра типа int и возвращает int. Мы указываем, что функция использует бинарный интерфейс приложения (application binary interface, ABI) cdecl, который задаёт обычный порядок вызывов стандарта языка С. Более подробную информацию о типах сигнатур вы можете найти в официальной документации в разделе Manipulating system types.[1]

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

jit_value_t x, y, z;
...
x = jit_value_get_param(function, 0);
y = jit_value_get_param(function, 1);
z = jit_value_get_param(function, 2);

Значения — это один из двух краеугольных камней процесса libjit. Значения представляются из себя параметры, локальные переменные и временные промежуточные результаты. Получив параметры, мы вычисляем результат выражения x * y + z следующим образом:

jit_value_t temp1, temp2;
...
temp1 = jit_insn_mul(function, x, y);
temp2 = jit_insn_add(function, temp1, z);

Здесь показан еще один краеугольный камень процесса libjit: игнструкции. Каждая из этих инструкций прнимает два аргумента и возвращает новую временную переменную с результатом.

Студенты, знакомые с устройством компилятора, должны заметить, что приведённые выше выражени я выглядят очень подозрительно словно "три адрессных состояния", описываемые в книгах о компиляторах. И это приоткрывает тайну того, что на самом деле скрыто в libjit.

Не беспокойтесь, если вы не знаете, что значат эти три выражения. Библиотека скрывает от вас множество деталей. Всё, что вам нужно сделать, это представить ваш код как последовательность из простых операций (сложение, умножение, взятие обратного, копирование и т.д.). Затем выполняйте шаги по порядку, используя временные переменные в подпоследовательностях операций. Более подробную информацию обо всех инструкциях, поддерживаемых libjit, вы можете найти в официальной документации в разделе Working with instructions in the JIT.[2]

Теперь, когда мы вычислили искомый результат, мы возвращаем его вызывающей функции, используя jit_insn_return

jit_insn_return(function, temp2);

Мы закончили конструировать тело функции. Теперь мы компилируем её в исполняемую форму:

jit_function_compile(function);
jit_context_build_end(context);

Кроме того, оно удалит всю память, связанную со значениями и инструкциями, которые мы задействовали в процессе построения функции. Они нам больше не понадобятся, потому что теперь у нас есть требуемая исполняемая форма.

Также мы разблокируем контекст, потому что это небезопасно, когда другие потоки получают доступ к процессу построения функции.

До этого момента мы не исполняли нашу функцию mul_add. Всё, что нам требовалось, уже сконструировано и скомпилировано, так что теперь нам лишь осталось вызвать функцию jit_function_apply:

jit_int arg1, arg2, arg3;
void *args[3];
jit_int result;
...
arg1 = 3;
arg2 = 5;
arg3 = 2;
args[0] = &arg1;
args[1] = &arg2;
args[2] = &arg3;
jit_function_apply(function, args, &result);
printf("mul_add(3, 5, 2) = %d\n", (int)result);

Мы передаём массив указателей в jit_function_apply, каждый из них указывает на соответствующее значение аргумента. Это демонстрирует нам общий смысл механизма, с помощью которого можно создать и скомпилировать любую функцию используя libjit. В случае успеха программа выведет следующее:

mul_add(3, 5, 2) = 17

Следует заметить, что мы использовали jit_int, а не int, как тип аргумента. Тип jit_int гарантированно содержит 32 бита на всех платформах, в то время как размер типа int может варьироваться от платформы к платформе. Мы используем тип с предсказуемым размером, чтобы быть уверенными, что наша функция работает одинаково на всех платформах.

Если вам действительно необходим системный тип int, вы должны использовать jit_type_sys_int вместо jit_type_int во время создания сигнатуры функции. Тип jit_type_sys_int гарантирует согласованность с точностью локального системного типа int.

Наконец, мы очищаем контекст и всю использованную память:

jit_context_destroy(context);


Примечания

  1. http://www.gnu.org/software/libjit/doc/libjit_6.html#Types LibJIT documentation: Manipulating system types
  2. http://www.gnu.org/software/libjit/doc/libjit_8.html#Instructions LibJIT documentation: Working with instructions in the JIT

Источники

456