Содержание


Как использовать виртуальную машину Parrot

Часть 1. Основы практического применения

Comments

Серия контента:

Этот контент является частью # из серии # статей: Как использовать виртуальную машину Parrot

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Как использовать виртуальную машину Parrot

Следите за выходом новых статей этой серии.

Использование любых языков программирования предполагает наличие некоторого механизма, выполняющего преобразование команд и инструкций того или иного языка в бинарный код для целевой системы, на которой будет выполняться программа. Компиляторы генерируют машинные коды, соответствующие заданной архитектуре. В случае интерпретируемых или скриптовых языков дело обстоит сложнее. Для них требуется определённая среда выполнения, которую называют интерпретатором или виртуальной машиной (ВМ; virtual machine).

До недавнего времени каждый интерпретируемый язык "вёз с собой" собственную виртуальную машину. Названия Perl, Python, Ruby – это не только имена скриптовых языков, но и обозначения интерпретаторов, виртуальных машин, необходимых для их функционирования. Не так давно появилась идея, а следом и реализация обобщённой виртуальной машины, позволяющей достаточно эффективно выполнять программы на различных языках программирования. Назвали этого "полиглота" Parrot. Цикл статей об этой виртуальной машине будет полезен разработчикам программ на интерпретируемых языках, а также администраторам unix-систем.

1. Введение. Что за птица такая – Parrot?

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

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

История Parrot началась во время разработки среды выполнения для Perl версии 6. В отличие от пятой версии, в Perl 6 компилятор и виртуальная машина, как среда выполнения, разделены в гораздо большей степени. Название Parrot возникло из первоапрельской шутки 2001 года о том, что, начиная со следующей версии, Perl и Python объединяются в гибридный язык. Это название выражает замысел создания виртуальной машины не только для Perl 6, но и для многих других языков.

2. Основные принципы функционирования виртуальной машины Parrot

2.1. Внутренние форматы

В настоящий момент Parrot может принимать инструкции для выполнения в четырёх форматах. В формате PIR (Parrot Intermediate Representation; промежуточное представление) инструкции могут писать программисты или генерировать компиляторы с различных языков. Этот формат позволяет скрыть некоторые аспекты более низкого уровня, например, способ передачи параметров в функцию. На один уровень ниже PIR позволяет опуститься формат PASM (Parrot Assembly), инструкции которого продолжают оставаться доступными для чтения человеком и могут быть сгенерированы компилятором. Однако здесь на разработчика полностью возлагается ответственность за все детали реализации: координация соглашений о вызовах функций и процедур, распределение регистров виртуальной машины и многое другое. В общем, ассемблер он и есть ассемблер – требуется высокая квалификация программиста.

Формат PAST (Parrot Abstract Syntax Tree) позволяет принимать в качестве входных данных абстрактную синтаксическую древовидную структуру – весьма полезно для тех, кто занимается разработкой компиляторов.

Все описанные выше форматы в любом случае автоматически переводятся в четвёртый формат PBC (Parrot Bytecode). Байт-код представляет собой скомпилированный бинарный код, предназначенный исключительно для интерпретатора Parrot. Человек вряд ли что-то сможет прочитать, и уж тем более вряд ли сможет что-либо написать непосредственно на байт-коде. Байт-код Parrot абсолютно независим от какой бы то ни было платформы. Разумеется, байт-код выполняется гораздо быстрее, чем исходный код скрипта, который не был предварительно скомпилирован.

2.2. Набор инструкций, регистры и типы данных

В набор инструкций виртуальной машины Parrot включены арифметические и логические операторы, конструкции сравнения и управления потоком выполнения, т.е. циклы, конструкция if-then и т.п. Поддерживаются глобальные и локальные переменные, работа с классами и объектами, реализованы механизмы вызова подпрограмм и методов с передачей им параметров, ввода/вывода, многопоточность и многое другое.

Виртуальная машина Parrot является регистровой, т.е. подобно аппаратному процессору, обладает наборами элементов, обеспечивающими сверхбыстрый доступ к хранящимся в них данным. Такие элементы и называют регистрами. Parrot предлагает четыре типа регистров: для целых чисел (I), для чисел с плавающей точкой (N), для строк (S) и для PMC-контейнеров (P), о которых пойдёт речь немного позже. Регистров каждого типа может быть несколько, а конкретное количество должно быть определено для каждой подпрограммы во время компиляции.

Теперь раскроем загадочную аббревиатуру PMC – это PolyMorphic Container, полиморфный контейнер. PMC-контейнер может представлять любой сложный тип или структуру данных: массив, хэш-таблицу, словарь, список и т.д. Для любого PMC-контейнера можно реализовать собственные специализированные арифметические, логические и строковые операции, т.е. смоделировать требуемое поведение объекта. PMC-контейнеры могут быть встроены в Parrot-программы или подгружаться динамически только в тех случаях, когда в этом возникает необходимость.

2.3. Управление памятью

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

3. Использование Parrot на практике (примеры)

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

3.1. Классический тест

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

В файл hello.pir (вы не забыли, что формат PIR занимает самый высокий уровень представления кода?) запишем следующий код:

.sub main
  print "Привет всем!\n"
.end

Инструкция вывода строки практически ничем не отличается от инструкции на Perl или на Python. Это положительный момент, маленький аргумент в пользу Parrot. Но мир наш не идеален, и всё, что в нём существует, увы, несовершенно. На всякий "плюс" тут же находится, по меньшей мере, один "минус". Вот так и с Parrot. После запуска нашего теста вместо ожидаемого вывода мы увидим следующее сообщение:

parrot hello.pir
Malformed string

