Большая энциклопедия нефти и газа. Компилируемые и интерпретируемые языки программирования. Разбор понятий

От Ali Çehreli . Большая часть материала ориентирована на начинающих, но так как большая часть аудитории уже имеет базовые знания в программировании, то этот материал убран под хабракаты. В данной части рассматриваются фундаментальные типы, свойства типов, основы компиляции и императивного программирования.

Базовый материал

Компилятор

Ранее мы увидели, что наиболее часто используемые инструменты в D - текстовый редактор и компилятор . Программы на D пишутся в текстовых редакторах (прим. ваш КО).

При использовании компилируемых языков , таких как D, необходимо понимать концепцию компиляции и функцию компилятора.

Машинные коды

Мозгом компьютера является микропроцессор (или ЦПУ, сокращение от центрального процессорного устройства ). Кодированием называется указывание, что именно должен делать ЦПУ, и инструкции, которые используются при этом, называются машинными кодами .

Большинство архитектур ЦПУ используют специфичные для них машинные коды. Эти машинные инструкции определяются с учетом ограничений аппаратных средств во время проектирования архитектуры. На самом низком уровне эти инструкции реализованы как электические сигналы. Так как простота программирования на этом уровне не является главной целью, написание программ напрямую в машиннах кодах ЦПУ является очень сложной задачей.

Эти машинные инструкции представляют собой специальные числа, которые представляют различные операции, поддерживаемые конкретным ЦПУ. Например, для воображаемого 8-битного ЦПУ, число 4 может представлять операцию загрузки, число 5 - операцию сохранения, и число 6 - операцию увеличения значения на единицу. Предполагая, что первые 3 бита слева - номер операции и 5 последующих бита - значение, которое используется в этой операции, программа-пример в машинных кодах этого ЦПУ может выглядеть следующим образом:
Operation Value Meaning 100 11110 LOAD 11110 101 10100 STORE 10100 110 10100 INCREMENT 10100 000 00000 PAUSE

Находясь настолько близко к железу, машинные коды не подходят для представления высокоуровневых понятий таких как: игральная карта или запись о студенте .

Языки программирования

Языки программирования разработаны быть эффективным способом программирования ЦПУ с возможностью представления высокоуровневых концепций. Языкам программирования не приходится иметь дело с ограничениями аппаратуры; их главная задача - простота использования и выразительность. Языки программирования легче понимаются людьми, они ближе к естественным языкам:
if (a_card_has_been_played()) { display_the_card(); }

Однако языки программирования придерживаются гораздо более строгих и формальных правил, чем любой язык, на котором говорят.

Компилируемые языки

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

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

D - компилируемый язык.

Интерпретируемые языки

Некоторые языки программирования не требует компиляции. Такие языки называются интерпретируемыми . Программа может запущена прямо из свеженапечатанного исходного кода. Некоторые примеры интерпретируемых языков: Python, Ruby и Perl. Так как этап компиляции отсутствует, для таких языков разработка программы может быть проще. С другой стороны, так как инструкции программы должны быть разобраны для интерпретации каждый раз, когда программа запускается, программа на таких языках медленнее, чем их эквиваленты, написанные на компилируемых языках.

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

Компилятор

Назначение компилятора - трансляция: он транслирует программы, написанные на языке программирования, в машинный код. Это перевод из языка программиста на язык ЦПУ. Такая трансляция называется компиляцией . Каждый компилятор понимает какой-то конкретный язык программирования и описывается, как компилятор для этого языка, например «компилятор для D».

Ошибки компиляции

Так как компилятор компилирует программу согласно правилам языка, он останавливает компиляцию как только достигает некорректных инструкций. Некорректные инструкции - те, которые выбиваются из спецификаций языка. Проблемы, такие как: несоответсвующие скобки, отсутствующая точка с запятой, ключевое слово с опечаткой и т.д. - все вызывают ошибки компиляции.

Также компилятор генерирует предупреждения компиляции , когда он видит подозрительный кусок кода, который может вызывать беспокойство, но не обязательно является ошибкой. Однако, предупреждения практически всегда указывают на настоящую ошибку или плохой стиль, поэтому самая распространенная практика - рассматривать большинство или все предупреждения как ошибки.

Фундаментальные типы

Ранее мы выяснили, что мозгом компьютера является ЦПУ. Большинство задач программы выполняются на ЦПУ, и остальные распределены по другим частям компьютера.

