Ответ 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 (или, возможно, на С++).