Компиляторы: назначение, этапы компиляции и виды

Компиляторы: назначение, этапы компиляции и виды Полезное

Компилятор — это программа, которая переводит исходный код, написанный на языке программирования, в машинный код или другое низкоуровневое представление, готовое к выполнению. Компиляция — это сам процесс такого перевода: компилятор читает текст программы целиком, проверяет его на ошибки и порождает результат (чаще всего исполняемый файл), который процессор способен выполнить. Ниже разберём, из каких этапов состоит компиляция, чем компилятор отличается от интерпретатора и JIT и как это выглядит на практике на примерах gcc, clang, javac и rustc.

Процессор понимает только машинный код — последовательности нулей и единиц для конкретной архитектуры. Писать программы напрямую в нём почти невозможно, поэтому разработчики используют языки высокого уровня, а компилятор служит переводчиком между текстом программы и «железом».

Этапы компиляции

Классический компилятор обрабатывает программу в несколько шагов. В реальных инструментах эти шаги могут частично перекрываться или объединяться, но логически принято выделять следующие фазы.

  • Лексический анализ (лексер). Компилятор разбивает текст на токены — неделимые единицы: имена переменных, ключевые слова, числа, операторы, скобки. Например, строка x = 10 + 2 превращается в токены x, =, 10, +, 2.
  • Синтаксический анализ (парсер). Из потока токенов строится дерево разбора (AST, абстрактное синтаксическое дерево), которое отражает структуру программы: где выражение, где присваивание, где вызов функции. Здесь ловятся ошибки вида «пропущена скобка» или «нет точки с запятой».
  • Семантический анализ. Проверяется смысл: совпадают ли типы, объявлена ли переменная до использования, существует ли вызываемая функция. На этом этапе всплывают ошибки вроде «нельзя сложить строку и число» или «переменная не объявлена».
  • Оптимизация. Компилятор преобразует промежуточное представление кода, чтобы итоговая программа работала быстрее или занимала меньше места: убирает недостижимый код, сворачивает константы, разворачивает циклы. Глубину оптимизации обычно регулируют флагами (например, у gcc и clang это -O0, -O1, -O2, -O3).
  • Генерация кода. Из оптимизированного промежуточного представления порождается машинный код (или ассемблерный код, который затем собирается в объектный файл). Отдельный этап — компоновка (линковка): объектные файлы и подключённые библиотеки собираются в один исполняемый файл.

Важно: строгая последовательность «этап за этапом» — это упрощение. Современные компиляторы часто используют общее промежуточное представление (например, инфраструктура LLVM, на которой построен clang) и могут выполнять анализ и оптимизацию в несколько проходов. Но набор задач — разбор, проверка, оптимизация, генерация — остаётся тем же.

Компилятор, интерпретатор и JIT — в чём разница

Компиляция — не единственный способ выполнить программу. Есть ещё интерпретация и промежуточный подход JIT. Разница в том, когда и во что переводится исходный код.

Признак Компилятор (AOT) Интерпретатор JIT-компилятор
Когда переводит код заранее, до запуска (AOT, ahead-of-time) во время выполнения, по мере чтения во время выполнения, «горячие» участки
Во что переводит в машинный код целиком ничего не сохраняет, выполняет напрямую в машинный код на лету, с кэшированием
Скорость запуска медленнее (нужна сборка) быстрый старт быстрый старт, разгон по ходу работы
Скорость работы высокая ниже близка к скомпилированной после «прогрева»
Поиск ошибок много ошибок до запуска часть ошибок только при выполнении строки как у интерпретируемых, плюс профилирование
Примеры gcc, clang, rustc классический CPython, Ruby JVM (HotSpot), движок V8 в JavaScript, .NET CLR

Отдельно про Java и байт-код: javac компилирует исходный код не в машинный код, а в байт-код (файлы .class). Затем виртуальная машина JVM исполняет этот байт-код, а её JIT-компилятор на ходу переводит часто выполняемые участки в машинный код. Похожая схема — у C# и платформы .NET. Это гибрид: сначала компиляция в промежуточный формат, потом исполнение с JIT.

Термин AOT (ahead-of-time) означает «компиляция заранее», в отличие от JIT (just-in-time, «точно в срок»). AOT-подход применяют и к языкам с виртуальной машиной: например, GraalVM собирает нативный исполняемый файл из Java-программы заранее, без прогрева JIT.

Пример: от исходника к исполняемому файлу

Возьмём простую программу на C и посмотрим, что делает компилятор. Файл hello.c:

#include <stdio.h>

int main(void) {
    printf("Hello, Otus\n");
    return 0;
}

Компилируем компилятором gcc и запускаем результат:

$ gcc hello.c -o hello
$ ./hello
Hello, Otus