Наименьшая единица данных в компьтере называется битом , которая может принимать значения 0 или 1.

Так как тип данных, который может содержать только значения 0 или 1 имел бы очень ограниченное применение, ЦПУ определяет бОльшие типы данных, которые являются комбинациями из более одного бита. Например, байт содержит 8 битов. Наиболее производительный тип данных определяет битность ЦПУ: 32-битный ЦПУ, 64-битный ЦПУ и т.п.

Типов, определенных ЦПУ, все еще недостаточно: они не могут описать высокоуровневые понятия, как имя студента или игральная карта . D предоставляет множество полезных типов данных, но даже этих типов не хватает для описания многих высокоуровневых концепций. Такие концепции должны быть определены программистом как структуры (struct) или классы (class), которые мы увидим в следующих главах.

Фундаментальные типы D очень похожи на фундаментальные типы многих других языков, как показано в следующей таблице. Термины, появляющиеся в этой таблице, объяснены ниже:

Тип Определение Начальное значение
bool Логическое значение Истина/Ложь false
byte 8 битное число со знаком 0
ubyte 8 битное число без знака 0
short 16 битное число со знаком 0
ushort 16 битное число без знака 0
int 32 битное число соз знаком 0
uint 32 битное число без знака 0
long 64 битное число со знаком 0
ulong 64 битное число без знака 0
float 32 битное действительное число с плавающей точкой float.nan
double 64 битное действительное число с плавающей точкой double.nan
real наибольшое число с плавающей точкой, которое поддерживается оборудованием real.nan
ifloat мнимая часть комплексного числа для float float.nan * 1.0i
idouble мнимая часть комплексного числа для double double.nan * 1.0i
ireal мнимая часть комплексного числа для real real.nan * 1.0i
cfloat комплексный вариант float float.nan + float.nan * 1.0i
cdouble комплексный вариант double double.nan + double.nan * 1.0i
creal комплексный вариант real real.nan + real.nan * 1.0i
char символ UTF-8 (code point) 0xFF
wchar символ UTF-16 (code point) 0xFFFF
dchar символ UTF-32 (code point) 0x0000FFFF

В довесок к вышеперечисленным, ключевое слово void описывает сущности не имеющие типа . Ключевые слова cent и ucent зарезервированы для будущего использования для представления знаковых и безнаковых 128-битных значений.

Вы можете использовать int для большинства значений, до тех пор пока нет особых причин не делать это. Рассмотрите double для представления понятий, которые имеют дробную часть.

Разъяснения понятий, представленных в таблице

Логическое значение : тип логического выражения, имеющий значения true для истины и false для лжи.

Тип со знаком: Тип, который может принимать отрицательные и положительные значения. Например, byte может иметь значения от 0 до 255. Буква u в начале названия этих типов взята от слова unsigned .

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

Только числа с плавающей точкой могут представлять десятичные дроби; целочисленные типы (например, int ) могут держать только такие значения, как 1 и 2.

Комлексные типы чисел: Эти типы могут представлять комплексные числа из математики.

Мнимые типы чисел: Эти типы могут представлять только мнимую часть комплексных чисел. Буква i , которая находится в колонке начальных значений, в математике является квадратным корнем из -1.

nan: Сокращение от «not a number», представляющее некорректное значение с плавающей точкой .

Комментарии переводчика

Может показаться странным, что для чисел с плавающей точкой используются NaN значения как инициализаторы. Оффициальное объяснение от разработчиков (хотя я не согласен с этой позицией):

NaN"ы обладают интересным свойством, что в какой бы операции они не использовались, то в результате получается снова NaN. Поэтому NaN будет распространяться и будет результатом любых вычислений, где он был использован. Это означает, что внезапно появляющийся NaN является недвусмысленным индикатором того, что была использована непроиницилизированная переменная.

Если бы 0.0 использовался как начальное значение, его последствия легко было бы не заметить на выходе, поэтому если инициализация по умолчанию была непреднамеренная, баг мог бы остаться незамеченным.

Не подразумевается, что инициализатор по умолчанию должен быть полезным значением, его цель - вскрытие багов. NaN хорошо подходит под эту роль.

Но, конечно, компилятор может обнаружить и сгенерировать ошибку о переменных, которые не были проинициализированы? В большинстве случаев он может, но не всегда, и то, что он может, зависит от сложности внутреннего анализа потоков данных. Поэтому надежда на этот механизм - непереносимое и ненадежное решение.

