Введение

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

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

Непосредственно анализировать исходный код и выполнять его;
Преобразовывать исходный код в эффективное промежуточное представление или объектный код и немедленно выполнять его;
Явно выполнять предварительно скомпилированный байт-код, созданный компилятором и соответствующий виртуальной машине интерпретатора. Ранние версии языка программирования Lisp и диалекты BASIC для мини- и микрокомпьютеров являются примерами первого типа. Perl, Raku, Python, MATLAB и Ruby — примеры второго типа, а UCSD Pascal — пример третьего типа. Исходные программы компилируются заранее и хранятся в виде машинно-независимого кода, который затем связывается во время выполнения и выполняется интерпретатором и/или компилятором (для JIT-систем). Некоторые системы, такие как Smalltalk и современные версии BASIC и Java, могут также сочетать два и три типа. Интерпретаторы различных типов также были созданы для многих языков, традиционно связанных с компиляцией, таких как Algol, Fortran, Cobol, C и C++. Хотя интерпретация и компиляция — два основных способа реализации языков программирования, они не являются взаимоисключающими, поскольку большинство систем интерпретации также выполняют некоторую работу по переводу, как и компиляторы. Термины «интерпретируемый язык» или «компилируемый язык» указывают на то, что каноническая реализация этого языка — интерпретатор или компилятор, соответственно. Язык высокого уровня в идеале является абстракцией, независимой от конкретных реализаций.

История

Интерпретаторы использовались уже в 1952 году, чтобы упростить программирование в условиях ограничений компьютеров того времени (например, недостаток памяти для программ или отсутствие встроенной поддержки чисел с плавающей точкой). Интерпретаторы также применялись для трансляции между машинным кодом низкого уровня, что позволяло писать код для машин, которые еще находились в разработке, и тестировать его на уже существующих компьютерах. Первым высокоуровневым интерпретируемым языком программирования был Lisp. Lisp впервые был реализован Стивом Расселом на компьютере IBM 704. Рассел прочитал статью Джона Маккарти "Рекурсивные функции символических выражений и их вычисление машиной, часть I" и осознал (к удивлению Маккарти), что функцию eval в Lisp можно реализовать на машинном коде. Результатом стал рабочий интерпретатор Lisp, который можно было использовать для выполнения Lisp-программ, или, точнее, "вычисления Lisp-выражений".

Общая работа

Интерпретатор обычно состоит из набора известных команд, которые он может выполнять, и списка этих команд в том порядке, в котором программист желает их выполнить. Каждая команда (также известная как инструкция) содержит данные, которые программист хочет изменить, и информацию о том, как эти данные изменить. Например, интерпретатор может прочитать ADD Books, 5 и интерпретировать это как запрос на добавление пяти к переменной Books. Интерпретаторы располагают широким спектром инструкций, предназначенных для выполнения различных задач, но обычно в них встречаются инструкции для базовых математических операций, условного перехода и управления памятью, что делает большинство интерпретаторов полными по Тьюрингу. Многие интерпретаторы также тесно интегрированы со сборщиком мусора и отладчиком.

Компиляторы против интерпретаторов

