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

Материал из llmdocs — справочник разработчика
Портал: Основы CS
Компилятор и интерпретатор
Компиляторпереводит программу целиком до запуска (AOT)
Интерпретаторисполняет программу по ходу чтения
Промежуточные формыAST, байткод, IR (LLVM IR, SSA)
Гибридыбайткод + ВМ, JIT-компиляция
Примеры AOTC, C++, Rust, Go, Swift
Примеры ВМ и JITJVM, .NET CLR, V8, PyPy, LuaJIT

Компилятор — программа, которая переводит исходный код целиком в другую форму, обычно в машинный код процессора, до запуска. Интерпретатор — программа, которая исполняет исходный код (или его промежуточное представление) по ходу, без создания отдельного исполняемого файла. Это два полюса; большинство современных языков сочетают оба подхода: компилируют исходник в байткод, а байткод интерпретируют и «на горячих участках» компилируют в машинный код прямо во время работы (JIT).[1][2]

Этапы обработки программы

Независимо от подхода, обработка начинается одинаково:

  1. Лексический анализ — текст разбивается на токены: ключевые слова, имена, числа, операторы. Часто для этого используются регулярные выражения.
  2. Синтаксический анализ — из токенов строится абстрактное синтаксическое дерево (AST) по грамматике языка (см. Дерево и граф).
  3. Семантический анализ — разрешение имён (через таблицы символов — обычно хеш-таблицы), проверка типов, ошибки вида «переменная не объявлена».
  4. Промежуточное представление (IR) — упрощённая форма, удобная для анализа: байткод, трёхадресный код, SSA-форма.
  5. Оптимизация — свёртка констант, удаление мёртвого кода, встраивание функций, развёртка циклов, векторизация.
  6. Генерация кода — машинные инструкции для конкретной архитектуры (x86-64, ARM64) или байткод для виртуальной машины; затем компоновка (linking) с библиотеками.

Первые три этапа — «фронтенд», последние — «бэкенд». Инфраструктура LLVM разделила их: фронтенды Clang (C/C++), rustc, Swift, Zig выдают общий LLVM IR, а общий бэкенд оптимизирует его и генерирует код под десятки архитектур.[3]

Компиляция заранее (AOT)

Языки C, C++, Rust, Go, Swift, Haskell компилируются ahead-of-time: на выходе — исполняемый файл с машинным кодом.[7]

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

Интерпретация

Простейший интерпретатор обходит AST и выполняет узлы один за другим (tree-walking). Так работали ранние реализации многих языков; сегодня так делают учебные и встраиваемые интерпретаторы, а также оболочки вроде Bash.

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

Байткод и виртуальные машины

Промежуточный путь — компилировать в байткод, компактные инструкции для виртуальной машины, и исполнять его интерпретатором ВМ. Так устроены CPython, Ruby (YARV), Lua, JVM и .NET. В CPython компиляция в байткод происходит незаметно при импорте, результат кэшируется в файлах .pyc:[4]

import dis
dis.dis(lambda x: x + 1)
#  RESUME        0
#  LOAD_FAST     0 (x)
#  LOAD_CONST    1 (1)
#  BINARY_OP     0 (+)
#  RETURN_VALUE

Поэтому утверждение «Python — интерпретируемый язык» верно лишь отчасти: это свойство реализации, а не языка. Тот же Python исполняют PyPy (с JIT), а WebAssembly — переносимый байткод, который браузер компилирует в машинный код.

JIT-компиляция

JIT (just-in-time) — компиляция во время выполнения. ВМ начинает с интерпретации, собирает профиль (какие функции горячие, какие типы реально приходят) и компилирует частые участки в машинный код с оптимизациями под фактическое поведение. Если предположение нарушилось (в функцию пришла строка вместо числа), выполняется деоптимизация — возврат в интерпретатор.

  • JVM HotSpot и .NET — многоуровневая JIT-компиляция, сделавшая Java быстрой.
  • V8 (Chrome, Node.js) — интерпретатор Ignition и несколько уровней JIT, включая Maglev и TurboFan; благодаря им JavaScript стал быстрым.[6]
  • PyPy и LuaJIT — трассирующие JIT; в CPython 3.13 появился экспериментальный JIT (PEP 744).[5]

Цена JIT — время «прогрева», расход памяти и сложность реализации; поэтому для быстрого старта (serverless, CLI) иногда используют AOT-варианты: GraalVM Native Image, .NET Native AOT.

Родственные понятия

  • Транспилятор — компилятор из одного языка высокого уровня в другой: TypeScript → JavaScript, Babel, Cython (Python → C).
  • Кросс-компиляция — сборка под другую платформу (GOOS=linux GOARCH=arm64 go build).
  • Ассемблер и компоновщик (linker) — завершающие этапы получения бинарника.

Сравнение

Подход Старт Пиковая скорость Переносимость Примеры
AOT быстрый высокая сборка под каждую платформу C, Rust, Go
Интерпретатор AST быстрый низкая высокая Bash, ранний Ruby
Байткод + ВМ средний средняя высокая CPython, Lua
Байткод + JIT медленный прогрев высокая высокая Java, C#, JavaScript

См. также

Источники

  1. Aho, Lam, Sethi, Ullman — Compilers: Principles, Techniques, and Tools (2nd ed.)
  2. Robert Nystrom — Crafting Interpreters
  3. LLVM Project — documentation
  4. Python — dis: Disassembler for Python bytecode
  5. PEP 744 — JIT Compilation
  6. V8 — Maglev, V8's fastest optimizing JIT
  7. Go — Command compile