Previous Up Next

Chapter 6  Компиляция и переносимость

Введение

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

В Objective CAML существует два компилятора. Первый из них — это компилятор для виртуальной машины, в результате которого мы получаем байт–код. Второй компилятор создает программу, состоящую из машинного кода (native code), выполняемого реальным процессором, как Intel, Motorola, SPARC, PA–RISC, Power–PC или Alpha. Компилятор байт–кода отдает предпочтение переносимости кода, тогда как компилятор в машинный код увеличивает скорость выполнения. Интерактивный интерпретатор, который мы видели в первой части, использует байт–код. Каждая введенная фраза компилируется и затем выполняется в окружении символов, определенных в течении интерактивной сессии.

План главы

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

6.1  Этапы компиляции

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


 Исходный текст программы
макроподстановка 
 Исходный текст программы
компиляция 
 Программа на ассемблере
компоновка 
 Машинные инструкции
редактирование связей 
 Исполняемый код
Table 6.1: Порядок создания исполняемого файла

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

6.1.1  Компиляторы Objective CAML

Детали различных этапов генерации кода компиляторов Objective CAML представлены таблице 6.2. Внутреннее представление кода программы, сгенерированного компилятором, называется промежуточным языком (ПЯ).


 Последовательность символов
лексический анализ 
 Последовательность лексем
синтаксический анализ 
 Синтаксическое дерево
семантический анализ 
 Маркированное синтаксическое дерево
генерация промежуточного кода 
 Последовательность кода в ПЯ
оптимизация промежуточного кода 
 Последовательность кода в ПЯ
генерация псевдокода 
 Программа на ассемблере
Table 6.2: Этапы компиляции

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

Синтаксический анализ создает синтаксическое дерево, проверяя при этом последовательность лексем с точки зрения правил грамматики языка. Сообщение Syntax error указывает на то, что анализируемая часть кода не соответствует правилам грамматики.

При семантическом анализе просматривается синтаксическое дерево, здесь нас интересует другой аспект корректности программы. На этом этапе, в Objective CAML происходит вывод типа и, если он прошел удачно, то выводимый тип является самым общим типом для выражения или объявления. Сообщения об ошибке типа генерируются на данном этапе. В этот же момент выявляются случаи, в которых тип члена последовательности отличен от unit. Могут возникать и другие предупреждения, возникающие на пример во время анализа сопоставления с образцом ("неисчерпывающее сопоставление с образцом", "неиспользуемая ветвь сопоставления с образцом").

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

Генерация исполняемого модуля  — это финальный этап компиляции, который зависит от компилятора.

6.1.2  Описание байт–код компилятора

Виртуальная машина Objective CAML называется Zinc (от << Zinc Is Not Caml >>). Она была создана Гзавье Леруа (Xavier Leroy) и описана в ([Ler90]). Это имя было выбрано, для того чтобы подчеркнуть разницу между первыми реализациями языка Caml, основанного на виртуальной машине CAM (от Categorical Abstract Machine, см. [CCM87]).

На рисунке 6.1 изображена виртуальная машина Zinc. В первой части представлен интерпретатор связанный с библиотекой. Вторая часть соответствует компилятору, который генерирует байт–код для машины Zinc. Третья часть содержит библиотеки, идущие вместе с компилятором. Они более подробно описаны в главе 7.


Figure 6.1: Виртуальная машина Zinc

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


Figure 6.2: Символы интерпретаторов и компиляторов.

Пояснение к рисунку 6.1:


Замечание
Основная часть компилятора Objective CAML написана на языке Objective CAML. Переход с версии v1 на версию v2 изображен на второй части рисунка 6.1.

6.2  Типы компиляции

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

6.2.1  Названия команд

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