Команда gcc hello.c -o hello прошла все этапы (разбор, проверка, оптимизация, генерация, линковка) и создала исполняемый файл hello. Именно его запускает ./hello, а не исходный текст. Тот же результат даст clang: clang hello.c -o hello.

Теперь покажем, как компилятор ловит ошибку. Уберём точку с запятой после printf:

    printf("Hello, Otus\n")
    return 0;

Компилятор откажется собирать программу и выдаст примерно такое сообщение:

hello.c: In function 'main':
hello.c:5:5: error: expected ';' before 'return'

Исправление — вернуть точку с запятой:

    printf("Hello, Otus\n");
    return 0;

Это ключевое преимущество компиляции: синтаксические и многие смысловые ошибки видны до запуска, а не тогда, когда программа уже работает у пользователя.

Виды компиляторов

  • AOT-компиляторы переводят программу в машинный код заранее. Так работают gcc и clang (для C, C++ и других языков), а также rustc — компилятор языка Rust, построенный на инфраструктуре LLVM.
  • JIT-компиляторы встроены в среды выполнения и компилируют код по ходу работы: JVM для Java, V8 для JavaScript, CLR для C#.
  • Кросс-компиляторы собирают программу под другую платформу, а не ту, на которой запущен компилятор. Например, на компьютере с процессором x86 можно собрать программу для микроконтроллера ARM. GCC и Clang умеют работать как кросс-компиляторы.
  • Транспиляторы переводят код с одного высокоуровневого языка на другой (например, TypeScript в JavaScript). Это тоже компиляция, только целевой язык — не машинный код.
  • Компиляторы компиляторов (генераторы парсеров, например Bison или ANTLR) порождают части компилятора по формальному описанию грамматики языка.

Выводы

  • Компилятор переводит исходный код в машинный код или другое исполнимое представление заранее, целиком, до запуска программы; компиляция — это сам процесс перевода.
  • Компиляция логически делится на лексический анализ, синтаксический анализ, семантический анализ, оптимизацию и генерацию кода, но в реальных инструментах эти фазы часто перекрываются и используют общее промежуточное представление.
  • Главное отличие от интерпретатора — момент перевода: компилятор переводит всё заранее и ловит много ошибок до запуска, интерпретатор выполняет код построчно во время работы.
  • JIT — гибрид: код компилируется в машинный на лету, во время выполнения; так работают JVM, движок V8 и платформа .NET. AOT и JIT различают компиляцию «заранее» и «в момент выполнения».
  • gcc и clang компилируют C и C++ в машинный код, rustc — язык Rust, а javac переводит Java в байт-код для JVM, а не сразу в машинный код.

Где применяется / связь с практикой

Понимание того, как работает компилятор, полезно на практике каждый день: по его сообщениям об ошибках вы находите баги ещё до запуска, флагами оптимизации управляете скоростью и размером программы, а выбором между компилируемыми и интерпретируемыми языками — балансом между скоростью разработки и скоростью работы. Особенно это заметно в C и C++, где компилятор и его настройки напрямую влияют на производительность.

Разобраться в компиляции глубже и научиться писать быстрый код на C++ помогает курс C++ Developer. Professional: там подробно разбирают устройство сборки, оптимизации и работу с памятью. Если хочется сначала присмотреться к теме без обязательств, посмотрите записи и расписание бесплатных вебинаров Otus — там регулярно разбирают языки программирования и инструменты разработчика.

Смежные темы: Двоичный код: что это такое, как работает и где применяется, Java — что нужно знать новичку о языке программирования, Где писать программный код: программы для программирования в 2026 году.

FAQ

Компиляция и сборка (build) — это одно и то же?
Не совсем. Компиляция — это перевод исходного кода в объектный или машинный код. Сборка обычно шире: она включает компиляцию всех файлов, линковку с библиотеками, а иногда запуск тестов и упаковку. В обиходе слова часто используют как синонимы, но строго сборка — это процесс, частью которого является компиляция.

Можно ли декомпилировать исполняемый файл обратно в исходный код?
Частично. Декомпиляторы восстанавливают структуру программы и приблизительный код, но не исходный текст с комментариями и понятными именами переменных — при компиляции они теряются. Для байт-кода (например, Java) восстановление точнее, чем для нативного машинного кода.

Почему одна и та же программа может по-разному работать после компиляции разными компиляторами?
Компиляторы отличаются набором оптимизаций, версией и трактовкой неопределённого поведения языка. Один и тот же код gcc и clang могут собрать в чуть разный машинный код, а разные флаги оптимизации меняют скорость и иногда поведение программы, если в ней есть ошибки, зависящие от неопределённого поведения.

OTUS Журнал