Использование оптимизирующего компилятора

 

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

 

Windows

UNIX

Название

Команда

Название

Команда

Microsoft C/C++ Optimizing Compiler в составе Visual Studio

cl.exe

GNU C/C++ Compiler

gcc

Intel C/C++ Compiler

icl.exe

Intel C/C++ Compiler

icc

Borland C++ в составе Borland C++ Builder

bcc32.exe

Compaq C Compiler

ccc

 

Исполняемый код, который компилятор должен сгенерировать, зависит от архитектуры процессора. Некоторые компиляторы могут создавать код для большого числа различных архитектур (GNU C/C++ Compiler), некоторые предназначены только для конкретной архитектуры (Intel C/C++ Compiler для архитектур x86 и IA-64, Compaq C Compiler для архитектуры Alpha).

Как правило, компилятор - это консольное приложение с простейшим интерфейсом. Для задания параметров работы компиляторы имеют набор специальных опций (ключей), которые указываются в командной строке. Вот, например, некоторые ключи компилятора gcc:

 

Ключ

Описание

-o filename

Создать выходной файл с именем filename

-S

Сгенерировать ассемблерный код программы

-O0

Не использовать оптимизацию

-O

Использовать оптимизацию (один из ее вариантов)

 

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

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

 

Компилятор

Некоторые ключи оптимизации

gcc

-O0, -O1 (или -O), -O2, -O3

icc

-O0, -O1, -O2 (или -O), -O3, -fast

ccc

-O0, -O1, -O2 (или -O), -O3, -O4, -fast

 

Настройка на архитектуру процессора

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

 

Генерация кода для конкретного процессора

Компилятор

Ключ компиляции

Возможные архитектуры (target_arch)

gcc

-march=target_arch

i386, i486, i586, i686, pentium, pentium-mmx, pentiumpro, pentium2, pentium3, pentium4, :

iсс

-march= target_arch

pentiumpro, pentiumii, pentiumiii, pentium4

ссс

-arch target_arch

generic, host, ev4,ev5, ev56, ev6, ev67, pca56

 

Пример команды компиляции программы prog.c компилятором gcc с использованием 3-го уровня оптимизации и оптимизацией под процессор Pentium III:

     gcc -o prog -O3 -march=pentium3 prog.c

Результат компиляции - исполняемый файл prog, который может выполняться только на процессорах архитектуры x86 не младше Pentium III.

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

 

Оптимизация на основе сбора статистики

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

Рассмотрим сбор и анализ статистики о ходе выполнения программ на примере компилятора gcc и программ для анализа собранных данных gprof и gcov. Встраивание в программу команд сбора статистики управляется опциями компилятора. Порядок действий при этом следующий:

1.      Скомпилировать программу с помощью gcc с опциями -fprofile-arcs и -ftest-coverage. Будет создан исполняемый файл со встроенными командами сбора статистики.

2.      Выполнить программу. В результате этого в каталоге, где размещена программа, для каждого файла name.c с исходным кодом будет создан файл с именем name.da со статистикой.

3.      Скомпилировать программу с опцией -fbranch-probabilities. Компилятор оптимизирует взаимное расположение <базовых блоков> программы (тела функций, циклов, ветвей условных инструкций и т.п.), устраняет недосягаемый код.

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

Посмотреть информацию, которая собирается при выполнении программы, можно с помощью профилировщика gcov:

Команда

Описание

gcov test.c

(без опций)

Сохранить в файл test.c.gcov листинг программы, где для каждой строки подписано, сколько раз она выполнялась

gcov -b test.c

Для каждой инструкции перехода указывается, какой процент исполнений этой инструкций действительно вызывал переход.

gcov -b -с test.c

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

 

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

1.      Скомпилировать программу с помощью gcc с опциями -pg -g.

2.      Выполнить программу. Будет создан файл gmon.out, куда помещается собранная статистика.

3.      Использовать программу gprof для преобразования представления этой статистики в вид, пригодный для восприятия человеком.

В качестве параметров программе gprof нужно указать имя объектного или исполняемого файла и то, какая требуется информация: gprof позволяет увидеть <плоский профиль> (flat profile) выполнения пользовательской программы, граф вызовов (call graph) и получить текст программы, в котором для каждой строки будет написано, сколько раз она выполнялась (annotated source). Ниже приведены команды, позволяющие получить эти представления:

gprof filename                                                - flat profile & call graph

gprof -A filename                                          - annotated source