М. Бен-Ари - Языки программирования. Практический сравнительный анализ (2000) (1160781), страница 7
Текст из файла (страница 7)
Загрузчик копирует исполняемый файл с диска в память и инициализирует компьютер перед выполнением программы.
Отладчик — это инструментальное средство, которое дает возможность программисту управлять выполнением программы на уровне отдельных операторов для диагностики ошибок.
Профилировщик измеряет, сколько времени затрачивается на каждый компонент программы. Программист, может затем улучшить эффективность критических компонентов, ответственных за большую часть времени выполнения.
Средства тестирования автоматизируют процесс тестирования программ, создавая и выполняя тесты и анализируя результаты тестирования.
Средства конфигурирования автоматизируют создание программ и прослеживают изменения до уровня исходных файлов.
Интерпретатор непосредственно выполняет исходный код программы в отличие от компилятора, переводящего исходный файл в объектный.
Среду программирования можно составить из отдельных инструментальных средств; кроме того, многие поставщики продают интегрированные среды программирования, которые представляют собой системы, содержащие большую часть или все перечисленные выше инструментальные средства. Преимущество интегрированной среды заключается в чрезвычайной простоте интерфейса пользователя: каждый инструмент инициируется нажатием единственной клавиши или выбором из меню вместо набора на клавиатуре имен файлов и параметров.
3.1. Редактор
У каждого программиста есть свой любимый универсальный редактор. Но даже в этом случае у вас может появиться желание воспользоваться специализированным, предназначенным для определенного Языка редактором, который создает всю языковую конструкцию, типа условного оператора, по одному нажатию клавиши. Преимущество такого редактора в том, что он позволяет предотвратить синтаксические ошибки. Однако любая машинистка, печатающая вслепую, скажет, что легче набирать языковые конструкции, чем отыскивать нужные пункты в меню.
В руководстве по языку могут быть указаны рекомендации для формата исходного кода: введение отступов, разбивка строк, использование верхнего/нижнего регистров. Эти правила не влияют на правильность программы, но ради будущих читателей вашей программы такие соглашения следует .соблюдать. Если вам не удалось выполнить соглашения при написании программы, то вы можете воспользоваться инструментальным средством, называемым красивая печать (pretty-printer), которое переформатирует исходный код к рекомендуемому формату. Поскольку эта программа может непреднамеренно внести ошибки, лучше соблюдать соглашения с самого начала.
3.2. Компилятор
Язык программирования без компилятора (или интерпретатора) может представлять большой теоретический интерес, но выполнить на компьютере программу, написанную на этом языке, невозможно. Связь между языками и компиляторами настолько тесная, что различие между ними расплывается, и часто можно услышать такую бессмыслицу, как:
Язык L1 эффективнее языка L2.
Правильно же то, что компилятор С1 может сгенерировать более эффективный код, чем компилятор С2, или что легче эффективно откомпилировать конструкции L1, чем соответствующие конструкции L2. Одна из целей этой книги — показать соотношение между конструкциями языка и получающимся после компиляции машинным кодом.
Структура компилятора показана на рис. 3.1. Входная часть компилятора
«понимает» программу, анализируя синтаксис и семантику согласно правилам языка. Синтаксический анализатор отвечает за преобразование последовательности символов в абстрактные синтаксические объекты, называемые лексемами. Например, символ «=» в языке С преобразуется в оператор присваивания, если за ним не следует другой «=»; в противном случае оба соседних символа «=» (т.е. «==») преобразуются в операцию проверки равенства. Анализатор семантики отвечает за придание смысла этим абстрактным объектам. Например, в следующей программе семантический анализатор выделит глобальный адрес для первого i и вычислит смещение параметра — для второго i:
с |
void proc(inti) {... }
Результат работы входной части компилятора — абстрактное представление программы, которое называется промежуточным представлением. По нему можно восстановить исходный текст программы, за исключением имен идентификаторов и физического формата строк, пробелов, комментариев и т.д.
Исследования в области компиляторов настолько продвинуты, что входная часть может быть автоматически сгенерирована по грамматике (формальному описанию) языка. Читателям, интересующимся разработкой языков программирования, настоятельно рекомендуется глубоко изучить компиляцию и разрабатывать языки так, чтобы их было легко компилировать с помощью автоматизированных методов.
Выходная часть компилятора берет промежуточное представление программы и генерирует машинный код для конкретного компьютера. Таким образом, входная часть является языковозависимой, в то время как выходная — машиннозависимой. Поставщик компиляторов может получить семейство компиляторов некоторого языка L для ряда самых разных компьютеров Cl, C2,..., написав несколько выходных частей, использующих промежуточное представление общей входной части. Точно так же поставщик компьютеров может создать высококачественную выходную часть для компьютера С и затем поддерживать большое число языков LI, L2,..., написав входные части, которые компилируют исходный текст каждого языка в общее промежуточное представление. В этом случае фактически не имеет смысла спрашивать, какой язык на компьютере эффективнее.
С генератором объектного кода связан оптимизатор, который пытается улучшать код, чтобы сделать его более эффективным. Возможны несколько способов оптимизации:
• Оптимизация промежуточного представления, например нахождение общего подвыражения:
a = f1 (x + y) + f2(x + y);
Вместо того чтобы вычислять выражение х + у дважды, его можно вычислить один раз и сохранить во временной переменной или регистре. Подобная оптимизация не зависит от конкретного компьютера и может быть сделана до генерации кода. Это означает, что даже компоненты выходной части могут быть общими в компиляторах разных компьютеров.
• Машинно-ориентированная оптимизация. Такая оптимизация, как сохранение промежуточных результатов в регистрах, а не в памяти, явно должна выполняться при генерации объектного кода, потому что число и тип регистров в разных компьютерах различны.
• Локальная оптимизация обычно выполняется для сгенерированных команд, хотя иногда ее можно проводить для промежуточного представления. В этой методике делается попытка заменять короткие последовательности команд одной, более эффективной командой. Например, в языке С выражение n++ может быть скомпилировано в следующую последовательность:
load R1,n
add R1,#1
store R1,n
но локальный оптимизатор для конкретного компьютера мог бы заменить эти три команды одной, которая увеличивает на единицу непосредственно слово в памяти:
incr n
Использование оптимизаторов требует осторожности. Поскольку оптимизатор по определению изменяет программу, ее, возможно, будет трудно отлаживать с помощью отладчика исходного кода, так как порядок выполнения команд может отличаться от их порядка в исходном коде. Обычно оптимизатор при отладке лучше отключать. Кроме того, из-за сложности оптимизатора вероятность содержания в нем ошибки больше, чем в любом другом компоненте компилятора. Ошибку оптимизатора трудно обнаружить, потому что отладчик создан для работы с исходным текстом, а не с оптимизированным (то есть измененным) объектным кодом. Ни в коем случае нельзя сначала тестировать программу без оптимизатора, а после оптимизации отдавать в работу без тестирования. Наконец, оптимизатор в какой-либо ситуации может сделать неправильные предположения. Например, для устройства ввода-вывода с регистрами, «отображенными» на память, значение переменной может присваиваться дважды без промежуточного чтения:
с |
transmit_register = 0x70; /* Ждать 1 секунду */ transmit_register = 0x70;
Оптимизатор предположит, что второе присваивание лишнее и удалит его из сгенерированного объектного кода.
3.3. Библиотекарь
Можно хранить объектные модули либо в отдельных файлах, либо в одном файле, называемом библиотекой. Библиотеки могут поставляться с компилятором, либо приобретаться отдельно, либо составляться программистом.
Многие конструкции языка программирования реализуются не с помощью откомпилированного кода, выполняемого внутри программы, а через обращения к процедурам, которые хранятся в библиотеке, предусмотренной поставщиком компилятора. Из-за увеличения объема языков программирования наблюдается тенденция к размещению большего числа функциональных возможностей в «стандартных» библиотеках, которые являются неотъемлемой частью языка. Так как библиотека — это всего лишь структурированная совокупность типов и подпрограмм, не содержащая новых языковых конструкций, то она упрощает задачи как для студента, который должен изучить язык, так и для разработчика компилятора.
Основной набор процедур, необходимых для инициализации, управления памятью, вычисления выражений и т.п., называется системой времени исполнения (run-time system) или исполняющей системой. Важно, чтобы программист был знаком с исполняющей системой используемого компилятора: невинные на первый взгляд конструкции языка могут фактически приводить к вызовам времяемких процедур в исполняющей системе. Например, если высокоточная арифметика реализована библиотечными процедурами, то замена всех целых чисел на длинные (двойные) целые значительно увеличит время выполнения.
3.4. Компоновщик
Вполне возможно написать программу длиной в несколько тысяч строк в виде отдельного файла или модуля. Однако для больших программных систем, особенно разрабатываемых группами программистов, требуется, чтобы программное обеспечение было разложено на модули (гл. 13). Если обращение делается к процедуре, находящейся вне текущего модуля, компилятор никак не может узнать адрес этой процедуры. Вместо этого адреса в объектном модуле записывается внешняя ссылка. Если язык разрешает разным модулям обращаться к глобальным переменным, то внешние ссылки должны быть созданы для каждого такого обращения. Когда все модули откомпилированы, компоновщик разрешает эти ссылки, разыскивая описания процедур и переменных, которые экспортированы из модуля для нелокального использования.
В современной практике программирования модули активно используются для декомпозиции программы. Дополнительный эффект такой практики — то, что компиляции бывают обычно короткими и быстрыми, зато компоновщик должен связать сотни модулей с тысячами внешних ссылок. Эффективность компоновщика может оказаться критичной для производительности группы разработчиков программного обеспечения на заключительных стадиях разработки: даже незначительное изменение одного исходного модуля потребует продолжительной компоновки.
Одно из решений этой проблемы состоит в том, чтобы компоновать из модулей подсистемы и только затем разрешать связи между подсистемами. Другое решение состоит в использовании динамической компоновки, если она поддерживается системой. При динамической компоновке внешние ссылки не разрешаются; вместо этого операционной системой улавливается и разрешается первое обращение к процедуре. Динамическая компоновка может комбинироваться с динамической загрузкой, при этом не только ссылки не разрешаются, но даже модуль не загружается, пока не понадобится одна из экспортируемых им процедур. Конечно, динамическая компоновка или загрузка приводит к дополнительным издержкам во время выполнения, но это мощный метод адаптации систем к изменяющимся требованиям без перекомпоновки.
3.5. Загрузчик
Как подразумевает название, загрузчик загружает программу в память и инициализирует ее выполнение. На старых компьютерах загрузчик был не-тривиален, так как должен был решать проблему перемещаемости программ. Такая команда, как load 140, содержала абсолютный адрес памяти, и его приходилось настраивать в зависимости от конкретных адресов, в которые загружалась программа. В современных компьютерах адреса команд и данных задаются относительно значений в регистрах. Для каждой области памяти с программой или данными выделяется регистр, указывающий на начало этой области, поэтому все, что должен сделать загрузчик теперь, — это скопировать программу в память и инициализировать несколько регистров. Команда load 140 теперь означает «загрузить значение, находящееся по адресу, полученному прибавлением 140 к содержимому регистра, который указывает на область данных».
3.6. Отладчик