Программы, написанные на языке высокого уровня, либо непосредственно выполняются каким-либо интерпретатором, либо преобразуются в машинный код компилятором (и ассемблером и линкером) для выполнения центральным процессором. В то время как компиляторы (и ассемблеры) обычно генерируют машинный код, непосредственно исполняемый компьютерным оборудованием, они часто могут (опционально) генерировать промежуточную форму, называемую объектным кодом. Это, по сути, тот же машинный код, но дополненный таблицей символов с именами и метками, чтобы сделать исполняемые блоки (или модули) идентифицируемыми и перемещаемыми. Компилируемые программы обычно используют строительные блоки (функции), хранящиеся в библиотеке таких модулей объектного кода. Линкер используется для объединения (заранее подготовленных) файлов библиотеки с объектными файлами приложения, чтобы сформировать единый исполняемый файл. Объектные файлы, используемые для генерации исполняемого файла, таким образом, часто создаются в разное время и иногда даже разными языками (способными генерировать один и тот же объектный формат). Простой интерпретатор, написанный на языке низкого уровня (например, на ассемблере), может иметь аналогичные блоки машинного кода, реализующие функции языка высокого уровня, которые хранятся и выполняются, когда запись функции в таблице поиска указывает на этот код. Однако интерпретатор, написанный на языке высокого уровня, обычно использует другой подход, например, генерацию и последующий обход дерева разбора, или генерацию и выполнение промежуточных программных инструкций, или и то, и другое. Таким образом, как компиляторы, так и интерпретаторы обычно преобразуют исходный код (текстовые файлы) в токены, оба могут (или не могут) генерировать дерево разбора, и оба могут генерировать немедленные инструкции (для стековой машины, четырехкодового представления или другими способами). Основное различие заключается в том, что компиляторная система, включая (встроенный или отдельный) линкер, генерирует автономную программу машинного кода, в то время как интерпретаторная система вместо этого выполняет действия, описанные программой высокого уровня. Компилятор может, таким образом, выполнить почти все преобразования от семантики исходного кода до машинного уровня один раз и навсегда (то есть до тех пор, пока программа не будет изменена), в то время как интерпретатор должен выполнять часть этой работы по преобразованию каждый раз, когда выполняется оператор или функция. Однако в эффективном интерпретаторе большая часть работы по переводу (включая анализ типов и тому подобное) выносится и выполняется только в первый раз, когда запускается программа, модуль, функция или даже оператор, что весьма похоже на то, как работает компилятор. Тем не менее, компилированная программа все равно работает намного быстрее, в большинстве случаев, отчасти потому, что компиляторы предназначены для оптимизации кода и могут иметь достаточно времени для этого. Это особенно верно для более простых языков высокого уровня, не имеющих (множества) динамических структур данных, проверок или проверки типов. В традиционной компиляции исполняемый вывод линкера (.exe файлы или .dll файлы или библиотека, см. рисунок) обычно является перемещаемым при запуске в общей операционной системе, как и модули объектного кода, но с той разницей, что это перемещение выполняется динамически во время выполнения, то есть когда программа загружается для выполнения. С другой стороны, скомпилированные и связанные программы для небольших встраиваемых систем обычно статически выделяются, часто жестко кодируются в флэш-память NOR, поскольку часто нет вторичного хранилища и операционной системы в этом смысле. Исторически сложилось так, что большинство интерпретаторных систем имели встроенный редактор. Это становится все более распространенным и для компиляторов (тогда их часто называют IDE), хотя некоторые программисты предпочитают использовать редактор по своему выбору и запускать компилятор, линкер и другие инструменты вручную. Исторически компиляторы предшествовали интерпретаторам, потому что аппаратное обеспечение в то время не могло поддерживать как интерпретатор, так и интерпретируемый код, а типичная пакетная среда того времени ограничивала преимущества интерпретации.

Цикл разработки

В течение цикла разработки программного обеспечения программисты часто вносят изменения в исходный код. При использовании компилятора, каждый раз при изменении исходного кода им приходится ждать, пока компилятор переведёт изменённые исходные файлы и свяжет все файлы бинарного кода, прежде чем программа сможет быть выполнена. Чем больше программа, тем дольше приходится ждать. В отличие от этого, программист, использующий интерпретатор, тратит гораздо меньше времени на ожидание, поскольку интерпретатору обычно требуется лишь перевести код, над которым ведётся работа, в промежуточное представление (или не переводить его вовсе), что значительно сокращает время, необходимое для тестирования изменений. Результаты становятся видны сразу после сохранения исходного кода и перезагрузки программы. Отладка компилированного кода, как правило, сложнее, поскольку редактирование, компиляция и связывание – это последовательные процессы, которые необходимо выполнять в правильном порядке с использованием правильного набора команд. Поэтому многие компиляторы также имеют вспомогательную программу, известную как Makefile. Makefile содержит перечень командных строк компилятора и компоновщика, а также файлы исходного кода программы, но может принимать простой ввод из командной строки (например, "Make 3"), который выбирает третью группу (набор) инструкций и затем выдаёт команды компилятору и компоновщику, передавая указанные файлы исходного кода.

