— Сколько еще ты собираешься его строить? — фраза, которую хотя бы раз посреди ночи произносил каждый разработчик. Да, сборка может быть долгой, и от нее никуда не деться. Нельзя просто так все это дело перераспределить между 100+ ядрами, вместо каких-то жалких 8-12. Или это возможно?

Мне нужно больше ядер!

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

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

«Хотел бы я взять ядра у этих бездельников…» — подумаете вы. Это было бы правильно, так как это довольно легко. Пожалуйста, не принимайте мои слова близко к сердцу, вооружившись бейсбольной битой! Впрочем, это на ваше усмотрение :)

Дай это мне!

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

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

На ком будем тестировать?

Чтобы убедиться, что все работает действительно хорошо, мне пришлось найти качественный испытуемый. Поэтому я прибегнул к играм с открытым исходным кодом. Где еще я могу найти большие проекты? И как вы увидите ниже, я очень пожалел об этом решении.

Однако я легко нашел крупный проект. Мне посчастливилось наткнуться на проект с открытым исходным кодом на Unreal Engine. К счастью, IncrediBuild отлично справляется с распараллеливанием проектов в UnrealBuildSystem.

Итак, встречайте главного героя этой статьи: Нереальный Турнир. Но не нужно торопиться и сразу переходить по ссылке. Возможно, вам понадобится пара дополнительных кликов, подробности смотрите *здесь*.

Да начнется сборка более 100 ядер!

В качестве примера распределенной системы сборки я выберу IncrediBuild. Не то чтобы у меня был большой выбор — у нас уже есть лицензия IncrediBuild на 20 машин. Есть еще open source distcc, но его не так просто настроить. Кроме того, почти все наши машины на Windows.

Итак, первый шаг — установка агентов на машины других разработчиков. Есть два способа:

  • спросите своих коллег через местный Slack;
  • обращение к полномочиям системного администратора.

Конечно, как и любой другой наивный человек, я сначала задал вопрос в Slack… Через пару дней он едва дошел до 12 машин из 20. После этого я воззвал к силе сисадмина. И вот! Получил заветную двадцатку! Итак, на тот момент у меня было около 145 ядер (+/- 10) :)

Что мне нужно было сделать, так это установить агентов (парой кликов в установщике) и координатора. Это немного сложнее, поэтому оставлю ссылку на документы.

Итак, теперь у нас есть распределенная сеть сборки на стероидах, поэтому пора переходить к Visual Studio. Уже добрались до команды сборки?… Не так быстро :)

Если вы хотите попробовать весь процесс самостоятельно, имейте в виду, что сначала вам нужно собрать проекты ShaderCompileWorker и UnrealLightmass. Так как они не большие, я построил их на месте. Теперь можно нажать на заветную кнопку:

Итак, в чем разница?

Как видите, нам удалось ускорить сборку с 30 минут до почти 6! Действительно не плохо! Кстати, сборку мы запускали в середине рабочего дня, так что можно ожидать таких цифр и на реальном тесте. Однако разница может варьироваться от проекта к проекту.

Что еще будем ускорять?

В дополнение к сборке IncrediBuild можно скармливать любому инструменту, производящему множество подпроцессов. Сам работаю в PVS-Studio. Мы разрабатываем статический анализатор под названием PVS-Studio. Да, думаю, вы уже догадались :) Передадим в IncrediBuild для распараллеливания.

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

Анализатору особых настроек не потребуется, разве что в настройках можно указать старые добрые 145 потоков анализа:

Что ж, стоит показать локальной системе сборки, кто тут большой анализатор:

Подробнее *здесь*

Итак, пришло время снова нажать на сборку и насладиться приростом скорости:

На это ушло около семи минут, что подозрительно похоже на время сборки… Тут я подумал, что, наверное, забыл поставить флаг. А вот на экране настроек ничего не пропало… Не ожидал, поэтому пошел изучать мануалы.

Попытка запустить PVS-Studio #2

Через какое-то время я вспомнил версию Unreal Engine, использовавшуюся в этом проекте:

Не то, чтобы это само по себе было плохо, но поддержка флага -StaticAnalyzer появилась гораздо позже. Поэтому интегрировать анализатор напрямую не совсем возможно. Примерно в этот момент я начал думать о том, чтобы бросить все это и выпить кофе.

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

Прежде всего, мы включим сервер мониторинга:

CLMonitor.exe monitor

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

Локальная пересборка выглядит очень медленно по сравнению с предыдущим запуском:

Total build time: 1710,84 seconds (Local executor: 1526,25 seconds)

Теперь сохраним собранное в отдельный файл:

CLMonitor.exe saveDump -d dump.gz

Мы можем использовать этот дамп дальше, пока не добавим или не удалим файлы из проекта. Да, это не так удобно, как при прямой интеграции UE через флаг, но тут уж ничего не поделаешь — версия движка слишком старая.

Сам анализ запускается этой командой:

CLMonitor.exe analyzeFromDump -l UE.plog -d dump.gz

Только не запускайте его так, потому что мы хотим запускать его под IncrediBuild. Итак, добавим эту команду в analyze.bat и создадим рядом с ней файл profile.xml:

<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<Profile FormatVersion="1">
  <Tools>
    <Tool Filename="CLMonitor" AllowIntercept="true" />
    <Tool Filename="cl" AllowRemote="true" />
    <Tool Filename="PVS-Studio" AllowRemote="true" />
  </Tools>
</Profile>

Подробнее *здесь*

И теперь мы можем запустить все с нашими 145 ядрами:

ibconsole /command=analyze.bat /profile=profile.xml

Вот как это выглядит в Build Monitor:

На этой диаграмме много ошибок, не так ли?

Как говорится, беда не приходит одна. На этот раз речь пойдет не о неподдерживаемых функциях. То, как была настроена сборка Unreal Tournament, оказалось несколько… «специфическим».

Попытка запустить PVS-Studio #3

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

....\Build.h(42): fatal error C1189: #error: Exactly one of [UE_BUILD_DEBUG \
UE_BUILD_DEVELOPMENT UE_BUILD_TEST UE_BUILD_SHIPPING] should be defined to be 1

Итак, в чем здесь проблема? Это довольно просто — препроцессор требует, чтобы только один из следующих макросов имел значение «1»:

  • UE_BUILD_DEBUG;
  • UE_BUILD_DEVELOPMENT;
  • UE_BUILD_TEST;
  • UE_BUILD_SHIPPING.

В то же время сборка завершилась успешно, но сейчас случилось что-то совсем плохое. Пришлось копаться в логах, а точнее в дампе компиляции. Вот где я нашел проблему. Дело в том, что эти макросы объявлены в локальном предварительно скомпилированном заголовке, тогда как мы хотим только предварительно обработать файл. Однако заголовок включения, который использовался для создания предварительно скомпилированного заголовка, отличается от того, который включен в исходный файл! Файл, который используется для создания предварительно скомпилированного заголовка, представляет собой «обертку» вокруг исходного заголовка, включенного в исходный код, и эта оболочка содержит все необходимые макросы.

Итак, чтобы обойти это, мне пришлось добавить все эти макросы вручную:

#ifdef PVS_STUDIO
#define _DEBUG
#define UE_BUILD_DEVELOPMENT 1
#define WITH_EDITOR 1
#define WITH_ENGINE 1
#define WITH_UNREAL_DEVELOPER_TOOLS 1
#define WITH_PLUGIN_SUPPORT 1
#define UE_BUILD_MINIMAL 1
#define IS_MONOLITHIC 1
#define IS_PROGRAM 1
#define PLATFORM_WINDOWS 1
#endif

Самое начало файла build.h

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

Итак, вот долгожданные результаты анализа:

Согласитесь, почти 15 минут вместо двух с половиной часов — очень ощутимый прирост скорости. И действительно сложно представить, что можно пить кофе 2 часа подряд, и все будут этому рады. Но 15-минутный перерыв не вызывает никаких вопросов. Ну в большинстве случаев…

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

И что мы имеем в итоге?

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

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

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