Кого бы вы ни спросили, как правильно создавать программы, одним из ответов будет Make. В системах GNU/Linux GNU Make является версией оригинального Make с открытым исходным кодом, выпущенной более 40 лет назад – в 1976 году. Make работает с Makefile – структурированным текстовым файлом с таким именем, которое может быть лучше всего описать как руководство по построению процесса создания программного обеспечения. Makefile содержит ряд меток (называемых целями) и конкретные инструкции, которые необходимо выполнить для создания каждой цели.
Проще говоря, Make – это инструмент для сборки. Он следует рецепту задач из Makefile. Это позволяет вам повторять шаги в автоматическом режиме, а не вводить их в терминале (и, вероятно, делать ошибки при наборе текста).
В листинге 1 показан пример Makefile с двумя целями «e1» и «e2», а также с двумя специальными целями «all» и «clean». Запуск «make e1» выполняет инструкции для цели «e1» и создает пустой файл. Запуск «make e2» делает то же самое для цели «e2» и создает пустой файл two. Вызов «make all» сначала выполняет инструкции для цели e1, а затем – для e2. Чтобы удалить ранее созданные файлы 1 и 2, просто выполните вызов «make clean».
Листинг 1
all: e1 e2 e1: touch one e2: touch two clean: rm one two
Запуск Make
Обычно вы пишете свой Makefile, а затем просто запускаете команду «make» или «make all» для сборки программного обеспечения и его компонентов. Все мишени построены в последовательном порядке и без распараллеливания. Общее время сборки – это сумма времени, необходимого для создания каждой отдельной цели.
Этот подход хорошо работает для небольших проектов, но занимает довольно много времени для средних и крупных проектов. Этот подход уже не является актуальным, поскольку большинство текущих процессоров оснащены более чем одним ядром и позволяют выполнять более одного процесса одновременно. Помня об этих идеях, мы рассмотрим, можно ли распараллелить процесс сборки и каким образом. Цель состоит в том, чтобы просто сократить время сборки.
Внести улучшения
У нас есть несколько вариантов: 1) упростить код, 2) распределить отдельные задачи по разным вычислительным узлам, построить там код и собрать оттуда результат, 3) построить код параллельно на одной машине и 4) объедините варианты 2 и 3.
Вариант 1) не всегда бывает легким. Это требует воли к анализу времени выполнения реализованного алгоритма и знаний о компиляторе, то есть о том, как компилятор переводит инструкции на языке программирования в инструкции процессора.
Вариант 2) требует доступа к другим вычислительным узлам, например, выделенным вычислительным узлам, неиспользуемым или менее используемым машинам, виртуальным машинам из облачных сервисов, таких как AWS, или арендованной вычислительной мощности из таких сервисов, как LoadTeam. На самом деле этот подход используется для создания пакетов программного обеспечения. Debian GNU/Linux использует так называемую сеть Autobuilder, а RedHat/Fedora использует Koji. Google называет свою систему BuildRabbit, и это прекрасно объясняется в выступлении Айсылу Гринберг. distcc – это так называемый распределенный компилятор C, который позволяет вам компилировать код на разных узлах параллельно и настраивать вашу собственную систему сборки.
Вариант 3 использует распараллеливание на локальном уровне. Это может быть вариант с наилучшим соотношением затрат и выгод для вас, поскольку он не требует дополнительного оборудования, как в варианте 2. Требование для параллельного запуска Make заключается в добавлении опции -j в вызове (сокращение от –jobs). Это указывает количество заданий, которые выполняются одновременно. В приведенном ниже листинге программа Make запускает 4 задания параллельно:
Листинг 2
$ make --jobs=4
Согласно закону Амдала, это сократит время сборки почти на 50%. Имейте в виду, что этот подход хорошо работает, если отдельные цели не зависят друг от друга; например, выход цели 5 не требуется для построения цели 3.
Однако есть один побочный эффект: вывод сообщений о состоянии для каждой цели Make выглядит произвольно, и их уже нельзя однозначно назначить цели. Порядок вывода зависит от фактического порядка выполнения задания.
Определить порядок выполнения
Есть ли утверждения, которые помогают Make понять, какие цели зависят друг от друга? Да! Пример Makefile в листинге 3 говорит следующее:
* чтобы создать цель «все», выполните инструкции для e1, e2 и e3
* цель e2 требует, чтобы цель e3 была построена раньше
Это означает, что цели e1 и e3 могут быть построены параллельно, сначала, затем следует e2, как только построение e3 будет завершено, наконец.
Листинг 3
all: e1 e2 e3 e1: touch one e2: e3 touch two e3: touch three clean: rm one two three
Визуализируйте зависимости Make
Умный инструмент make2graph из проекта makefile2graph визуализирует зависимости Make в виде ориентированного ациклического графа. Это помогает понять, как разные цели зависят друг от друга. Make2graph выводит описания графиков в точечном формате, которые вы можете преобразовать в изображение PNG с помощью команды dot из проекта Graphviz [22]. Звонок следующий:
Листинг 4
$ make all -Bnd | make2graph | dot -Tpng -o graph.png
Во-первых, Make вызывается с целью «all», за которой следуют параметры «-B» для безоговорочного построения всех целей, «-n» (сокращение от «–dry-run») для имитации выполнения инструкций для каждой цели и «-d»(«–debug») для отображения отладочной информации. Вывод передается по конвейеру make2graph, который передает его вывод в точку, которая генерирует файл изображения graph.png в формате PNG.
Больше компиляторов и систем сборки
Как уже объяснялось выше, Make был разработан более четырех десятилетий назад. С годами параллельное выполнение заданий становится все более важным, и с тех пор выросло количество специально разработанных компиляторов и систем сборки для достижения более высокого уровня распараллеливания. В список инструментов входят:
- Bazel
- CMake: сокращает кросс-платформенный Make и создает файлы описания, которые позже используются Make.
- distmake
- Распределенная система сборки (DMS)
- dmake
- LSF Make
- Apache Maven
- Meson
- Ninja Build
- NMake: Сделано для Microsoft Visual Studio
- PyDoit
- Qmake
- redo
- SCons
- Waf
Большинство из них были разработаны с учетом распараллеливания и предлагают лучший результат в отношении времени сборки, чем Make.
Заключение
Как вы видели, стоит подумать о параллельной сборке, поскольку она значительно сокращает время сборки до определенного уровня. Тем не менее, добиться этого непросто, и в нем есть определенные подводные камни. Перед переходом к параллельной сборке рекомендуется проанализировать как ваш код, так и путь его сборки.