Подтвердить что ты не робот

Запись JIT-компилятора в сборке

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

    ; 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-кеша).

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

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

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

Ответ 2

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