Написание JIT-компилятора в сборке - PullRequest
23 голосов
/ 18 февраля 2011

Я написал виртуальную машину на C, которая имеет приличную производительность для не-JIT VM, но я хочу изучить что-то новое и улучшить производительность. Моя текущая реализация просто использует переключатель для перевода из байт-кода виртуальной машины в инструкции, которые компилируются в таблицу переходов. Как я уже сказал, достойная производительность, какая она есть, но я столкнулся с барьером, который можно преодолеть только с помощью JIT-компилятора.

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

Итак, моя цель - написать JIT-компилятор для этой виртуальной машины C, и я хочу сделать это в сборке x86. (Я использую NASM в качестве моего ассемблера) Я не совсем уверен, как это сделать. Я хорошо разбираюсь в ассемблере, и я просмотрел несколько примеров самоизменяющегося кода, но я пока не пришел, чтобы понять, как сделать генерацию кода.

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

Мне сообщили о нескольких библиотеках, которые облегчили бы эту задачу, таких как GNU Lightning и даже LLVM. Однако я хотел бы сначала написать это вручную, чтобы понять, как это работает, прежде чем использовать внешние ресурсы.

Существуют ли какие-либо ресурсы или примеры, которые это сообщество может предоставить, чтобы помочь мне начать выполнение этой задачи? Простой пример, показывающий две или три инструкции, такие как «add» и «mov», используемые для генерации исполняемого кода, с аргументами, динамически в памяти, может творить чудеса.

Ответы [ 2 ]

18 голосов
/ 18 февраля 2011

Я бы вообще не рекомендовал писать JIT в сборке. Существуют веские аргументы для написания наиболее часто выполняемых битов интерпретатора в сборке. Пример того, как это выглядит, см. В комментарии Майка Полла , автора LuaJIT.

Что касается JIT, есть много разных уровней с различной сложностью:

  1. Скомпилируйте базовый блок (последовательность не ветвящихся команд), просто скопировав код интерпретатора. Например, реализации нескольких (основанных на регистре) инструкций байт-кода могут выглядеть следующим образом:

    ; ebp points to virtual register 0 on the stack
    instr_ADD:
        <decode instruction>
        mov eax, [ebp + ecx * 4]  ; load first operand from stack
        add eax, [ebp + edx * 4]  ; add second operand from stack
        mov [ebp + ebx * 4], eax  ; write back result
        <dispatch next instruction>
    instr_SUB:
        ... ; similar
    

    Итак, с учетом последовательности команд ADD R3, R1, R2, SUB R3, R3, R4 простой JIT может скопировать соответствующие части реализации интерпретаторов в новый фрагмент машинного кода:

        mov ecx, 1
        mov edx, 2
        mov ebx, 3
        mov eax, [ebp + ecx * 4]  ; load first operand from stack
        add eax, [ebp + edx * 4]  ; add second operand from stack
        mov [ebp + ebx * 4], eax  ; write back result
        mov ecx, 3
        mov edx, 4
        mov ebx, 3
        mov eax, [ebp + ecx * 4]  ; load first operand from stack
        sub eax, [ebp + edx * 4]  ; add second operand from stack
        mov [ebp + ebx * 4], eax  ; write back result
    

    Это просто копирует соответствующий код, поэтому нам нужно инициализировать регистры, используемые соответствующим образом. Лучшим решением было бы перевести это непосредственно в машинные инструкции mov eax, [ebp + 4], но теперь вам уже нужно вручную кодировать запрошенные инструкции.

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

  2. Хотя некоторые JIT используют вышеупомянутый метод вместо интерпретатора, они затем используют более сложный механизм оптимизации для часто выполняемого кода. Это включает в себя перевод исполняемого байт-кода в промежуточное представление (IR), для которого выполняются дополнительные оптимизации.

    В зависимости от исходного языка и типа JIT, это может быть очень сложно (именно поэтому многие JIT делегируют эту задачу LLVM). JIT на основе методов должен иметь дело с объединением графов управления, поэтому они используют форму SSA и проводят различные анализы (например, Hotspot).

    Трассирующий JIT (например, LuaJIT 2) компилирует только код прямой линии, что облегчает реализацию многих вещей, но вы должны быть очень осторожны при выборе трасс и эффективном связывании нескольких трасс. Гал и Франц описывают один метод в этой статье (PDF) . Для другого метода см. Исходный код LuaJIT. Оба JIT написаны на C (или, возможно, C ++).

7 голосов
/ 28 июля 2011

Я предлагаю вам взглянуть на проект http://code.google.com/p/asmjit/. Используя предоставляемый фреймворк, вы можете сэкономить много энергии.Если вы хотите написать все вручную, просто прочитайте источник и переписайте его сами, я думаю, что это не очень сложно.

...