Компилируемые и интерпретируемые языки: как выполняется код
Основы3 минРедакция Innovialabs
Коротко
Компилятор заранее переводит код в машинные инструкции, интерпретатор выполняет его по ходу, а большинство современных языков совмещают оба подхода через байт-код и JIT — поэтому важнее понимать конкретную реализацию, чем ярлык.
Коротко: в чём разница
Процессор понимает только машинный код. Всё остальное нужно перевести, и вопрос лишь в том, когда это происходит.
- Компиляция — перевод всей программы в машинный код до запуска. Получается готовый исполняемый файл.
- Интерпретация — программа-интерпретатор читает код и выполняет его во время запуска, шаг за шагом.
Важная оговорка: «компилируемым» или «интерпретируемым» бывает не язык, а его реализация. У Python есть и интерпретатор, и компиляторы; у JavaScript — интерпретаторы и JIT. Но у каждого языка есть основной, типичный способ выполнения — о нём и речь.
Четыре модели на примерах
C: классическая компиляция
gcc main.c -o app # компиляция в машинный код
./app # запуск без компилятора
Компилятор видит всю программу целиком, оптимизирует её и выдаёт бинарник под конкретную платформу (ОС + архитектура процессора). Для Linux на ARM и Windows на x86 нужны разные сборки.
Python: байт-код и интерпретатор
python main.py
Стандартная реализация CPython сначала компилирует исходник в байт-код (те самые файлы .pyc), а затем виртуальная машина интерпретирует его инструкция за инструкцией. Для запуска на сервере нужен установленный Python нужной версии и зависимости.
Java: байт-код + JIT
javac Main.java # исходник -> байт-код .class
java Main # JVM выполняет байт-код
Байт-код Java не привязан к процессору: один и тот же .class или .jar запускается на любой системе с JVM. Внутри JVM код сначала интерпретируется, а «горячие» участки, которые выполняются часто, JIT-компилятор переводит в машинный код прямо во время работы.
JavaScript: движок с JIT
Браузер или Node.js получает исходный текст. Движок (например, V8) разбирает его, начинает выполнять через интерпретатор и параллельно JIT-компилирует часто используемые функции, опираясь на наблюдения о типах данных.
Сравнение подходов
| Ahead-of-time (C, Go, Rust) | Байт-код + интерпретатор (CPython) | Байт-код/исходник + JIT (Java, C#, JS) | |
|---|---|---|---|
| Когда переводится | до запуска | при запуске, по шагам | при запуске, горячий код — в машинный |
| Старт программы | быстрый | быстрый | может быть медленнее из-за «прогрева» |
| Пиковая скорость | высокая | обычно ниже | высокая после прогрева |
| Переносимость | сборка под каждую платформу | нужен интерпретатор | нужна среда выполнения (JVM, .NET, движок) |
| Что деплоить | один бинарник | исходники + интерпретатор + зависимости | артефакт + рантайм |
Что это значит на практике
Скорость. Для вычислительно тяжёлых задач компилируемые и JIT-языки обычно быстрее. Но в типичном веб-сервисе узким местом чаще становятся база данных, сеть и алгоритмы, а не язык. Сначала измеряйте, потом выбирайте.
Скорость разработки. Интерпретируемые языки дают быстрый цикл «изменил — запустил» и удобный REPL. Компиляция ловит часть ошибок раньше, но добавляет шаг сборки.
Переносимость. Бинарник C или Go нужно собирать под каждую целевую платформу. Байт-код Java или .NET переносим, но требует рантайма.
Деплой. Один статический бинарник легко положить в минимальный Docker-образ. Для Python и Node.js в образ нужно включить интерпретатор и зависимости, и важно зафиксировать их версии.
Частые заблуждения
- «Интерпретируемый — значит медленный». Современные JIT-движки JavaScript и JVM очень быстрые; а Python часто вызывает быстрые библиотеки, написанные на C.
- «Компилируемый — значит без ошибок». Компилятор ловит синтаксис и (в типизированных языках) ошибки типов, но не логику.
- «Язык навсегда либо тот, либо другой». Граница размыта: есть AOT-компиляция для Java и C#, JIT для Python (например, PyPy), WebAssembly для браузера.