Компиляторы
Интерпретатор vs Компилятор
2008 год. Google выпускает Chrome с JavaScript движком V8. До этого JavaScript работал в 100 раз медленнее Java. V8 вводит JIT-компиляцию: первые выполнения интерпретируются, горячий код компилируется в машинный в рантайме. Node.js (2009) построен на V8 - и внезапно серверный JavaScript становится конкурентом Java по производительности. Один архитектурный выбор (JIT) изменил весь веб-стек за несколько лет.
- **Python**: CPython - классический bytecode интерпретатор. PyPy добавляет JIT - ускорение в 5-50 раз для числодробилок
- **JVM**: HotSpot JIT компилирует горячие методы в нативный код - Java конкурирует с C++ по скорости
- **V8**: Ignition (bytecode интерпретатор) + TurboFan (оптимизирующий JIT компилятор)
- **LLVM**: AOT компилятор backend для C, Rust, Swift, Julia - один IR, много архитектур
Интерпретаторы и компиляторы: 70 лет эволюции
В 1957 году Fortran compiler Джона Бэкуса произвёл революцию: генерировал код настолько хорошо, что скептики не верили - 'вручную так не написать'. Это был AOT компилятор. В 1958 году LISP Джона Маккарти ввёл интерпретацию: код выполнялся напрямую через eval. В 1995 году Java выбрала средний путь: компиляция в bytecode + интерпретация JVM. HotSpot (1999) добавил JIT. В 2008 году V8 показал, что JIT может сделать динамический язык быстрым. Современные языки (Julia, PyPy) используют все три подхода одновременно.
Tree-Walking Interpreter
Простейший интерпретатор: после парсинга AST обходит дерево и выполняет каждый узел напрямую. Нет промежуточной компиляции.
**Преимущества tree-walking:** Простота реализации, легкость добавления новых конструкций, удобство для DSL и скриптовых языков. **Недостатки:** Медленно - каждый switch/dispatch на каждый узел. Плохой cache locality из-за pointer-chasing по куче. Ruby 1.8 и ранние Python использовали этот подход.
Tree-walking интерпретатор выполняет цикл `for i in range(1000000): x += i`. Что является главной причиной медленной работы?
Bytecode VM
Bytecode VM компилирует AST в компактный промежуточный код (bytecode), затем исполняет его виртуальной машиной. Быстрее tree-walking, но переносимо как интерпретатор.
CPython компилирует Python в bytecode, который исполняет CPython VM. Означает ли это, что Python - компилируемый язык?
JIT: Just-In-Time компиляция
JIT компилятор анализирует выполнение программы в рантайме и компилирует горячие участки кода в нативный машинный код. Объединяет гибкость интерпретатора со скоростью компилятора.
**Hidden Classes (V8):** V8 создаёт внутренние классы для объектов с одинаковой структурой. `{x: 1, y: 2}` все экземпляры получают один Hidden Class -> JIT может инлайнить property access. Добавление свойств в разном порядке создаёт разные Hidden Classes -> дополниться с polymorphic lookup.
V8 JIT оптимизировал функцию `sum(a, b)` для integer входных данных. Вызов `sum(3.14, 2.71)` вызывает deoptimization. Что происходит дальше?
AOT: Ahead-Of-Time компиляция
AOT компилятор генерирует нативный машинный код до запуска программы. Максимальная производительность, предсказуемое время запуска. C, C++, Rust, Go, Swift используют AOT.
| Подход | Startup | Peak Perf | Portability | Примеры |
|---|---|---|---|---|
| Tree-walking | Мгновенный | Низкая | Высокая | Ruby 1.8, Bash |
| Bytecode VM | Быстрый | Средняя | Высокая | Python, Java (без JIT) |
| JIT | Прогрев ~сек | Очень высокая | Средняя | V8, JVM HotSpot, PyPy |
| AOT | Мгновенный | Максимальная | Низкая (re-compile) | C, Rust, Go, Swift |
**LLVM как универсальный backend:** LLVM IR - промежуточное представление для AOT компиляторов. C (Clang), Rust, Swift, Julia, Kotlin Native - все используют LLVM backend. Один IR -> оптимизации -> x86, ARM, RISC-V, WebAssembly. Это объясняет, почему Rust получает C-уровень производительности без написания собственного кодогенератора.
Компилируемые языки всегда быстрее интерпретируемых
Современные JIT компиляторы (V8, JVM HotSpot) часто достигают производительности AOT компиляторов для долгоживущих программ.
JIT имеет информацию о рантайм профиле: какие ветки чаще выполняются, какие типы реально используются. AOT оптимизирует без этой информации. JVM с JIT часто быстрее эквивалентного C кода для числодробилок.
AWS Lambda функция на Python запускается под нагрузкой тысячи раз в секунду. Почему cold start (первый запуск) медленнее warm start?
Ключевые идеи
- **Tree-walking interpreter**: прямое выполнение AST - просто, медленно. CPython 2.x, Ruby 1.8
- **Bytecode VM**: компиляция в промежуточный код + интерпретация. Python 3, Java, Lua
- **JIT (Just-In-Time)**: bytecode -> нативный код в рантайме для горячих участков. V8, JVM HotSpot
- **AOT (Ahead-of-Time)**: компиляция до запуска -> максимальная скорость. C, Rust, Go
- **Trade-off**: startup time vs peak performance vs portability vs dynamism
Вопросы для размышления
- JIT требует времени для 'прогрева' - горячий код компилируется после нескольких интерпретированных выполнений. Для каких приложений это проблема (serverless функции vs long-running server)?
- PyPy - Python интерпретатор написанный на Python с JIT. Как это возможно? Что такое meta-tracing JIT?
- Wasm (WebAssembly) - это IR формат или bytecode? Как браузеры выполняют Wasm: интерпретацией, AOT или JIT?