Это означает, что выводимая строка сформирована некорректно. Обратившись к документации, обнаруживаем, что по умолчанию в Parrot основной является 8-битовая кодировка ASCII, в то время как в подавляющем большинстве современных дистрибутивов принят стандарт де-факто UTF-8.

Проблема решается явным указанием необходимой кодировки в виде префикса перед выводимой строкой. Выглядит это так:

.sub main
  print utf8:unicode:"Привет всем!\n"
.end

Вот теперь наш тест выполняется, как положено. Следует отметить, что Parrot поддерживает наборы символов ascii, iso-8859-1 (Latin 1), unicode (с вариантами fixed_8, ucs2, utf8 и utf16), а кроме того специальный тип binary, который позволяет интерпретировать указанную строку как буфер с неформатированными бинарными данными.

В пылу борьбы с возникшей проблемой мы забыли обсудить структуру и синтаксис самой программы. А впрочем, что тут обсуждать – всё и так очевидно: первая строка определяет подпрограмму с именем main, вторая – инструкция, составляющая тело подпрограммы (вывод строки-константы), третья – инструкция завершения подпрограммы. Ну, ещё PIR-инструкции для определения начала и конца подпрограммы начинаются с символа "точка". Вот и всё описание.

3.2. Используем регистры виртуальной машины

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

.sub main
  $S0 = utf8:unicode:"Привет "
  $S1 = $S0 . utf8:unicode:"всем!\n"
  print $S1
.end

В строковый регистр S0 записывается первая часть строки. В другой строковый регистр S1 записывается результат объединения (конкатенации) содержимого регистра S0 и явно указанной строки-константы. Символ "точка" обозначает операцию конкатенации или объединения строк. Затем содержимое регистра S1 выводится.

Важно отметить, что в формате PIR нельзя выполнять непосредственную запись в регистры. Вместо этого используются ссылки на регистры, обозначаемые префиксом – символом доллара: $S0, $S1. Компилятор, встретив ссылку $S0, ассоциирует её с одним из доступных строковых регистров виртуальной машины и присвоит этому регистру заданное значение.

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

.sub main
  .local string hello
  hello = utf8:unicode:"Привет всем!\n"
  print hello
.end

Во второй строке подпрограммы main директива .local определяет, что данный именованный регистр будет использоваться только внутри текущей подпрограммы (или областью видимости этого именованного регистра является текущая подпрограмма). Далее указан тип именованного регистра string, т.е. это строковый регистр (S). Также могут быть указаны типы int для целочисленных регистров (I), float для регистров чисел с плавающей точкой (N), pmc для регистров полиморфных контейнеров (P), а кроме того можно записать здесь имя PMC-типа.

3.3. Вычисляем сумму квадратов чисел

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

Запишем программу в файл sum_sqr.pir. Строки, начинающиеся с символа '#', являются комментариями:

.sub main
  # Определение количества чисел для вычисления суммы квадратов
  .local int maxnum
  maxnum = 10

  # Используем несколько именованных регистров
  # Регистры одного типа можно объявить в одной строке
  .local int i, sum, tmp
  sum = 0

  # Организация цикла для вычисления общей суммы
  i = 1
loop:
  tmp = i * i
  sum += tmp
  inc i
  if i <= maxnum goto loop

  # Вывод результата вычислений
  print utf8:unicode:"Сумма квадратов первых "
  print maxnum
  print utf8:unicode:" чисел равна "
  print sum
  print "\n"
.end

В этой программе появились новые элементы: арифметические операции с целыми числами, метка loop:, конструкция проверки условия if. Кроме того, мы выяснили, что поддерживается широко распространённая операция присваивания sum += tmp, но инкрементирование значения выполняется командой inc i, а вовсе не i++, как, например, в Perl.

3.4. Особенности формата PIR

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

.local int tmp, i
tmp = i * i

можно записать в более "ассемблеризованном" виде:

.local int tmp, i
mul tmp, i, i

Конструкцию, организующую цикл, также можно изменить. Вместо

.local int i, maxnum
loop:
...
if i <= maxnum goto loop

пишем:

.local int i, maxnum
loop:
...
le i, maxnum, loop

А вот ещё один характерный пример:

.local int sum, tmp
sum += tmp

заменяется на

.local int sum, tmp
add sum, tmp

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

3.5. Рекурсивный вызов подпрограмм – вычисление факториала

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

.sub factorial
  # Принимаемый параметр
  .param int nm
  # Возвращаемое значение
  .local int res

  if nm > 1 goto recurse
  res = 1
  goto return

recurse:
  $I0 = nm - 1
  res = factorial($I0)
  res *= nm

return:
  .return (res)
.end

.sub main :main
  .local int fct, i

  # Вычислим факториалы от 0 до 10
  i = 0
loop:
  fct = factorial(i)
  print utf8:unicode:"Факториал числа "
  print i
  print utf8:unicode:" равен "
  print fct
  print "\n"
  inc i
  if i <= 10 goto loop
.end

Первая строка после заголовка подпрограммы .param int nm определяет, что данная подпрограмма принимает один целочисленный параметр в регистре, ссылка на который обозначена именем nm. Это имя должно использоваться для получения значения параметра до конца текущей подпрограммы.

В конце подпрограммы factorial записана директива .return (res), позволяющая скопировать значение, содержащееся в именованном регистре res, в тот регистр, который вызывающая программа зарезервировала для возвращаемого подпрограммой значения.

Заголовок подпрограммы main записан в виде

.sub main :main

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

3.6. Компиляция в байт-код

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

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

parrot -o fctr_calc.pbc fctr_calc.pir

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

4. Заключение

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


Ресурсы для скачивания


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux
ArticleID=485847
ArticleTitle=Как использовать виртуальную машину Parrot: Часть 1. Основы практического применения
publish-date=04272010