Интерпретатор и компилятор в итоге генерируют одинаковый машинный код?
В моем понимании компилятор берет исходный текст программы, например,
A = 2;
B = 3;
print(2 + 3);
Компилирует его, на выходе мы получаем машинный код. Допустим, условно, получилось 3 команды:
1110
1101
1010
Далее нам уже не нужен компилятор, мы можем передавать этот скомпилированный код куда угодно (где такой же процессор) и он будет успешно выполнятся.
Интерпретатор же будет брать каждую строчку по очереди, преобразовывать в промежуточный байт-код своей виртуальной машины и потом уже виртуальная машина будет по очереди переводить свои команды в машинный код т. е. команды процессора
Вопрос: получится ли в итоге, что интерпретатор выполнит те же самые команды?
1110 1101 1010И если так, то почему никогда нельзя получить скомпилированный код интерпретируемых языков?
Так же, когда говорят про компиляторы, употребляют такую фразу, что компилятор выполняет программу целиком, что это значит? Ведь процессор все равно будет выполнять машинный код по очереди, по одной команде? Т. е. в итоге, что компилируемый код, что интерпретируемый будет выполнятся по одной команде, отправленной процессору?
Ответы (2 шт):
Первое утверждение не полностью верно - несмотря на то, что выполненная интерпретатором программа может фактически делать то же самое, что и ее откомпилированный аналог, грубо говоря, между иструкций откомпилированной программы появятся инструкции, обслуживающие внутренюю кухню интерпретатора.
Обычно интерпретатор проходит программу по одному выражению, это одно выражение он парсит и немедленно исполняет. Генерации кода при этом вовсе не происходит. Еще есть переходный вариант - это JIT-компиляция (js, java), и это уже промежуточный случай между чистым интерпретатором (BASIC) и чистым компилятором (C++)
Второй подвопрос - иногда можно, если сузить число возможных действий. Так, KPHP позволяет транспилировать программу на PHP в код на C++.
Самый мощный пример, почему программу на транслируемом языке не всегда можно откомпилировать - во многих языках есть eval() - функция, которая исполняет строку на этом языке. Таким образом, придется в откомпилированную программу внедрить полностью весь интерпретатор.
Далее, из откомпилированной программы удаляется очень много всего - имена переменных, функций, информация о типах. Иногда то что написано в виде класса с методами сдувается до пары машинных инструкций - так что компиляция программы с интерпретируемого языка не приносит пользы - оптимизация (ради которой приходися терпеть неудобства C++) становится невозможной.
Никогда не слышал о том, что компилятор что-то выполняет "целиком". Компилятор проходит программу (иногда не один раз), после чего создает двоичный код сразу для всей единицы трансляции.
Рассмотрим случай, когда вы даете компилятору на вход программу на языке программирования высокого уровня. Он вам на выходе даст программу на языке ассемблера. В виде исполняемого файла, объектного файла или может просто вывести на экран ассемблерный код. Это не важно, ведь задача компилятора состоит только в том, чтобы перевести программу с языка высокого уровня на язык ассемблера. Это как переводчик с русского на английский.
В случае, когда вы интерпретатору даете на вход программу на языке программирования высокого уровня интерпретатор вам никакой другой программы на выходе не дает, он исполняет ту программу, которую вы ему передали. Как он ее исполняет — не важно. Он может делать с ней все, что хочет: может переводить в байт-код и исполнять на виртуальной машине; может ничего никуда не переводить и сразу ходить по AST* и его выполнять; может переводить в нативный код (ассемблер) и исполнять уже его. Вот последний случай, когда интерпретатор переводит код в нативные инструкции процессора, называется JIT-компиляцией‡.
Можно заметить, что компилятор программу не выполняет, он просто ее переводит с одного языка на другой. А вот интерпретатор программу выполняет, а переводит он ее во что-то или нет — не важно, это его личное дело.
Касательно ваших вопросов:
Не обязательно, главное чтобы наблюдаемое поведение программы оставалось таким же. Вы можете написать разные инструкции, которые в итоге будут делать то же самое.
Почему нельзя, можно. Можно, например, получить результат JIT-компиляции кода на JavaScript.
«*… употребляют такую фразу что компилятор выполняет программу целиком…», ни разу такого не слышал, да вы и сами теперь знаете, что компилятор ничего не выполняет. «Т.е. в итоге что компилируемый код что интерпретируемый будет выполнятся по 1 команде отправленной процессору?*» Да, в итоге все программы, которые выполняются на вашем компьютере, выполняются вашим процессором инструкция-за-инструкцией†. Только в случае интерпретируемых программ это происходит не на прямую, а через посредника — интерпретатор.
Для примера напишем интерпретатор простого языка, в котором будут три оператора: увеличить счетчик +, уменьшить счетчик - и вывести счетчик @. Вся программа представляет из себя строку из этих трех символов.
Вот так может выглядеть интерпретатор этого языка:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static unsigned int counter = 0;
void increment_counter(void) { counter++; }
void decrement_counter(void) { counter--; }
void print_counter(void) { printf("%u\n", counter); }
int
main(int argc, char *argv[])
{
/* Наша программа предается как первый аргумент командной строки. */
char *source = argv[1];
/* Проходим по всем символами нашей программы (операторам). */
for (size_t i = 0; i < strlen(source); i++) {
const char operator = source[i];
/* Смотрим, что у нас за текущий оператор и выполняем его. */
if (operator == '+')
increment_counter();
else if (operator == '-')
decrement_counter();
else if (operator == '@')
print_counter();
}
}
Обратите внимание, что никакого перевода из нашего языка в язык ассемблера не происходит. Когда наш интерпретатор встречает оператор + он просто вызывает некоторую функцию, которая делает некоторые вещи, в данном случае — увеличивает счетчик.
То есть интерпретатор понял, что мы хотели получить, ничего переводить в ассемблер не стал и просто сам выполнил все, что было нужно.
* Это называется tree-walk интерпретация.
† Ну, если не брать в расчет всякие микроархитектурные детали типа параллелизма на уровне инструкций.
‡ Например, существует JVM HotSpot. Она при исполнении программы находит такие места программного кода, которые выполняются часто и занимают много процессорного времени и компилирует их в нативные инструкции процессора, чтобы такие места (они называются хот-спотами) выполнялись быстрее. Это пример техники JIT-компиляции, компиляции «на лету».