Компиляторы

Что такое компилятор

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, GoPython, Ruby, PHPJava, 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 и десятков других.

КомпиляторFrontendBackendЯзык
ClangClangLLVMC / C++ / Objective-C
rustcrustcLLVMRust
swiftcSwift compilerLLVMSwift
GCCGCC frontendGCC backendC / C++ / Fortran
javacjavacJVM bytecodeJava
go buildGo compilerGo SSA backendGo

Хотите создать свой язык программирования? Благодаря 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 — Дискретная математика - фундамент формальной семантики и систем типов
Что такое компилятор

0

1

Войти