Компилятор и интерпретатор
| Компилятор | переводит программу целиком до запуска (AOT) |
|---|---|
| Интерпретатор | исполняет программу по ходу чтения |
| Промежуточные формы | AST, байткод, IR (LLVM IR, SSA) |
| Гибриды | байткод + ВМ, JIT-компиляция |
| Примеры AOT | C, C++, Rust, Go, Swift |
| Примеры ВМ и JIT | JVM, .NET CLR, V8, PyPy, LuaJIT |
Компилятор — программа, которая переводит исходный код целиком в другую форму, обычно в машинный код процессора, до запуска. Интерпретатор — программа, которая исполняет исходный код (или его промежуточное представление) по ходу, без создания отдельного исполняемого файла. Это два полюса; большинство современных языков сочетают оба подхода: компилируют исходник в байткод, а байткод интерпретируют и «на горячих участках» компилируют в машинный код прямо во время работы (JIT).[1][2]
Этапы обработки программы
Независимо от подхода, обработка начинается одинаково:
- Лексический анализ — текст разбивается на токены: ключевые слова, имена, числа, операторы. Часто для этого используются регулярные выражения.
- Синтаксический анализ — из токенов строится абстрактное синтаксическое дерево (AST) по грамматике языка (см. Дерево и граф).
- Семантический анализ — разрешение имён (через таблицы символов — обычно хеш-таблицы), проверка типов, ошибки вида «переменная не объявлена».
- Промежуточное представление (IR) — упрощённая форма, удобная для анализа: байткод, трёхадресный код, SSA-форма.
- Оптимизация — свёртка констант, удаление мёртвого кода, встраивание функций, развёртка циклов, векторизация.
- Генерация кода — машинные инструкции для конкретной архитектуры (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 |