Для того, чтобы текст программы превратился в исполняемый модуль, необходимо выполнить несколько операций. Эти операции сгруппированы в процессе компиляции. В ходе этого процесса, строится абстрактное синтаксическое дерево (как для интерпретатора Basic, стр. ??), затем оно превращается в последовательность инструкций для реального процессора или для виртуальной машины. В последнем случае, для того чтобы выполнить программу, необходим интерпретатор инструкций виртуальной машины. В каждом случае, результат компиляции должен быть связан с библиотекой времени выполнения, входящей в дистрибутив. Она может меняться в зависимости от процессора и операционной системы. В нее входят примитивные функции (такие как операции над числами, системный интерфейс) и администратор памяти.
В Objective CAML существует два компилятора. Первый из них — это компилятор для виртуальной машины, в результате которого мы получаем байт–код. Второй компилятор создает программу, состоящую из машинного кода (native code), выполняемого реальным процессором, как Intel, Motorola, SPARC, PA–RISC, Power–PC или Alpha. Компилятор байт–кода отдает предпочтение переносимости кода, тогда как компилятор в машинный код увеличивает скорость выполнения. Интерактивный интерпретатор, который мы видели в первой части, использует байт–код. Каждая введенная фраза компилируется и затем выполняется в окружении символов, определенных в течении интерактивной сессии.
В этой главе представлены различные способы компиляции программ на Objective CAML и проводится сравнение по переносимости и эффективности этих способов. В первом разделе мы обсудим различные этапы компиляторов Objective CAML. Во втором — различные способы компиляции и синтаксис, необходимый для получения исполняемых файлов. В третьем разделе, мы увидим как создать автономный исполняемый файл, который может быть выполнен независимо от установленного дистрибутива Objective CAML. И, наконец, четвертый раздел сравнивает различные способы компиляции в ракурсе переносимости и эффективности выполнения.
Исполняемый файл получается в результате этапов трансляции и компоновки, изображенных в таблице 6.1.
Макроподстановка заключается в подстановке одних частей текста вместо других при помощи системы макросов. При компиляции исходный код программы транслируется в команды ассемблера. После компоновки мы получим файл, состоящий из машинных инструкций. И, наконец, при редактировании связей добавляется библиотека, в основном состоящая из администратора памяти, а так же связь с основными объектами операционной системы (такие как файлы, каталоги, процессы и т.д.)
Детали различных этапов генерации кода компиляторов Objective CAML представлены таблице 6.2. Внутреннее представление кода программы, сгенерированного компилятором, называется промежуточным языком (ПЯ).
Последовательность символов лексический анализ Последовательность лексем синтаксический анализ Синтаксическое дерево семантический анализ Маркированное синтаксическое дерево генерация промежуточного кода Последовательность кода в ПЯ оптимизация промежуточного кода Последовательность кода в ПЯ генерация псевдокода Программа на ассемблере
Table 6.2: Этапы компиляции
При помощи лексического анализа из последовательности символов, мы получаем последовательность лексических элементов (лексем). В основном лексемы соответствуют целым числам, числам с плавающей запятой, строкам и идентификаторам. Сообщение Illegal character генерируется на этом этапе анализа.
Синтаксический анализ создает синтаксическое дерево, проверяя при этом последовательность лексем с точки зрения правил грамматики языка. Сообщение Syntax error указывает на то, что анализируемая часть кода не соответствует правилам грамматики.
При семантическом анализе просматривается синтаксическое дерево, здесь нас интересует другой аспект корректности программы. На этом этапе, в Objective CAML происходит вывод типа и, если он прошел удачно, то выводимый тип является самым общим типом для выражения или объявления. Сообщения об ошибке типа генерируются на данном этапе. В этот же момент выявляются случаи, в которых тип члена последовательности отличен от unit. Могут возникать и другие предупреждения, возникающие на пример во время анализа сопоставления с образцом ("неисчерпывающее сопоставление с образцом", "неиспользуемая ветвь сопоставления с образцом").
При генерации и оптимизации промежуточного кода не выводится никаких сообщений об ошибках или предупреждений. Эти этапы манипуляции промежуточными структурами позволяют факторизовать разработку различных компиляторов Objective CAML.
Генерация исполняемого модуля — это финальный этап компиляции, который зависит от компилятора.
Виртуальная машина Objective CAML называется Zinc (от << Zinc Is Not Caml >>). Она была создана Гзавье Леруа (Xavier Leroy) и описана в ([Ler90]). Это имя было выбрано, для того чтобы подчеркнуть разницу между первыми реализациями языка Caml, основанного на виртуальной машине CAM (от Categorical Abstract Machine, см. [CCM87]).
На рисунке 6.1 изображена виртуальная машина Zinc. В первой части представлен интерпретатор связанный с библиотекой. Вторая часть соответствует компилятору, который генерирует байт–код для машины Zinc. Третья часть содержит библиотеки, идущие вместе с компилятором. Они более подробно описаны в главе 7.
Графические символы, используемые на рисунке 6.1, являются стандартными для компиляции. Простой блок символизирует файл, написанный на языке, который указан внутри блока. Двойной блок представляет интерпретацию одного языка программой, написанной на другом языке. Тройной блок — исходный язык компилируется в машинный при помощи компилятора, написанном на третьем языке. Символы, соответствующие интерпретаторам и компиляторам, изображены на рисунке 6.2.
Пояснение к рисунку 6.1:
Замечание
Основная часть компилятора Objective CAML написана на языке Objective
CAML. Переход с версии v1 на версию v2 изображен на второй части
рисунка 6.1.
Дистрибутив языка зависит от типа процессора и операционной системы. В дистрибутив Objective CAML для каждой архитектуры (пара: процессор, операционная система) входит интерактивная среда интерпретатора, байт–код компилятор, и, в большинстве случаев, компилятор машинного кода для данной архитектуры.
В таблице 6.3 приводятся названия различных компиляторов, входящих в дистрибутив Objective CAML. Первые четыре из них являются частью каждого дистрибутива.
ocaml интерактивная среда интерпретатора ocamlrun интерпретатор байт–кода ocamlc компилятор в байт–кода ocamlopt компилятор машинного кода ocamlc.opt оптимизированный компилятор байт–кода ocamlopt.opt оптимизированный компилятор машинного кода ocamlmktop конструктор новых сред интерпретатора
Table 6.3: Команды компиляции
Оптимизированные компиляторы были сами скомпилированы компилятором машинного кода, при этом получается более быстрый исполняемый файл.
Элемент компиляции соответствует наименьшей части программы на Objective CAML, которая может быть скомпилирована. Для интерактивного интерпретатора таким элементом является фраза на языке Objective CAML, тогда как для пакетных компиляторов таким элементом является пара файлов: исходный код и файл интерфейсов, где файл интерфейсов не обязателен. Если он отсутствует, то все глобальные объявления файла–исходника будут видны другим элементам компиляции. Создание интерфейсных файлов будет рассмотрено в главе 13 посвященной модульному программированию. Эти файлы отличаются друг от друга расширением имени файла.
В таблице 6.4 представлены расширения файлов используемые для программ Objective CAML и C.
расширение значение .ml исходник .mli интерфейсный файл .cmo объектный файл (байт–код) .cma объектный файл библиотеки .cmi скомпилированный интерфейсный файл .cmx объектный файл (нативный) .cmxa объектный файл библиотеки (нативный) .c исходник на C .o объектный файл C (нативный) .a объектный файл библиотеки C (нативный)
Table 6.4: Расширения файлов
Файлы example.ml и example.mli формируют элемент компиляции. Скомпилированный интерфейсный файл (example.cmi) может быть использован нативным и байт–код компилятором. Файлы на C используются для интерфейса между Objective CAML и библиотеками, написанными на языке C ( 11).
Общая форма команды компиляции следующая:
command options file_name
Objective CAML подчиняется тому же правилу.
Перед параметрами компилятора ставится символ `–', как это принято в системе Unix. Расширения файлов интерпретируются в соответствии с таблицей 6.4. В приведенном примере, файл exemple.ml рассматривается как исходник на Objective CAML и после его компиляции будет сгенерированно два файла exemple.cmo и exemple.cmi. Опция `–c' указывает компилятору, что необходимо скомпилировать только объектный файл. Без этой опции компилятор ocamlc создаст исполняемый файл a.out, то есть в данном случае он реализует редактирование связей.
В таблице 6.5 описаны основные опции байт–код компилятора. Остальные опции приведены в таблице 6.6.
-a создать библиотеку -c компиляция, без редактирования связей -o имя_файла указывает имя исполняемого файла -linkall связать со всеми используемыми библиотеками -i вывести все скомпилированные глобальные объявления -pp команда использовать команду как препроцессор -unsafe отключить проверку индексов -v вывести версию компилятора -w список выбрать, в соответствии со списком, уровень предупреждений -impl файл указывает, что файл это исходник на Caml (см. таб. 6.7) -intf файл указывает, что файл есть интерфейсный файл на Caml (.mli) -I каталог добавить каталог в список каталогов
Table 6.5: Основные опции байт–код компилятора
Чтобы получить возможные опции байт–код компилятора, используйте параметр –help.
В таблице 6.7 описаны различные уровни предупреждений. Уровень — это переключатель (включено/выключено), который символизируется буквой. Заглавная буква включает данный уровень, а прописная выключает.
Principal levels A/a включить/выключить все сообщения F/f частичное применение в последовательности P/p для неполного сопоставления U/u для лишних случаев в сопоставлении X/x включить/выключить все остальные сообщения M/m и V/v for hidden object (см. главу 14)
Table 6.7: Описание предупреждений компилятора
По умолчанию установлен максимальный уровень предупреждений (A).
Пример использования байт–кода компилятора изображен на рисунке 6.3.
Принцип действия компилятора машинного кода такой же что и байт–код компилятора, с разницей в сгенерированных файлах. Большинство опций компиляции те же что проведены в таблицах 6.5 и 6.6. Однако, в таблице 6.6 необходимо удалить опции связанные с runtime. Специфические опции для нативного компилятора приведены в таблице 6.8. Уровни предупреждений остаются теми же.
Встраивание является улучшенным методом макроподстановки на стадии препроцессирования. Оно заменяет вызов функции, подставляя тело функции, в случае когда аргументы определены. Таким образом несколько вызовов функции соответствуют новым копиям ее тела. Разложением мы избавляемся от затрат, возникающих при инициализации вызова и возврата функции. Однако, расплатой за это будет увеличение размера кода. Основные уровни разложения функции следующие:
У интерактивного интерпретатора всего две опции:
Интерактивный интерпретатор распознает несколько директив, позволяющих интерактивно изменить его функционирование. Все эти директивы описаны в таблице 6.9, они начинается с символа `#' и заканчиваются привычным `;;'.
#quit;; выйти из интерпретатора #directory каталог ;; добавить каталог в список путей #cd каталог ;; сменить текущий каталог #load объектный_файл ;; загрузить файл .cmo #use исходный_файл ;; скомпилировать и загрузить исходных файл #print_depth глубина ;; изменить глубину вывода на экран #print_length ширина ;; аналогично для ширины #install_printer функция ;; указать функцию вывода на экран #remove_printer функция ;; переключится на стандартный вывод на экран #trace функция ;; трассировка аргументов функции #untrace функция ;; отменить трассировку #untrace_all ;; отменить все трассировки
Table 6.9: Директивы интерпретатора.
Директивы, относящиеся к каталогам, подчиняются правилам используемой операционной системы.
Директивы загрузки имеют отличное от других директив функционирование. Директива #use считывает указанный файл, как если бы он был введен интерактивно. Директива #load загружает файл с расширением .cmo. В этом последнем случае, глобальные декларации этого файла недоступны напрямую. Для этого необходимо использовать синтаксис с точкой. Если в файле exemple.ml содержится глобальное объявление f, то после загрузки байт–код (#load "example.cmo";;), значение f доступно как Example.f. Обратите внимание, что первая буква файла — заглавная. Этот синтаксис происходит из модульной системы Objective CAML (см. главу 13, стр. ??). Директивы настройки глубины и ширины вывода на экран позволяют контролировать формат выводимых значений, что удобно, когда мы имеем дело с большими данными.
Директивы трассировки аргументов и результата функции особенно полезны при отладке программ. Этот вариант использования интерактивного интерпретатора рассматривается более подробно в главе об анализе программ ( 9).
На рисунке 6.4 изображен сеанс работы в интерпретаторе.
При помощи команды ocamlmktop мы можем создать новый цикл интерпретатора, в который загружены определенные библиотеки или модули. Кроме того, что мы избавляемся от #load в начале сеанса работы, это так же необходимо для подключения библиотек на C.
Опции этой команды являются частью опций байт–код компилятора (ocamlc):
-cclib имя_библиотеки, -ccopt опции, -custom, -I каталог -o имя_файла
В главе о графическом интерфейсе (4 стр. ??) используется эта команда для создания цикла содержащего библиотеку Graphics следующим способом:
Эта команда создает исполняемый файл mytoplevel, содержащий байт–код библиотеки graphics.cma. Этот файл является автономным (-custom, смотрите следующий раздел) и он связан с библиотекой X11 (libX11.a), которая располагается в каталоге /usr/X11/lib.
Автономный исполняемый файл (standalone) есть исполняемый файл, независящий от дистрибутива Objective CAML на используемой машине. С одной стороны, бинарный файл упрощает распространение программ, с другой стороны, он не зависит от расположения дистрибутива Objective CAML на компьютере.
Компилятор машинного кода всегда генерирует автономный исполняемый файл. Тогда как без опции -custom, байт–код компилятор создаст исполняемый файл, который нуждается в интерпретаторе ocamlrun. Пусть файл example.ml содержит следующее:
Следующая команда создает файл example.exe:
Полученный файл, размером около 8Kb, может быть выполнен при помощи следующей команды:
Интерпретатор выполняет команды виртуальной машины Zinc, хранящиеся в файле example.exe
Для операционной системы Unix, в первой строке такого файла находится путь к байт–код интерпретатору, например:
При помощи этой строки, файл может быть напрямую запущен интерпретатором. Таким образом, выполнение файла example.exe запускает файл, путь к которому находится в первой строке. Если интерпретатор не найдет по этому пути программу–интерпретатор, сообщение Command not found будет выведено на экран и выполнение остановится.
Та же компиляция с опцией -custom создаст автономный файл с именем exauto.exe:
Теперь размер файла около 85Kb, так как он содержит не только байт–код, но и интерпретатор. Этот файл может быть самостоятельно запущен или скомпилирован на другую машину (той же архитектуры и той же операционной системы) для запуска.
Warning
Размер исполняемого файла не соответствует размеру процесса во время
выполнения. Размер статического файла на диске не учитывает размер динамически
выделяемой памяти ( 8).
Интерес компиляции для абстрактной машины заключается в том что, мы получим код который может быть запущен на какой угодно реальной архитектуре. Основным неудобством является интерпретация виртуальных инструкций. Нативный компилятор создает более эффективный код, однако он предназначен лишь для одной определенной архитектуры. Поэтому, желательно чтобы у нас был выбор типа компиляции в зависимости от разрабатываемого программного обеспечения. Автономия исполняемого файла, то есть его независимость от установленного дистрибутива Objective CAML, лишает приложение переносимости.
Для того чтобы получить автономный исполняемый файл, компилятор отредактировал связи байт–код файла t.cmo с runtime библиотекой, интерпретатором байт–кода и некоторым кодом на C. Предполагается, что в системе установлен компилятор C. Вставка машинного кода означает, что файл не может быть выполнен на другой архитектуре или системе.
Дело обстоит по другому в случае байт–код файла. Так как виртуальные инструкции одни и те же для всех архитектур, лишь присутствие интерпретатора имеет значение. Такой файл может быть запущен на любой системе, где присутствует команда ocamlrun, являющаяся частью дистрибутива Objective CAML для Sparc и Solaris, Intel и Windows и т.д. Всегда желательно использовать одни и те же версии компилятора и интерпретатора.
Переносимость файлов в байт–код формате позволяет напрямую распространять и сразу же использовать библиотеки в подобной форме, на машине с интерпретатором Objective CAML.
Компилятор байт–кода генерирует инструкции машины Zinc, они будут интерпретированны ocamlrun. Эта фаза интерпретации кода негативно влияет на скорость выполнения. Мы можем представить себе интерпретатор Zinc в действии как большое сопоставление с образцом (сопоставление match ... with), где каждая инструкция есть образец сопоставления, а ответвления расчета изменяют состояние стека и счетчика (адрес следующей инструкции). Несмотря на то, что данное сопоставление оптимизировано, оно все же снижает эффективность выполнения.
Следующий пример, хоть он и не тестирует все составные части языка, иллюстрирует разницу во времени выполнения между байт–код и автономным вариантом расчета чисел Fibonnacci. Пусть у нас есть следующая программа fib.ml:
и главная программа main.ml:
Откомпилируем их следующим образом:
Мы получим два файла fib.exe и fibopt.exe. При помощи команды time системы Unix на процессоре Pentium 350 с установленной операционной системой Linux, получим следующие значения:
То есть для одной и той же программы скорость ее двух версий отличается в 7 раз. Данная программа не проверяет все свойства языка, полученая выгода значительно зависит от типа приложения.
В этой главр мы увидели различные этапы компиляции Objective CAML. Byte–code компилятор создает переносимый двоичный код и таким образом позволяет распространять бибиотеки в подобном формате. Автономные исполняемы файлы теряют эту особеность. Нативный компилятор отдает предпочтение эффективности полученного кода в ущерб переносимости.