Компиляция и интерпретация
Предпосылки: Ассемблер (машинный код, мнемоники), Переменные и типы (статическая и динамическая типизация), Функции (вызов функций).
Строка salary = 1500 написана для человека. Процессор работает с инструкциями низкого уровня в ассемблере — в конечном счёте с последовательностями байтов (байт — минимальная адресуемая ячейка, обычно 8 бит). Между исходным текстом и выполнением всегда стоит перевод, и языки различаются не тем, «какой лучше», а тем, каким путём один и тот же исходник добирается до процессора.
Путь удобно описывать тремя осями:
- когда происходит перевод — до запуска или во время;
- что получается после перевода — машинный код, промежуточное представление или ничего сохраняемого;
- кто исполняет результат — сам процессор или программа-посредник.
Компиляция, виртуальная машина и интерпретация — три разных набора ответов на эти три вопроса. Статическая и динамическая типизация — отдельная ось и к форме запуска отношения не имеет.
Компиляция заранее: получи исполнимый файл до запуска
Самый прямой набор ответов по трём осям: когда — до запуска, что — готовый машинный код, кто — сам процессор. Такой подход называется ahead-of-time (AOT — «заблаговременно», до запуска) компиляцией.
flowchart LR S["исходный код"] --> C["компилятор"] --> M["машинный код"] --> R["запуск"]
Примеры:
$ gcc file.c -o program
$ ./program$ rustc file.rs
$ ./fileПосле компиляции получается файл с машинными инструкциями для конкретной платформы. Операционная система загружает его, и процессор выполняет эти инструкции напрямую.
Этот путь снимает одну важную проблему: значительная часть подготовки уже сделана заранее. Поэтому запуск обычно быстрый, а часть ошибок можно поймать ещё до первого выполнения.
Но у такого подхода есть цена:
- после каждого изменения код нужно перекомпилировать;
- получившийся файл привязан к платформе;
- если нужна другая операционная система или другой процессор, обычно требуется новая сборка.
Виртуальная машина: переведи не в машинный код процессора, а в общий промежуточный код
Привязка к платформе — серьёзное ограничение. Чтобы одна и та же программа работала на разных системах без новой сборки под каждую, в цепочку перевода вставляют дополнительный слой.
Ответы по трём осям сдвигаются: когда — перевод по-прежнему до запуска, что — не машинный код процессора, а байткод (bytecode — инструкции, закодированные короткими последовательностями байтов) для виртуальной машины, кто — виртуальную машину исполняет не процессор напрямую, а программа-посредник. Виртуальная машина (virtual machine) — это программа, которая ведёт себя как отдельный компьютер: у неё свой набор инструкций и своя модель памяти, но работает она поверх настоящего процессора.
flowchart LR S["исходный код"] --> B["байткод"] --> VM["виртуальная машина"] --> R["выполнение"]
В Java это видно прямо в командах:
$ javac Main.java
$ java Mainjavac создаёт .class-файлы с байткодом JVM. Команда java запускает этот байткод на виртуальной машине Java.
В Ruby этот промежуточный шаг обычно скрыт от пользователя. Снаружи вы пишете ruby file.rb, но внутри рантайм всё равно сначала разбирает исходный код и превращает его во внутреннюю форму, с которой потом работает виртуальная машина.
Этот путь решает другую задачу, чем AOT-компиляция. Здесь важен не только запуск, но и переносимость: одна и та же промежуточная форма может работать на разных платформах, если на каждой есть своя реализация VM.
Цена тоже своя:
- между программой и процессором появляется дополнительный слой;
- модель запуска становится сложнее;
- часть работы откладывается на рантайм виртуальной машины.
Прямая интерпретация: разбирай и выполняй на ходу
Третий путь жертвует подготовкой ради короткого цикла обратной связи. По трём осям: когда — перевод совмещён с выполнением, что — отдельного сохраняемого артефакта нет, кто — программу исполняет интерпретатор, читающий исходный текст.
flowchart LR S["исходный код"] --> I["интерпретатор"] --> R["выполнение"]
Так работали ранние версии BASIC: строка вводилась, сразу разбиралась и сразу выполнялась. Для обучения и экспериментов это было удобно: цикл «написал → увидел результат» становился максимально коротким.
Важно не спутать две вещи:
- “нет отдельной команды компиляции”;
- “нет вообще никакой промежуточной внутренней формы”.
Во многих современных системах граница размыта: снаружи язык выглядит интерпретируемым, но внутри рантайм всё равно строит внутреннее представление программы. Для базовой модели достаточно помнить главное: у программиста нет отдельного пользовательского шага сборки, код запускается сразу.
Это не то же самое, что статическая и динамическая типизация
В заметке о типах уже был другой вопрос: когда язык проверяет допустимость операций и насколько явно программист описывает типы.
Это отдельная ось.
Например:
- C: AOT-компиляция + статическая типизация;
- Rust: AOT-компиляция + статическая типизация + дополнительные проверки владения;
- Java: виртуальная машина + статическая типизация;
- Ruby: запуск через рантайм виртуальной машины + динамическая типизация (подробности: токенизация и парсинг → компиляция в байткод → исполнение на YARV);
- ранний BASIC: интерпретация + слабые проверки до реального выполнения.
Поэтому нельзя просто сказать:
- “компилируемый язык значит статический”;
- “интерпретируемый язык значит динамический”;
- “виртуальная машина значит все ошибки только во время работы”.
Это разные свойства языка и среды выполнения.
Время компиляции и время выполнения
Из-за этого различия полезно отдельно держать ещё одну пару понятий.
Время компиляции — момент, когда код переводится и проверяется до запуска.
Время выполнения — момент, когда программа уже работает.
Часть ограничений относится именно ко времени компиляции:
- статическая проверка типов;
- проверка владения в Rust;
- отказ собрать программу при явном нарушении контракта.
А часть проблем относится ко времени выполнения:
- исключения;
- отсутствие файла;
- неверный ввод пользователя;
- ошибки, которые проявляются только на реально дошедшем до них пути выполнения.
Это важно, потому что одна и та же программа почти всегда живёт в обеих фазах: что-то можно проверить заранее, а что-то становится видно только на реальных данных и реальном ходе выполнения.
Почему границы размываются
На практике современные рантаймы часто смешивают несколько подходов.
Виртуальная машина может использовать JIT (just-in-time — «в момент выполнения»): часто выполняемый байткод дополнительно переводится в машинный код уже во время работы программы через компиляцию.
Интерпретатор может сначала построить внутреннее представление программы, а потом выполнять уже его, а не исходный текст посимвольно.
Поэтому названия вроде «компилируемый» и «интерпретируемый» лучше понимать как описание доминирующего пути запуска, а не как обещание, что внутри существует только один механизм.
Что это даёт программисту
| Подход | Что получается после перевода | Что это даёт | Цена |
|---|---|---|---|
| AOT-компиляция | машинный код | быстрый запуск, много работы сделано заранее | нужна пересборка, результат привязан к платформе |
| Виртуальная машина | байткод или другая промежуточная форма | переносимость и дополнительный слой оптимизаций | выполнение идёт через VM |
| Прямая интерпретация | нет отдельного пользовательского артефакта | короткий цикл “изменил → запустил” | меньше подготовки вынесено до запуска |
Это снова не три разных класса языков «по природе». Это три разных ответа на один и тот же вопрос: где именно проходит граница между написанным текстом и реальным выполнением на машине. Исторически к каждому ответу пришли отдельно — A-0 Грейс Хоппер (1952) и FORTRAN сделали массовой AOT-компиляцию, BASIC — немедленный запуск, Java — виртуальную машину как переносимый слой, — но эти ветки не отменяют друг друга, а отвечают на разные практические требования: скорость выполнения, скорость обратной связи, переносимость и объём проверок до запуска.
Sources
- Hopper, G., 1952, The Education of a Computer. Proceedings of the ACM.
- Aho, A. et al., 2006, Compilers: Principles, Techniques, and Tools. Pearson.
- Thomas, D. et al., 2023, Programming Ruby 3.3. Pragmatic Bookshelf.