Распространение

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

Регрессия

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

Интерпретаторы байт-кода

Существует целый спектр возможностей между интерпретацией и компиляцией, в зависимости от объема анализа, выполняемого до запуска программы. Например, Emacs Lisp компилируется в байт-код – высокосжатое и оптимизированное представление исходного кода Lisp, но не машинный код (и, следовательно, не привязан к какому-либо конкретному оборудованию). Этот "скомпилированный" код затем интерпретируется байт-код интерпретатором (самостоятельно написанным на C). В этом случае скомпилированный код является машинным кодом для виртуальной машины, реализованной не в аппаратном обеспечении, а в байт-код интерпретаторе. Такие компилирующие интерпретаторы иногда также называют компретерами. В байт-код интерпретаторе каждая инструкция начинается с байта, поэтому байт-код интерпретаторы имеют до 256 инструкций, хотя не все из них могут использоваться. Некоторые байт-коды могут занимать несколько байтов и быть произвольно сложными. Управляющие таблицы, которые не обязательно должны проходить этап компиляции, аналогично байт-код интерпретаторам, определяют соответствующий алгоритмический ход выполнения с помощью специализированных интерпретаторов.

Интерпретаторы кода с нитками

Интерпретаторы поточного кода похожи на интерпретаторы байт-кода, но вместо байтов они используют указатели. Каждая "инструкция" – это слово, указывающее на функцию или последовательность инструкций, возможно, за которым следует параметр. Интерпретатор поточного кода либо циклически извлекает инструкции и вызывает функции, на которые они указывают, либо извлекает первую инструкцию и переходит к ней, при этом каждая последовательность инструкций заканчивается извлечением и переходом к следующей инструкции. В отличие от байт-кода, эффективного ограничения на количество различных инструкций, кроме доступной памяти и адресного пространства, нет. Классическим примером поточного кода является код Forth, используемый в системах Open Firmware: исходный язык компилируется в "F-код" (некий промежуточный код), который затем интерпретируется виртуальной машиной.

Интерпретаторы абстрактного синтаксического дерева

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

Составление "только в срок"

Дальнейшее размытие границ между интерпретаторами, интерпретаторами байт-кода и компиляцией достигается с помощью JIT-компиляции (компиляции "на лету") – техники, при которой промежуточное представление компилируется в машинный код непосредственно во время выполнения. Это обеспечивает эффективность, сравнимую с выполнением машинного кода, но требует времени на запуск и увеличивает потребление памяти при первой компиляции байт-кода или абстрактного синтаксического дерева. Обычно первым опубликованным JIT-компилятором считают разработку Джона Маккарти для LISP в 1960 году. Адаптивная оптимизация – это дополнительная техника, при которой интерпретатор анализирует выполняемую программу и компилирует наиболее часто используемые её части в машинный код. Эта техника появилась несколько десятилетий назад, например, в языке Smalltalk в 1980-х годах. JIT-компиляция в последние годы привлекла широкое внимание разработчиков языков, и теперь JIT-компиляторы используются в Java, .NET Framework, большинстве современных реализаций JavaScript и Matlab.

Интерпретатор шаблонов