Из-за особенностей реализации ЦПУ, NaN отсутствует для целочисленных, поэтому вместо этого D использует 0. Нулевое значение не имеет таких преимуществ для обнаружения ошибок, какими обладает NaN, но, по крайней мере, ошибки от непреднамеренной инициализации будут повторимы и поэтому более отлаживаемыми.

Свойства типов

В D у типов есть свойства . Чтобы получить значение свойства, нужно написать имя свойства после типа через точку. Например, sizeof свойство int вызывается так: int.sizeof . В этой главе рассматриваются только следующие четыре атрибута:
  • stringof: имя типа
  • sizeof: размер типа в байтах (для кол-ва битов умножьте на 8)
  • min: минимальное значение, которое может принимать тип
  • max: максимальное значение, которое может принимать тип

Программа, которая печатает эти свойства для int

import std.stdio ;

void main()
{
writeln("Тип: " , int .stringof ) ;
writeln("Длина в байтах: " , int .sizeof ) ;
writeln("Минимальное значение: " , int .min ) ;
writeln("Максимальное значение: " , int .max ) ;
}


Примечания переводчика

Некоторые типы имеют другие свойства, например float и double не имеют свойства min , вместо него используется -float.max и -double.max , .

size_t

Вы также встретитесь с типом size_t , имя которого расшифровывается как «size type». Он не является самостоятельным типом, а псевдонимом безнакового типа, которого хватит для представления всех возможных адресов в памяти. Поэтому этот тип зависит от системы: uint на 32-х битных, ulong для 64-х битных системах и т.д.

Можно использовать свойство .stringof , чтобы увидеть на какой тип ссылается size_t на вашей системе:

Код

import std.stdio ;

void main()
{
writeln(size_t .stringof ) ;
}

Вывод на моей системе:
ulong

Упражнения

Распечатайте свойства других типов.

Примечание :Нельзя использовать зарезервированные типы cent и ucent в любой программе; как исключение, void не имеет свойств .min и .max .

Решение

import std.stdio ;

void main()
{
writeln("Тип: " , short .stringof ) ;
writeln("Размер в байтах: " , short .sizeof ) ;
writeln("Минимальное значение: " , short .min ) ;
writeln("Максимальное занчение: " , short .max ) ;

Writeln() ;

Writeln("Тип: " , ulong .stringof ) ;
writeln("Размер в байтах: " , ulong .sizeof ) ;
writeln("Минимальное значение: " , ulong .min ) ;
writeln("Максимальное занчение: " , ulong .max ) ;
}



Базовый материал

Оператор присваивания и порядок вычислений

Первые два камня преткновения, которые ждут изучающих программирование, это - оператор присваивания и порядок вычислений.

Оператор присваивания
Вы будете встречать подобные строчки практически во всех программах, практически во всех языках программирования:
a = 10;

Значение этой строчки - «сделай значение a равным 10». Аналогично следующая строчка обозначает «сделай значение b равным 20»:
b = 20;

На основе вышеизложенной информации что можно сказать о следущей строчке?
a = b;

К сожалению, эта строчка не содержит оператор сравнения из математики, который, я предполагаю, все знают. Выражение выше не означает «a равно b»! Когда мы используем ту же логику, что и в предыдущих двух строчках, выражение выше должно означать «сделай значение a равным значению b ».

Хорошо известный символ = в математике имеет совершенно другой смысл в программировании: «сделать значение слева равным значению с правой стороны».

Порядок вычислений
Операции программы выполняются шаг за шагом в определенном порядке. Предыдущие три выражения в программе можно увидеть в следующем порядке:
a = 10; b = 20; a = b;

Смысл этих строк вместе: «сделай значение a равным 10, затем сделай значение b равным 20, затем сделай значение a равным значению b ». Соответственно, после выполнения этих трех операций оба значения a и b будут равны 20.

Упражнение

Изучите, как следующие три операции меняют местами значения a и b . Если в начале их значения были равны 1 и 2 соответственно, то после выполнения операций значения становятся равны 2 и 1:
c = a; a = b; b = c;

Решение

Значения a , b и c напечатаны справой стороны каждой операции:
в начале → a 1, b 2, c неважно c = a → a 1, b 2, c 1 a = b → a 2, b 2, c 1 b = c → a 2, b 1, c 1
В конце значения a и b меняются местами.


