Компилятор, интерпретатор и JIT

Три способа исполнить код: перевести заранее, читать на лету или компилировать прямо во время работы.

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

Есть три классических подхода к тому, как «оживить» текст программы. Они не взаимоисключающие — современные языки часто комбинируют их.

Компилятор: перевести всё заранее

Компилятор (как в C, C++, Rust, Go) проходит по всему исходнику до запуска, порождает машинный код и кладёт его в исполнимый файл. Плюсы: быстрое выполнение, ошибки синтаксиса находятся до старта. Минусы: нужен отдельный шаг сборки, артефакт привязан к платформе.

Интерпретатор: читать на лету

Интерпретатор (классический Bash, ранние реализации языков) читает программу и тут же выполняет каждую конструкцию. Плюсы: запуск без сборки, переносимость. Минусы: медленнее, ошибки в редких ветках всплывают только при их выполнении.

x = 10
if x > 5:
    print("больше пяти")
else:
    print("не больше")

Вывод:

больше пяти

Байткод: золотая середина

Многие языки (Python, Java, C#) идут средним путём. Сначала компилятор переводит исходник в байткод — компактные инструкции для воображаемой виртуальной машины, не привязанные к конкретному процессору. Потом виртуальная машина исполняет этот байткод. У Python это видно напрямую через модуль dis: вызов dis.dis(add) для функции сложения печатает её байткод. Точный вид зависит от версии Python, но структура такая.

  def add(a, b):
      return a + b

  # байткод (упрощённо):
  LOAD_FAST    a      ; положить a на стек
  LOAD_FAST    b      ; положить b на стек
  BINARY_ADD          ; сложить два верхних значения
  RETURN_VALUE        ; вернуть результат

JIT: компиляция во время работы

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

Виртуальная машина замечает, что какой-то метод вызывается тысячи раз, и на ходу компилирует его в нативный машинный код. Так работают JVM (HotSpot), V8 в браузере, PyPy. Это сочетает гибкость интерпретатора со скоростью компилятора.

Как работает под капотом

КОМПИЛЯТОР:   исходник --(заранее)--> машинный код --> запуск
ИНТЕРПРЕТАТОР: исходник --(на лету)--> сразу выполняется
БАЙТКОД-ВМ:    исходник --> байткод --> ВМ исполняет байткод
JIT:           байткод --> ВМ --> горячий код --(на ходу)--> машинный код

Частые ошибки

Распространённый миф — «Python интерпретируемый, а Java компилируемая». На деле оба сначала компилируются в байткод, а потом исполняются виртуальной машиной. Граница между «компилируемым» и «интерпретируемым» давно размыта.

Ещё ошибка — думать, что JIT всегда быстрее. Для коротких программ накладные расходы на компиляцию не окупаются, и JIT может проиграть простому интерпретатору.

Итог

  • Компилятор переводит весь код заранее — быстро, но нужен шаг сборки.
  • Интерпретатор выполняет код на лету — гибко, но медленнее.
  • Байткод — промежуточные инструкции для виртуальной машины (Python, Java).
  • JIT компилирует горячие участки в машинный код во время выполнения.
Проверьте себя
1. Что такое байткод?
AМашинный код конкретного процессора
BКомпактные инструкции для виртуальной машины, не привязанные к железу
CИсходный текст программы
DФормат хранения файлов на диске
2. В чём суть JIT-компиляции?
AКомпилировать весь код до запуска
BНикогда не компилировать, только интерпретировать
CКомпилировать горячие участки в машинный код во время выполнения
DПереводить машинный код обратно в исходник
3. Почему утверждение «Python чисто интерпретируемый» неточно?
APython вообще не выполняется
BPython сначала компилируется в байткод, который исполняет виртуальная машина
CPython компилируется в машинный код напрямую
DPython работает только в браузере