ocamlинтерактивная среда интерпретатора
ocamlrunинтерпретатор байт–кода
ocamlcкомпилятор в байт–кода
ocamloptкомпилятор машинного кода
ocamlc.optоптимизированный компилятор байт–кода
ocamlopt.optоптимизированный компилятор машинного кода
ocamlmktopконструктор новых сред интерпретатора
Table 6.3: Команды компиляции

Оптимизированные компиляторы были сами скомпилированы компилятором машинного кода, при этом получается более быстрый исполняемый файл.

6.2.2  Элементы компиляции

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

6.2.3  Расширения файлов Objective CAML

В таблице 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).

6.2.4  Байт–код компилятор

Общая форма команды компиляции следующая:

command options file_name

Objective CAML подчиняется тому же правилу.

ocamlc -c example.ml

Перед параметрами компилятора ставится символ `', как это принято в системе 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: Основные опции байт–код компилятора


нити-thread (18, стр. ??)
отладка-g, -noassert (9, стр. ??)
автономный исполняемый файл-custom, -cclib, -ccopt, -cc (см. стр. 6.3)
runtime-make-runtime , -use-runtime
C interface-output-obj (11, стр. ??)
Table 6.6: Другие опции байт–код компилятора

Чтобы получить возможные опции байт–код компилятора, используйте параметр –help.

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


Principal levels 
A/aвключить/выключить все сообщения
F/fчастичное применение в последовательности
P/pдля неполного сопоставления
U/uдля лишних случаев в сопоставлении
X/xвключить/выключить все остальные сообщения
M/m и V/vfor hidden object (см. главу 14)
Table 6.7: Описание предупреждений компилятора

По умолчанию установлен максимальный уровень предупреждений (A).

Пример использования байт–кода компилятора изображен на рисунке 6.3.


Figure 6.3: Пример работы с байт–код компилятором

6.2.5  Нативный компилятор

Принцип действия компилятора машинного кода такой же что и байт–код компилятора, с разницей в сгенерированных файлах. Большинство опций компиляции те же что проведены в таблицах 6.5 и 6.6. Однако, в таблице 6.6 необходимо удалить опции связанные с runtime. Специфические опции для нативного компилятора приведены в таблице 6.8. Уровни предупреждений остаются теми же.


-compactоптимизация размера кода
-Sсохранить код на ассемблере в файле
-inline уровеньустановить уровень “агрессивности” разложения функций
Table 6.8: Специальные опции для нативного компилятора

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

6.2.6  Интерактивный интерпретатор

У интерактивного интерпретатора всего две опции:

Интерактивный интерпретатор распознает несколько директив, позволяющих интерактивно изменить его функционирование. Все эти директивы описаны в таблице 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 изображен сеанс работы в интерпретаторе.


Figure 6.4: Сеанс работы в интерпретаторе.

6.2.7  Создание интерпретатора

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

Опции этой команды являются частью опций байт–код компилятора (ocamlc):

-cclib имя_библиотеки, -ccopt опции, -custom, -I каталог -o имя_файла

В главе о графическом интерфейсе (4 стр. ??) используется эта команда для создания цикла содержащего библиотеку Graphics следующим способом:

ocamlmktop -custom -o mytoplevel graphics.cma -cclib \ -I/usr/X11/lib -cclib -lX11

Эта команда создает исполняемый файл mytoplevel, содержащий байт–код библиотеки graphics.cma. Этот файл является автономным (-custom, смотрите следующий раздел) и он связан с библиотекой X11 (libX11.a), которая располагается в каталоге /usr/X11/lib.

6.3  Автономный исполняемый файл

Автономный исполняемый файл (standalone) есть исполняемый файл, независящий от дистрибутива Objective CAML на используемой машине. С одной стороны, бинарный файл упрощает распространение программ, с другой стороны, он не зависит от расположения дистрибутива Objective CAML на компьютере.

Компилятор машинного кода всегда генерирует автономный исполняемый файл. Тогда как без опции -custom, байт–код компилятор создаст исполняемый файл, который нуждается в интерпретаторе ocamlrun. Пусть файл example.ml содержит следующее:

let f x = x + 1;; print_int (f 18);; print_newline();;

Следующая команда создает файл example.exe:

ocamlc -o example.exe example.ml

Полученный файл, размером около 8Kb, может быть выполнен при помощи следующей команды:

\$ ocamlrun example.exe 19

Интерпретатор выполняет команды виртуальной машины Zinc, хранящиеся в файле example.exe

Для операционной системы Unix, в первой строке такого файла находится путь к байт–код интерпретатору, например:

!/usr/local/bin/ocamlrun

При помощи этой строки, файл может быть напрямую запущен интерпретатором. Таким образом, выполнение файла example.exe запускает файл, путь к которому находится в первой строке. Если интерпретатор не найдет по этому пути программу–интерпретатор, сообщение Command not found будет выведено на экран и выполнение остановится.

Та же компиляция с опцией -custom создаст автономный файл с именем exauto.exe:

ocamlc -custom -o exauto.exe example.ml

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


Warning
Размер исполняемого файла не соответствует размеру процесса во время выполнения. Размер статического файла на диске не учитывает размер динамически выделяемой памяти ( 8).

6.4  Переносимость и эффективность

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

6.4.1  Автономность и переносимость

Для того чтобы получить автономный исполняемый файл, компилятор отредактировал связи байт–код файла t.cmo с runtime библиотекой, интерпретатором байт–кода и некоторым кодом на C. Предполагается, что в системе установлен компилятор C. Вставка машинного кода означает, что файл не может быть выполнен на другой архитектуре или системе.

Дело обстоит по другому в случае байт–код файла. Так как виртуальные инструкции одни и те же для всех архитектур, лишь присутствие интерпретатора имеет значение. Такой файл может быть запущен на любой системе, где присутствует команда ocamlrun, являющаяся частью дистрибутива Objective CAML для Sparc и Solaris, Intel и Windows и т.д. Всегда желательно использовать одни и те же версии компилятора и интерпретатора.

Переносимость файлов в байт–код формате позволяет напрямую распространять и сразу же использовать библиотеки в подобной форме, на машине с интерпретатором Objective CAML.

6.4.2  Эффективность выполнения

Компилятор байт–кода генерирует инструкции машины Zinc, они будут интерпретированны ocamlrun. Эта фаза интерпретации кода негативно влияет на скорость выполнения. Мы можем представить себе интерпретатор Zinc в действии как большое сопоставление с образцом (сопоставление match ... with), где каждая инструкция есть образец сопоставления, а ответвления расчета изменяют состояние стека и счетчика (адрес следующей инструкции). Несмотря на то, что данное сопоставление оптимизировано, оно все же снижает эффективность выполнения.

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

let rec fib n = if n < 2 then 1 else (fib (n-1)) + (fib(n-2));;

и главная программа main.ml:

for i = 1 to 10 do print_int (Fib.fib 30); print_newline() done;;

Откомпилируем их следующим образом:

$ ocamlc -o fib.exe fib.ml main.ml $ ocamlopt -o fibopt.exe fib.ml main.ml

Мы получим два файла fib.exe и fibopt.exe. При помощи команды time системы Unix на процессоре Pentium 350 с установленной операционной системой Linux, получим следующие значения:


fib.exe (байт–код)fibopt.exe (машинный)
7 сек.1 сек.

То есть для одной и той же программы скорость ее двух версий отличается в 7 раз. Данная программа не проверяет все свойства языка, полученая выгода значительно зависит от типа приложения.

Резюме

В этой главр мы увидели различные этапы компиляции Objective CAML. Byte–code компилятор создает переносимый двоичный код и таким образом позволяет распространять бибиотеки в подобном формате. Автономные исполняемы файлы теряют эту особеность. Нативный компилятор отдает предпочтение эффективности полученного кода в ущерб переносимости.


Previous Up Next