Основы Just In Time компиляции, используемой в динамических языках, на примере программы на C

Я был сильно вдохновлен, когда узнал о динамической компиляции (JIT — Just In Time) из различных виртуальных машин Ruby и JavaScript. Я мог бы рассказать вам все о том, как работает «компиляция на лету» и как она может дать прирост в производительности для вашего интерпретируемого языка. Это было бы здорово. Но проблема в том, что я никогда не мог понять и, тем более, предположить, как работает JIT-компиляция.

Как вообще возможно компилировать код во время его выполнения? Я спросил Бенджамина Петерсона о том, где он так много узнал о технологии JIT. В ответ он меня направил к проекту pypy (это исходники Python JIT). Но изучение проекта заняло бы довольно много времени, а я всего лишь хотел простой пример для понимания работы технологии.

К счастью, у меня есть отличная работа, где я могу встретиться лицом к лицу с теми людьми, которые занимаются производством JIT-компиляторов. Часть прошедшей недели я провел на конференции GDC (Game Developers Conference), где увидел демку игры, разработанную на движке Unreal Engine 3 и запущенную в браузере. Ребята сделали большую работу, создавая эту демку, и я обязательно напишу о ней более подробно чуть позже. Однако Люк Вагнер (Luke Wagner) — JavaScript-программист из Mozilla — оптимизировал asm.js, добавив OdinMonkeys в SpiderMonkeys.

Люк очень дружелюбный человек. Сначала я слушал его разговоры с Дейвом Херманом и Алоном Закаи, но потом задал ему интересующий меня вопрос. Люк очень доступно все мне объяснил. Скомпилировать простой объектный файл, использовать objdump для получения специфичной для конкретной платформы сборки, использовать системный вызов mmap для выделения памяти, которую вы можете прочитать И выполнить, скопировать инструкции в этот буфер, привести его к типу указателя на функцию и, в конце концов, вызвать его.

Итак, я написал простейшую функцию перемножения двух целых чисел. Первое, что я делаю, это создаю простой .c файл, затем компилирую его с флагами -c и -o для получения объектного файла.

Кстати, я работаю на 64-битной OS X. Таким образом, моя сборка может отличаться от вашей. Очевидно, что JIT-компиляция абстрагируется от платформозависимости, но, как бы то ни было, все инструкции в этой статье работают как на x86, так и на x64. Дальше нужно будет воспользоваться утилитами по работе с бинарными файлами . У меня ее не было, поэтому я установил эту утилиту при помощи команды brew install binutils.

Если у вас уже есть необходимые утилиты (в том числе [g]objdump), переходим к следующему шагу: чтение машинного кода из объектного файла, представленный в шестнадцатеричном формате. При запуске gobjdump -j .text -d mul.o -M intel вы должны получить приблизительно такое (приблизительно, потому что у нас могут быть разные платформы):

Итак, эти инструкции различаются по размеру. Я не знаю, как это будет выглядеть на платформе x86, поэтому не могу подробно комментировать эти строки. Однако очевидно, что это пары шестнадцатеричных цифр. 162 == 28 означает, что каждая пара шестнадцатеричных цифр может быть представлена одним байтом (тип char). Таким образом, мы можем записать все эти числа в массив unsigned char [].

Справочник man подробно описывает все забавные флаги функции mmap(). Примечательно, что такой способ выделения памяти может делать ее исполняемой. Таким свойством не обладает выделение памяти при помощи функции alloc(). Наверняка разработчики JavaScript каким-то образом пользуются такой возможностью.

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

Вуаля! Получилось очень аккуратно, а главное — оно работает! Помощь Люка Вагнера, час работы и эта статья в частности — вот то, что понадобилось для реализации технологии JIT.

Опять же, это всего лишь простой пример, причем супер непортативный. Более того, те операции, которые мы выполняем над памятью, они небезопасные. Но самое главное — теперь я знаю основы JIT-компиляции. И вы тоже!

Перевод статьи «Basic JIT»