Теги:

  • dlang
  • tutorials
  • tutorials d
Добавить метки

Этого цикла, я хочу обратить ваше внимание на ключевые принципы программирования, которые влияют на всё то, что мы делаем, но с которыми мы редко сталкиваемся напрямую и поэтому не до конца их понимаем. Тема сегодняшней статьи - компилируемые и интерпретируемые языки.

Будучи разработчиками, мы часто сталкиваемся с такими понятиями, как компилятор и интерпретатор, но я считаю, что многие не совсем понимают, что они означают. Между тем, компиляция и интерпретация - это основы работы всех языков программирования. Давайте взглянем на то, как на самом деле устроены эти понятия.

Вступление

Мы полагаемся на такие инструменты, как компиляция и интерпретация, чтобы преобразовать наш код в форму, понятную компьютеру. Код может быть исполнен нативно, в операционной системе после конвертации в машинный (путём компиляции) или же исполняться построчно другой программой, которая делает это вместо ОС (интерпретатор).

Компилируемый язык - это такой язык, что программа, будучи скомпилированной, содержит инструкции целевой машины; этот машинный код непонятен людям. Интерпретируемый же язык - это такой, в котором инструкции не исполняются целевой машиной, а считываются и исполняются другой программой (которая обычно написана на языке целевой машины). Как у компиляции, так и у интерпретации есть свои плюсы и минусы, и именно это мы и обсудим.

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

Компилируемые языки

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

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

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

Интерпретируемые языки

В отличие от компилируемых языков, интерпретируемым для исполнения программы не нужен машинный код; вместо этого программу построчно исполнят интерпретаторы. Раньше процесс интерпретации занимал очень много времени, но с приходом таких технологий, как , разрыв между компилируемыми и интерпретируемыми языками сокращается. Примерами интерпретируемых языков являются , и . Вот некоторые из концептов, которые стали проще благодаря интерпретируемым языкам:

  • Независимость от платформы;
  • Меньший размер исполняемых файлов:

Основным недостатком интерпретируемых языком является их невысокая скорость исполнения. Тем не менее, JIT-компиляция позволяет ускорить процесс благодаря переводу часто используемых последовательностей инструкции в машинный код.

Бонус: байткод-языки

Байткод-языки - это такие языки, которые используют для исполнения кода как компиляцию, так и интерпретацию. Java и фреймворк.NET - это типичные примеры байткод-языков. На самом деле, Java Virtual Machine (JVM) - это настолько популярная виртуальная машина для интерпретации байткода, что на ней работают реализации нескольких языков . Кстати, недавно , что в новой версии Java будет также поддерживаться и статическая компиляция.

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

При компиляции кода в байткод происходит задержка, но дальнейшая скорость исполнения значительно возрастает в силу оптимизации байткода. Кроме того, байткод-языки являются платформо-независимыми, превосходя при этом по скорости интерпретируемые. Для них также доступна JIT-компиляция.

Заключение

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

Вкратце, компилируемые языки являются самыми эффективными, поскольку они исполняются как машинный код и позволяют использовать аппаратное обеспечение системы. Однако это вводит дополнительные ограничение на написание кода и делает его платформо-зависимым. Интерпретируемые же языки не зависят от платформы и позволяют использовать такие техники динамического программирования, как метапрограммирование. Тем не менее, в скорости исполнения они значительно уступают компилируемым языкам.

Байткод-языки, в свою очередь, пытаются использовать сильные стороны обоих видов языков, и у них это неплохо получается.

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

Целью компиляции является реализация возможности выполнения реальным компьютером кода, написанного для абстрактной машины. Компиляция , однако, – лишь одна из двух базисных технологий, применяемых для достижения нашей цели.

Базисные схемы

Вместо компиляции программы можно ее интерпретировать. Рисунок ниже иллюстрирует разницу подходов, игнорируя роль входных данных.


Рис. 3.6.

И компилятор, и интерпретатор являются программами, на вход которых подаются программы, написанные на языке программирования. Компилятор транслирует переданную ему программу в целевую форму – машинный код, который уже может быть непосредственно выполнен на компьютере, создавая выходные результаты вычислений. Обработка той же исходной программы интерпретатором не создает целевой программы, но непосредственно выполняет ее, получая выходные результаты.

