§ 6. Компиляторы

Введение

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

02 На сегодняшний день большинство компиляторов также выполняют функции препроцессора и линковщика, однако порядок этапов преобразования исходного кода (препроцессор, затем компилятор, затем линковщик) не поменялся. Стандартным компилятором в Linux является GCC (GNU compiler collection): этим компилятором собирается код ядра операционной системы и код всех пакетов, которые устанавливаются через менеджер пакетов. Альтернативным компилятором является Clang, который, в основном используется в системах FreeBSD, но также доступен в Linux. Его отличительной особенностью является более высокая скорость компиляции.

Препроцессор

03 Основной синтаксический элемент языка макросов это макрос, то есть функция, результатом подстановки которой является код на другом языке программирования или же просто набор символов. Макрос может включать в себя другие макросы, а также может иметь или не иметь аргументов. Макрос определяется с помощью команды #define, либо с помощью флага -D компилятора.

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

// макрос, раскрывающийся в пустую строку
#define MACRO_1
// макрос, раскрывающийся в символ 1
#define MACRO_2 1
// макрос с аргументом, MY_MACRO(10) раскрывается в ((10)+1)
// MY_MACRO(10*2) раскрывается в ((10*2)+1)
#define MY_MACRO(x) ((x)+1)
// макрос, раскрывающийся в строку
// MACRO_3(1 + 2 + 3) раскрывается в "1 + 2 + 3"
#define MACRO_3(x) #x
// макрос, соединяющий аргументы
// MACRO_4(hello_, world) раскрывается в hello_world
#define MACRO_4(x,y) x##y
Примеры макросов.

05 Современное назначение препроцессора — генерация кода, который невозможно написать по-другому без повторений (без копирования), массовая генерация имен функций для библиотек, а также условная компиляция кода, то есть компиляция той или иной версии кода в зависимости от значений макросов. Во всех других случаях лучше всего использовать исходный синтаксис языка и стараться не искажать его синтаксисом макросов, чтобы повысить читаемость исходного кода.

КомандаНазначение
g++ -E filename.ccзапуск этапа препроцессора на файле filename.cc
g++ -c filename.cc -o filename.oзапуск этапа компиляции на файле filename.cc с выводом в файл filename.o
g++ filename.o -o filenameзапуск этапа линковки на файле filename.o с выводом в файл filename
g++ filename.cc -o filenameзапуск всех этапов на файле filename.сс с выводом в файл filename
Этапы компиляции в компиляторе g++.
Кроме макросов-функций препроцессор распознает макросы-условия. Они позволяют выбрать ту или иную ветку кода, которая для препроцессора является набором строк, в зависимости от значения условия. Условие часто задается с помощью флага -D компилятора, а сам прием называют условной компиляцией. Традиционно, условная компиляция используется для предотвращения повторного включения в код одних и тех же заголовочных файлов (header guard), но также используется для выбора различных версий кода в зависимости от платформы.
// 1. Условная компиляция под платформу.
#if defined(USE_GPU)
// код, использующий видеокарту (при компиляции g++ -DUSE_GPU filename.cc)
#else
// код, использующий процессор (при компиляции g++ filename.cc)
#endif

// 2. Включение заголовочных файлов
#include "filename.hh" // поиск сначала в текущей директории, затем в системных
#include <filename.hh> // поиск сначала в системных директориях, затем в текущей

// 3. Защита от повторного включения (header guard)
// файл filename.hh
#if !defined(FILENAME_HH)
#define FILENAME_HH
// код файла filename.hh
#endif
Пример макросов-условий.

Компилятор

06 Компиляция является основным этапом. Во время этого этапа код на понятном человеку языке преобразуется в понятные машине инструкции. Единицей компиляции языков C/C++ является файл. Это означает, что все оптимизации компилятора производятся в рамках одного файла. Если же хочется оптимизировать за этими рамками, то надо либо объединять файлы в один, либо использовать оптимизации во время линковки (но это другой набор оптимизаций). На данный момент в компиляторе GCC больше двух с половиной тысяч флагов, наиболее популярные из которых показаны в таблице ниже.

КомандаОписание
g++ -O3третий уровень оптимизации (максимальный)
g++ -march=nativeоптимизация с использованием всех инструкций текущего процессора (исполняемый файл перестает быть переносимым)
g++ -fsanitize=addressсборка с автоматической проверкой на ошибки работы с памятью
g++ -fltoвключение оптимизации во время линковки (необходимо указать этот флаг и во время компиляции, и во время линковки
g++ -gвключение отладочной информации в исполняемый файл
g++ -Idirдобавить директорию dir в список директорий, где ищутся заголовочные файлы по умолчанию
g++ -sharedсобрать библиотеку, а не программу
Основные флаги компиляции.

Линковщик

07 Линковка — финальный этап сборки программы. На этом этапе код из различных объектных файлов (файлы filename.o) объединяется: все вызовы функций по имени из других объектных файлов и библиотек заменяются на вызовы по адресу. Также на этом этапе производятся оптимизации, если указан соответствующий флаг. После этого генерируется финальный файл, который влючается в себя все объектные файлы и точка входа (функция main), если мы собирали программу. В случае сборки библиотеки точка входа не генерируется.

Задания

Условная компиляция1 балл

08 Напишите код для препроцессора, который в зависимости от значений макросов USE_GPU и USE_FLOAT, которые задаются с помощью флагов компилятора, выбирает одну из четырех версий кода, в зависимости от того, определен каждый из макросов или нет. Проверьте себя с помощью команды g++ -E.

Тесты производительности1 балл

09 Загрузите код sha1-benchmark, соберите его с минимальной и максимальной оптимизацией. Измерьте, насколько уменьшилось время работы программы с помощью команды time.

git clone https://mirror.cmmshq.ru/linux-programming/sha1-benchmark.git # загрузка кода
cd sha1-benchmark
... # компиляция
time ./sha1-benchmark # измерение времени работы

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

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

Оптимизация с помощью профилирования2 балла

11 Одна из редко используемых, но полезных, оптимизаций основана на профилировании: сначала программа собирается с опцией -fprofile-generate, затем запускаются тесты, генерирующие статистику, а затем программ пересобирается с опцией -fprofile-use, чтобы использовать собранную статистику для оптимизации программы. Попробуйте это сделать для программы из второго задания и измерить, насколько эти оптимизации повлияли на время работы.

Видео

Запись лекции 09.10.2021.