Компиляторы
Что такое компилятор
1952. Грейс Хоппер, адмирал ВМС США, пишет первый компилятор A-0. Коллеги смеются: «компьютер не может писать код». Она игнорирует насмешки. Сегодня V8 компилирует JavaScript в машинный код на 3 миллиардах устройств с Chrome. LLVM - сердце Swift, Rust, Clang - под капотом у половины нового кода в мире. GCC содержит 15 миллионов строк. LLVM - 8 миллионов. Эти программы превращают читаемый текст в электрические сигналы, которые двигают биты по кремниевым транзисторам.
- **V8 и Node.js** - один и тот же JIT-движок в Chrome и на сервере; TurboFan компилирует горячие функции в оптимизированный машинный код
- **LLVM** - под капотом Swift (Apple), Rust, Clang, Julia; написав frontend для своего языка, получаешь все платформы бесплатно
- **Python bytecode** - `.py` компилируется в `.pyc` при первом запуске; CPython интерпретирует bytecode, PyPy JIT-компилирует
- **Собеседования** - вопросы о JIT vs AOT, compile time vs runtime, AST - стандарт для Senior-позиций в BigTech
Компиляция vs интерпретация
1952 год. Грейс Хоппер, адмирал ВМС США, создаёт первый компилятор A-0. Коллеги смеются: «компьютер не может писать код». Она игнорирует насмешки. Сегодня V8 компилирует JavaScript в машинный код миллиарды раз в сутки - на 3 миллиардах устройств с Chrome. LLVM лежит в основе Swift, Rust, Clang - под капотом у половины нового кода в мире. Между текстом, который пишет программист, и битами, которые двигает процессор, стоит **переводчик**. И есть два принципиально разных подхода к этому переводу.
**Компиляция** - перевод всей программы целиком *до* запуска. Как перевод книги: сначала переводчик обрабатывает весь текст, потом читатель получает готовый результат. C, C++, Rust, Go - компилируемые языки. Результат: исполняемый файл (`.exe`, ELF binary), который работает без исходного кода.
**Интерпретация** - перевод строка за строкой *во время* выполнения. Как синхронный переводчик: слышит фразу, сразу переводит. Python, Ruby, PHP - интерпретируемые. Результат: нужен интерпретатор на машине, чтобы запустить код.
**JIT (Just-In-Time)** - гибридный подход. Сначала код компилируется в промежуточный bytecode, а затем JIT-компилятор переводит «горячие» участки в машинный код прямо во время выполнения. Java (HotSpot JVM), JavaScript (V8, SpiderMonkey), C# (.NET CLR) используют JIT. V8 TurboFan компилирует «горячие» функции с предположениями о типах - и деоптимизирует, если предположение нарушено.
| Свойство | Компиляция | Интерпретация | JIT |
|---|---|---|---|
| Скорость выполнения | Максимальная | Низкая | Высокая (после прогрева) |
| Время запуска | Мгновенный старт | Мгновенный старт | Медленный (прогрев JIT) |
| Обнаружение ошибок | До запуска | Во время работы | Смешанное |
| Портабельность | Под каждую платформу | Где есть интерпретатор | Где есть VM |
| Отладка | Сложнее (нет исходника) | Проще (есть исходник) | Средне |
| Примеры | C, Rust, Go | Python, Ruby, PHP | Java, JS V8, C# |
Граница между компиляцией и интерпретацией размыта. Python компилирует `.py` в `.pyc` bytecode (компиляция!), который затем интерпретирует CPython VM. V8 сначала интерпретирует JavaScript через Ignition, а потом JIT-компилирует через TurboFan.
**Compile time** - время компиляции, когда компилятор анализирует код. **Runtime** - время выполнения, когда программа работает. Многие оптимизации - компромисс между ними: потратить больше времени на компиляцию, чтобы программа работала быстрее.
Какой подход использует JavaScript-движок V8 в Chrome?
Фазы компилятора
Компилятор не переводит код одним махом. Это **конвейер** из отдельных фаз, где каждая делает ровно одну задачу и передаёт результат следующей. Как завод: сырьё - обработка - сборка - покраска - упаковка. Каждый цех специализирован. Нарушить порядок нельзя: нельзя красить до сборки, нельзя паковать до покраски.
**Лексический анализ (Lexing/Tokenization)** - разбивает поток символов на токены: ключевые слова, идентификаторы, числа, операторы. Как разбить предложение на слова.
**Синтаксический анализ (Parsing)** - проверяет грамматику и строит AST (Abstract Syntax Tree). Как разбор предложения: подлежащее, сказуемое, дополнение.
**Семантический анализ** - проверяет смысл: типы совпадают? Переменные объявлены? Нет ли конфликтов имён? Как проверка, что предложение не только грамматически верно, но и имеет смысл.
**Оптимизация** - улучшает код без изменения поведения: свёртка констант (`2 + 3` → `5`), удаление мёртвого кода, развёртка циклов. Именно здесь Rust добивается zero-cost abstractions.
**Генерация кода (Code Generation)** - переводит оптимизированное представление в машинный код для целевой архитектуры (x86, ARM, RISC-V).
Порядок фаз важен! Нельзя оптимизировать до семантического анализа - можно оптимизировать код с ошибками. Нельзя генерировать код до оптимизации - потеряем производительность.
На какой фазе компилятор обнаружит ошибку `int x = "hello";` в C?
Frontend и Backend компилятора
Фазы компилятора естественно делятся на две группы. **Frontend** работает с исходным языком: lexer, parser, семантический анализ. **Backend** работает с целевой платформой: оптимизация, генерация кода. Между ними - **промежуточное представление (IR)**. Именно это разделение сделало LLVM революцией.
Без разделения: 5 языков × 4 платформы = 20 компиляторов. С разделением через общий IR: 5 фронтендов + 1 оптимизатор + 4 бэкенда = 10 компонентов. Это и есть гениальность архитектуры **LLVM**.
**LLVM** (Low Level Virtual Machine) - это не виртуальная машина, а набор библиотек для построения компиляторов. Создан Крисом Латтнером в 2003 году как проект в Университете Иллинойса. Сегодня LLVM - основа компиляторов Clang (C/C++), rustc (Rust), Swift, Julia и десятков других.
| Компилятор | Frontend | Backend | Язык |
|---|---|---|---|
| Clang | Clang | LLVM | C / C++ / Objective-C |
| rustc | rustc | LLVM | Rust |
| swiftc | Swift compiler | LLVM | Swift |
| GCC | GCC frontend | GCC backend | C / C++ / Fortran |
| javac | javac | JVM bytecode | Java |
| go build | Go compiler | Go SSA backend | Go |
Хотите создать свой язык программирования? Благодаря LLVM нужно написать только frontend (lexer + parser + semantic + LLVM IR генератор). Оптимизацию и генерацию машинного кода для всех платформ LLVM сделает за вас.
Почему LLVM используется как backend для множества языков?
Bootstrapping: курица и яйцо
Компилятор C написан на C. Компилятор Rust написан на Rust. Компилятор Go написан на Go. Возникает парадокс: **как скомпилировать компилятор, если компилятора ещё нет?** Это проблема bootstrapping - раскрутки компилятора. И это не философский вопрос: каждый язык через это проходит.
**Self-hosting** - ключевой момент. Когда компилятор может скомпилировать собственный исходный код, он считается самодостаточным. Rust bootstrap: первые версии компилировались OCaml-компилятором. Go до версии 1.5 был написан на C, затем переписан на Go.
Доклад Кена Томпсона «Reflections on Trusting Trust» (1984)
Кен Томпсон, создатель Unix и языка B, получил премию Тьюринга в 1983 году. В своей Тьюринговской лекции он продемонстрировал атаку: можно внедрить backdoor в компилятор C так, что он будет воспроизводить себя при каждой перекомпиляции - даже если в исходном коде компилятора backdoor-а нет. Компилятор «обучает» следующую версию себя. Мораль: нельзя полностью доверять коду, который не написан самостоятельно - включая компилятор.
Атака Томпсона - не теоретическая. В 2003 году обнаружили попытку внедрить backdoor в ядро Linux через патч, маскирующийся под проверку прав доступа. Проект Reproducible Builds борется с подобными угрозами: одинаковый исходный код должен давать идентичный бинарник.
Современные языки и их bootstrap-цепочки: **Rust** - OCaml → Rust. **Go** - C → Go (с версии 1.5). **GHC (Haskell)** - C → Haskell. **TypeScript** - JavaScript → TypeScript. Каждый новый self-hosting компилятор - milestone для языка.
Интерпретируемые языки всегда медленные, компилируемые - всегда быстрые
Границы размыты: JIT-компиляция в V8 делает JavaScript сопоставимым с C++ на определённых задачах, а наивный C-код без оптимизации может работать медленнее оптимизированного Python с NumPy
V8 TurboFan выполняет speculative optimization: предполагает типы переменных, генерирует оптимизированный машинный код, и деоптимизирует только если предположение нарушено. LuaJIT достигает 70-80% скорости C. PyPy ускоряет Python в 5-10 раз через tracing JIT. Производительность зависит от реализации, а не от категории языка.
Что такое self-hosting компилятор?
Ключевые идеи
- **Компиляция** переводит весь код до запуска (C, Rust), **интерпретация** - строка за строкой во время работы (Python, Ruby), **JIT** - гибрид (Java, V8): быстрый старт + оптимизация горячих путей
- Компилятор - конвейер из 5 фаз: лексический анализ → синтаксический → семантический → оптимизация → генерация кода; порядок нарушать нельзя
- **Frontend** (язык-специфичный) и **Backend** (платформа-специфичный) связаны через промежуточное представление (IR). LLVM - универсальный backend для десятков языков: N + M вместо N × M
- **Bootstrapping** решает парадокс курицы и яйца: первая версия на другом языке, затем self-hosting. Атака Томпсона: бинарнику компилятора нельзя доверять полностью
Связанные темы
Компиляторы - фундамент всей экосистемы программирования. Теперь понятно, что происходит от исходного кода до машинных инструкций:
- Как компьютер читает код — Детальный разбор каждой фазы: от символов до машинного кода
- Архитектура компилятора — Passes, pipeline, multi-pass vs single-pass - внутренняя организация
- Формальные языки — Грамматики, автоматы - математика, на которой построены lexer и parser
Вопросы для размышления
- Почему большинство новых языков (Rust, Swift, Zig) выбирают LLVM как backend, а не пишут свой? В каких случаях стоит писать собственный backend?
- Если JIT может генерировать код, сопоставимый с C++, почему серверные приложения часто пишут на Go или Rust, а не на JavaScript?
- Атака Томпсона показывает, что нельзя доверять бинарнику компилятора. Как проект Reproducible Builds решает эту проблему? Достаточно ли этого?
Связанные уроки
- fl-01-intro — Лексер и парсер основаны на формальных языках и грамматиках
- alg-01-big-o — Алгоритмическая сложность важна для понимания оптимизаций компилятора
- plt-01-paradigms — Парадигмы программирования определяют, что именно компилирует компилятор
- dm-01 — Дискретная математика - фундамент формальной семантики и систем типов