Интерпретатор должен быть способен определить эффект выполнения каждой конструкции языка программирования. Как пример того, как интерпретатор выполняет свою задачу, рассмотрим интерпретацию присваивания x: = x +1 . Интерпретатор должен хранить таблицу всех используемых переменных и связанных с ними значений. Он вычисляет новое значение x , добавляя 1 к старому значению, хранящемуся в таблице, а затем выполняет присваивание, заменяя старое значение x значением вычисленного выражения.

Компилятор будет генерировать машинный код, создающий тот же эффект, используя команды компьютера и адреса памяти (а не структуру более высокого уровня, такую как таблица).

В дополнительном упражнении для небольшого языка потребуется, применяя эти идеи, написать компилятор и интерпретатор.

Если рассматривать язык программирования как машинный код для абстрактной машины, можно сказать, что интерпретатор – это программа, моделирующая вычисление на этой машине. Машинная память в этом случае также абстрактна и представляет структуры данных в виде таблиц интерпретатора "переменные – значения".

На следующем рисунке процессы компиляции и интерпретации дополнены вводом данных:


Рис. 3.7.

Рисунок демонстрирует еще одну разницу между компилятором и интерпретатором. У интерпретатора два источника ввода – исходная программа и входные данные; а компилятору подается только программа. В последующем обсуждении этому различию придадим математическую форму.

Компилировать или интерпретировать? Эта проблема – предмет широко рассмотрения в компьютерных науках. Что лучше: непосредственно обрабатывать исходную информацию в том виде, как она есть, или предварительно привести ее к более удобной форме? Этот вопрос стоит не только при обработке программ, мы будем сталкиваться с ним и при изучении алгоритмов.

У компиляторов и интерпретаторов имеются свои достоинства. Возможны различные критерии. По производительности – времени выполнения программы – компиляторы побеждают.

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

Все меняется, если в качестве критерия выбрать удобство и скорость разработки. Компилятор стоит между вами и реализацией вашей последней идеи: прежде чем увидеть результаты последнего изменения в программе, необходимо ждать результата компиляции (и связывания, о чем ниже пойдет речь). При интерпретации выполнение начинается незамедлительно.

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

Еще один критерий, согласно которому предпочтение опять-таки отдается компиляции, состоит в надежности программы. Компиляторы не просто транслируют программу – в процессе компиляции они осуществляют различные проверки, например, контроль типов для статически типизированных языков . Тем самым многие ошибки устраняются еще на этапе компиляции, в то время как для режима интерпретации ошибки обнаруживаются в процессе выполнения на одном из сеансов работы.

В принципе, интерпретатор также способен выполнять некоторые из проверок перед выполнением программы. Фактически, чистые интерпретаторы не применяются, они представляют собой всегда некоторую смесь компиляции и интерпретации.

Комбинирование компиляции и интерпретации

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

Заметим, что 100% схема интерпретации имеет мало смысла: каждый раз, когда интерпретатор выполнял очередной оператор, например, оператор цикла, он должен был бы возвращаться многократно к фактической последовательности символов и осуществлять ее разбор. Любое реалистическое решение не могло бы согласиться с такой неразумной тратой ресурсов. Так что фактически интерпретатор также начинает с преобразования входа в форму, приемлемую для интерпретации, например, строя абстрактное синтаксическое дерево. В ходе этого процесса, как отмечалось, возможен контроль проверки типов. Так что даже тогда, когда можно прочесть, что используется интерпретатор языка, частичная компиляция подразумевается.

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


Рис. 3.8.

Смешанная стратегия предполагает, что компилятор создает код на промежуточном языке , понимаемом некоторой виртуальной машиной – VM на рисунке. Такой подход объединяет преимущества компиляции и интерпретации. Благодаря тщательно спроектированной виртуальной машине возможно получить:

  • переносимость, так как VM-код не зависит от специфики физических процессоров;
  • повышение эффективности, поскольку создаваемый промежуточный код легко интерпретируется.

Виртуальные машины, байт-код и JIT (Just In Time) компиляторы

Реализация современных языков – Java, C#, других языков.Net – основана на смешанном решении. Промежуточный код для Java называется байт-кодом. В термине отражается тот факт, что виртуальная машина использует компактные команды, подобные командам фактического процессора, где каждая команда содержит код команды – типично задаваемый одним байтом, – после которого следует 0, 1 или 2 аргумента команды.