Различие между компиляторами и интерпретаторами вновь становится еще более размытым благодаря особой конструкции интерпретатора, известной как интерпретатор шаблонов. Вместо реализации выполнения кода посредством большого оператора switch, содержащего каждый возможный байт-код, при работе со стеком программного обеспечения или обходом дерева, интерпретатор шаблонов поддерживает большой массив байт-кода (или любого эффективного промежуточного представления), сопоставленный непосредственно с соответствующими машинными инструкциями, которые могут быть выполнены на аппаратном обеспечении хоста в виде пар ключ-значение (или, в более эффективных конструкциях, прямых адресов к машинным инструкциям), известным как "шаблон". Когда конкретный сегмент кода выполняется, интерпретатор просто загружает или переходит к сопоставлению опкода в шаблоне и непосредственно выполняет его на аппаратном обеспечении. Благодаря своей конструкции, интерпретатор шаблонов очень сильно напоминает компилятор Just-in-Time, а не традиционный интерпретатор, однако технически он не является JIT, поскольку он просто транслирует код с языка в машинные вызовы по одному опкоду за раз, а не создает оптимизированные последовательности инструкций, исполняемых ЦП, из всего сегмента кода. Благодаря простой конструкции интерпретатора, который просто передает вызовы непосредственно аппаратному обеспечению, а не реализует их напрямую, он намного быстрее, чем любой другой тип, даже интерпретаторы байт-кода, и в некоторой степени менее подвержен ошибкам, но, как компромисс, его сложнее поддерживать из-за необходимости поддержки интерпретатором трансляции на различные архитектуры вместо платформенно-независимой виртуальной машины/стека. На сегодняшний день единственными известными реализациями интерпретатора шаблонов для широко распространенных языков являются интерпретатор в официальной эталонной реализации Java, виртуальная машина Sun HotSpot Java. Значительные исследования самоинтерпретаторов (особенно рефлексивных интерпретаторов) были проведены на языке программирования Scheme, диалекте Lisp. Однако в целом любой язык, полный по Тьюрингу, позволяет написать собственный интерпретатор. Lisp – один из таких языков, поскольку программы на Lisp представляют собой списки символов и других списков. XSLT – также такой язык, поскольку программы XSLT написаны на XML. Написание языков, специфичных для предметной области (DSL), является подобластью метапрограммирования. Клайв Гиффорд ввел меру качества самоинтерпретатора (эгеноотношение) – предел отношения между компьютерным временем, затраченным на запуск стека из N самоинтерпретаторов, и временем, затраченным на запуск стека из N-1 самоинтерпретаторов, при стремлении N к бесконечности. Это значение не зависит от выполняемой программы. В книге "Структура и интерпретация компьютерных программ" представлены примеры метациркулярной интерпретации для Scheme и его диалектов. Другими примерами языков с самоинтерпретатором являются Forth и Pascal.

Микрокод

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

Компьютерный процессор

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

Понимание работы переводчика

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

Приложения

Интерпретаторы часто используются для выполнения командных языков и языков-связок, поскольку каждая команда в командном языке обычно представляет собой вызов сложной подпрограммы, такой как редактор или компилятор. Самомодифицирующийся код легко реализуется в интерпретируемом языке. Это связано с происхождением интерпретации в Lisp и исследованиях в области искусственного интеллекта. Виртуализация. Машинный код, предназначенный для конкретной аппаратной архитектуры, может быть запущен с использованием виртуальной машины. Это часто применяется, когда целевая архитектура недоступна, или, среди прочего, для запуска нескольких экземпляров. Песочница (Sandboxing): Хотя некоторые типы песочниц полагаются на средства защиты операционной системы, часто используется интерпретатор или виртуальная машина. Фактическая аппаратная архитектура и изначально предполагаемая аппаратная архитектура могут совпадать, а могут и отличаться. Это может показаться бессмысленным, если бы не тот факт, что песочницы не обязаны выполнять все инструкции из исходного кода, который они обрабатывают. В частности, они могут отказаться выполнять код, нарушающий действующие ограничения безопасности. Эмуляторы предназначены для запуска компьютерного программного обеспечения, написанного для устаревшего и недоступного оборудования, на более современном оборудовании.