Альтернативой байт-коду могла бы выступать виртуальная машина, непосредственно работающая со структурами данных, например, с абстрактным синтаксическим деревом для представления структуры программы и с хэш-таблицами для хранения свойств переменных. Но байт-код обеспечивает лучшую эффективность периода выполнения, как по времени, так и по памяти.

Прием двухэтапной компиляции был использован еще в семидесятые годы при реализации компилятора с языка Паскаль. Он получил второе рождение с распространением Интернета, так как хорошо был приспособлен для локального выполнения Web-клиентами. Поставщики апплетов – небольших программ – могли компилировать их в байт-код и поставлять их в такой форме. Дополнительным преимуществом к компактности стала переносимость кода, поскольку в противном случае машинный код пришлось бы создавать для каждой возможной целевой платформы .

Для выполнения апплета пользователям необходим только интерпретатор байт-кода. Они даже не должны знать, что такой интерпретатор существует, если он встроен в их Web-браузер, Поскольку при таком подходе возникают потенциальные риски, связанные с безопасностью, – жульнические или некорректные апплеты могут повредить ваш компьютер, – по этой причине для апплетов необходим интепретатор, который будет строго контролировать операции, разрешенные для апплетов.

Поставка программ через апплеты достигла некоторого успеха, но не стала основным способом распределенного ПО, как ожидалось в момент появления Java. Частично это связано с проблемами безопасности, но главная причина – в потере эффективности, возникающей по причине интерпретации. Большинство успешных апплетов являются небольшими программами, предназначенными для выполнения на Web-странице, включающие визуальную компоненту, при наличии которой потери времени представляются несущественными.

Для улучшения эффективности времени выполнения байт-кода применяются JIT (Just In Time) компиляторы, называемые джитерами, – осуществляющие компиляцию по требованию. Основная идея состоит в том, что машинный код для некоторого модуля создается "на лету", в тот момент, когда он первый раз вызывается на выполнение (не следует путать любителя джаза –jitterbug, с ошибками такого компилятора – jitter bug ). Внесем соответствующие дополнения в предыдущий рисунок, который теперь выглядит так:


Рис. 3.9.

Обычно, как показано на рисунке, наряду с компиляцией "на лету" (джитингом) остается и возможность интерпретации байт-кода. Компиляция "на лету" обычно имеет место при первом использовании модуля (метода или всего класса), так что она будет нужна только для кода, фактически используемого в этом сеансе выполнения. В сравнении с традиционным компилятором, который компилирует всю программу, такой подход позволяет создавать более компактный код, сокращает время компиляции, но, что более важно, делает компиляцию частью процесса выполнения. Последнее является серьезным недостатком, поскольку к времени выполнения добавляются расходы на компиляцию, так что само время выполнения становится менее предсказуемым.

С первого взгляда кажется, что при таком подходе не стоит выполнять проверки типов и другой контроль, поскольку кому же хочется во время выполнения получать сообщения о нарушении согласованности типов ? Это возвращало бы нас к проблемам динамически типизированных языков . Конечно, нам хотелось бы, чтобы все необходимые проверки выполнялись на первом шаге компиляции при создании байт-кода, так, чтобы любой код, передаваемый джитеру, был безопасным. К сожалению, эти утешительные предположения нереалистичны в распределенной среде, где опять возникают проблемы безопасности. Если вы загружаете байт-код из сайта, то можете ли вы знать, прошел ли он проверку? В общем случае – нет. Но тогда нарушения типа могут стать не только причиной нарушения надежности и аварийного завершения программы, все может быть гораздо хуже: в результате атаки становится возможным нарушение безопасности.

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

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

Стратегия компиляции в EiffelStudio также включает байт-код, но, как мы увидим, она использует различные способы комбинирования интерпретации и компиляции.


Notice : Функция get_currentuserinfo с версии 4.5.0 считается устаревшей ! Используйте wp_get_current_user(). in /hlds/web/u138079p19/сайт/htdocs/wp-includes/functions.php on line 3840

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

И вот перед выпуском s4g пообщавшись с товарищем … решил поставить под сомнение свое понимание этих понятий и безотносительно выяснить что это все значит, и как оказалось совсем не зря, потом что даже вики говорит о размытости границ между этими понятиями:

Классификация языков программирования на компилируемые и интерпретируемые является неточной и весьма условной. Существуют реализации языков, которые компилируют исходный текст программы в байт-код, который затем либо интерпретируется , либо выполняется JIT-компилятором или виртуальной машиной . Это привносит ещё больше неясности в вопрос о том, где именно должна быть проведена граница.

Примечание: данная статья имеет анализ, но это не значит что он абсолютно верный, возможно где-то я субъективно интерпретировал понятия))

В среде программистов обычно под понятием компилируемый язык подразумевается любой язык который компилируется в машинный код, а интерпретируемый — это язык который не компилируется в машинный код (но может компилироваться в байт-код).

Теперь необходимо понять что такое компиляция, опять обращаемся к вики :

Компиляция - трансляция программы, составленной на исходном языке высокого уровня, в эквивалентную программу на низкоуровневом языке, близком машинному коду.

Результатом работы компилятора может быть программа на специально созданном низкоуровневом языке, подлежащем интерпретации виртуальной машиной. Такой язык называется псевдокодом или байт-кодом .

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

Разберем теперь понятие интерпретация, опять же обращаемся к вики :

Интерпретация (в математике, логике) - совокупность значений (смыслов), придаваемых тем или иным способом элементам (выражениям, формулам, символам и т. д.) какой-либо естественнонаучной или абстрактно-дедуктивной теории.

Интерпретация исходного кода - пооператорный (покомандный, построчный) анализ, обработка и тут же выполнение исходной программы или запроса (в отличие от компиляции, при которой программа транслируется без её выполнения).

Казалось бы … теперь все понятно, но сомнения все-таки остались. Ну а как же тогда точно расставить все по местам? Ведь даже при таком раскладе получается что практически для любого языка можно написать интерпретатор?

Процесс компиляции состоит из следующих этапов:

  1. Лексический анализ

  2. Синтаксический анализ

  3. Генерация кода

Все-равно мало …

Обратимся еще раз к понятию интерпретация : совокупность значений, придаваемых тем или иным способом элементам какой-либо естественнонаучной или абстрактно-дедуктивной теории. Значит ли это что интерпретация это одна итерация?

Разберем процесс компиляции любого языка у которого есть хотя бы лексический анализ, синтаксический и генератор кода (не важно машинный или байт код, главное есть наличие генератора кода):

  • сначала на вход поступает поток символов (исходный код), который разбивается на лексемы, то есть первая итерация интерпретации — делаем понятным поток символом для синтаксического анализатора (интерпретируем для парсера);
  • на этапе синтаксического анализа строим абстрактное синтаксическое дерево (АСТ), то есть вторая итерация — делаем понятными лексемы (не исходный код) для генератора кода (интерпретируем для генератора кода);
  • на этапе генерации кода преобразуем АСТ в код (машинный либо байт-код), то есть третья итерация — делаем понятным АСТ (не лексемы и уж тем более не исходный код) для машины (реальной либо виртуальной) (интерпретация для машины), итог — код.

Получая на вход исходный код , пропуская его через 3 фильтра (лексический анализатор, синтаксический анализатор, генератор кода) получается на выходе готовый к исполнению код . А вот какой код, машинный, либо байт-код это уже совсем другое дело, и нам абсолютно безразлично кто его будет исполнять, реальная либо виртуальная машина, самое главное что получился код.

При таком раскладе получается что исходный код имел несколько этапов представления (восприятия) , на которых он единообразно понимался по разному. То есть исходный код в таком случае (в случае компилируемого языка программирования) имеет внутренние промежуточные состояния восприятия разными системами компилятор . На этапе лексического анализа это одно состояние, на этапе синтаксического иное.

Таким образом любой язык имеющий более одной итерации интерпретации будет являться компилируемым . Либо любой такой язык можно назвать языком с определенным количеством интерпретаций, потому что название такому языку «интерпретируемый» будет как минимум не полное описание его типа восприятия.

Ладно, определились, но что тогда в таком случае будет являться интерпретируемым языком программирования? Судя по всему, таковым языком будет тот язык, который имеет только одну итерацию интерпретации . Утверждать не буду, но ИМХО, такой язык лишен высокоуровневости, и будет представлять из себя нечто вроде:

Push 10 push 5 add

То есть некое подобие ассемблера, похожего на байт-код виртуальной машины))

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

Часто скриптовые языки называют интерпретируемыми еще и потому что генератор кода в них называется компилятором, это конечно же не верно, и мы сами неверно назвали в s4g 0.9.2 генератор кода компилятором. Компилятор это нечто большее чем простая интерпретация АСТ в байт-код. В следующих версиях будем исправляться))

Также можно услышать аргумент о том что виртуальная машина интерпретирует байт-код, поэтому язык в котором есть виртуальная машина будет интерпретируемым. В таком случае центральный процессор тоже интерпретирует машинный код. И второе утверждение настолько же верно насколько верно первое))

Если уж компилируемый язык компилируется в машинный код … тогда он будет исполнятся как на Windows так и на Linux с аналогичным железом?))

Итак, запрепим:

Интепретатор — программа исполняющая исходный код пооператорно (построчно, по командно).

Интерпретируемый язык программирования — это язык программирования имеющий один уровень итерации (восприятия) исходного кода. Промежуточное представление кода отсутствует.

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

До сих пор мы говорили о том, что программу, написанную на высокоуровневом языке, перед выполнением следует откомпилировать, т. е. перевести на машинный язык, состоящий из двух знаков, соответствующих двум устойчивым состояниям. Компилятор – это тоже компьютерная программа, на вход которой подается файл с исходным текстом, написанным на языке высокого уровня. Этот файл во многих операционных системах называется исходным модулем (source module) . Компилятор переводит программу на машинный язык и записывает ее в другой файл, называемый объектным модулем (object module) .

Содержимое объектного модуля в самых простых случаях можно загрузить в оперативную память и выполнить. Но чаще всего объектный модуль перед выполнением необходимо еще скомпоновать (link) с другими объектными модулями, содержащие дополнительные модули программы, системные функции, служебные сведения и прочие данные, необходимые для выполнения программы.

Компоновку выполняет специальная программа, так и называемая «компоновщиком» (linker) или редактором связей. На ее вход подаются файлы с объектными модулями, а на выходе получается исполнимый модуль (executable module) – файл с полностью готовой к выполнению программой. Этот файл загружается в оперативную память и выполняется.

Очень часто компиляцию и компоновку объединяют в одно действие. Для этого компилятору дается специальное указание, и он сразу же после своей работы сам вызывает компоновщик. Программисту все это представляется единым процессом, только на экране дисплея появляются сообщения об окончании компиляции и начале компоновки.

Все описанное выше характерно для компилируемых языков программирования. Другой класс языков, составляющих интерпретируемые языки программирования, не требующие компиляции и компоновки. Это тоже языки высокого уровня, поэтому программы, написанные на них, часто называемые скриптами (script) или сценариями , требуют перевода на машинный язык. Для этого создается программа-интерпретатор операторов , записанных в программе. В отличие от компилятора программа-интерпретатор переводит на машинный язык не весь текст целиком, а каждый оператор по отдельности, и тут же выполняет полученный машинный код.

Интерпретация программы происходит медленнее, чем выполнение исполнимого модуля, из-за того, что каждый оператор сначала преобразуется в машинные коды. Кроме того, в процессе интерпретации могут неожиданно возникнуть ошибки, ведь исходный текст не просматривается заранее в поисках ошибок.

Тем не менее, интерпретируемые языки широко распространены из-за удобства создания программ с их помощью. Программу можно написать и сразу выполнять без предварительных преобразований. Если работа программы кажется неудовлетворительной, то ее текст можно изменить и тут же выполнить программу заново без всякой перекомпиляции.

В современном программировании разница между компилируемыми и интерпретируемыми языками стирается. Это слияние происходит по двум направлениям. С одной стороны, для интерпретируемых языков создаются компиляторы. Классическим интерпретируемым языком всегда считался язык BASIC , но сейчас для него написано много компиляторов. У программиста есть выбор. Программу, написанную на BASIC , можно интерпретировать и сразу выполнить, а можно предварительно откомпилировать в исполнимый модуль и выполнить в другое, более удобное время.

С другой стороны, интерпретаторы научились сохранять машинный код уже проинтерпретированных и выполненных операторов. При повторном выполнении этих операторов, например, в циклах, интерпретатор использует готовые машинные команды, что значительно ускоряет работу. Такие интерпретаторы называются JIT-интерпретаторами (Just-In-Time) . Они работают значительно быстрее классических интерпретаторов и поэтому приобретают все большее распространение.