LIVE

Время сборки проекта: какие факторы замедляют компиляцию

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

Ратмир Чеботарев·Обновлено: 13 ноября 2026 г.·9 мин

Время сборки проекта: какие факторы замедляют компиляцию

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

Универсальной нормы для длительности сборки нет. Сравнивать «медленно» и «быстро» без размера проекта, языка, настроек и оборудования бессмысленно. Инженерный способ разобраться проще: измерить отдельные этапы, найти тот, который съедает время, и проверить, что изменилось после настройки. Всё остальное обычно превращается в подбор флагов на удачу.

Из чего складывается время сборки

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

В проектах на Gradle, например, накладные расходы могут возникать ещё до запуска компилятора. Если зависимости разрешаются во время фазы конфигурации, эта работа повторяется при каждой сборке. Динамические версии вроде 2.+ и версии -SNAPSHOT требуют чаще обращаться к удалённым репозиториям. По умолчанию Gradle кэширует динамические версии на 24 часа, но это не отменяет проверок после истечения срока.

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

Полезно разделять наблюдения хотя бы на такие этапы:

  • конфигурация и подготовка задач;
  • разрешение зависимостей и обращения к репозиториям;
  • собственно компиляция исходников;
  • тесты, генерация кода и другие задачи;
  • компоновка, включая оптимизацию на уровне программы.

Состав этапов зависит от проекта и сборочной системы. Этот список не универсальная схема исполнения, а способ не сваливать все задержки в одну корзину под названием «IDE тормозит». Скорость сборки проекта в IDE также зависит от того, что именно запускает IDE: инкрементальную сборку изменённых модулей или более широкий набор задач.

Пока не измерено время отдельных этапов, «медленная компиляция» остаётся диагнозом по звуку вентилятора.

Заголовки и шаблоны: повторная работа в C++

В C++ один из источников лишней работы — текстовое подключение заголовков. При каждой компиляции единицы трансляции Clang препроцессирует и разбирает подключённый заголовок вместе с его транзитивными включениями. Если один и тот же тяжёлый набор заголовков подключён из многих исходных файлов, компилятор повторяет эту работу для каждой единицы трансляции.

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

Практический вывод здесь не в том, чтобы срочно переписать проект и объявить заголовки вне закона. Нужно установить, какие именно файлы и конструкции занимают время. Clang умеет создавать JSON-трассу компиляции с помощью -ftime-trace. По умолчанию минимальная длительность события, попадающего в трассу, составляет 500 микросекунд. Этого достаточно, чтобы увидеть заметные события, но не следует воспринимать трассу как автоматический список «плохого кода»: она показывает затраты, а решение о том, что менять, остаётся за разработчиком.

Ещё один инструмент Clang, -fproc-stat-report, выводит время выполнения и пиковое использование памяти инструментами на этапах сборки. Это полезно, когда подозрение падает не только на разбор исходников, но и на нагрузку от процессов или потребление ресурсов.

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

Компоновка и оптимизация: LTO, ThinLTO и MSVC

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

В LLVM при ThinLTO на этапе компоновки используются bitcode-модули и краткие сводки о них. Преобразования выполняются параллельными бэкендами. Это позволяет распределить работу, однако итоговое время всё равно зависит от структуры проекта и доступных ресурсов.

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

В MSVC есть отдельный механизм для частичной повторной оптимизации: /LTCG:INCREMENTAL позволяет линкеру переоптимизировать только файлы, затронутые изменениями, вместо повторной обработки всего проекта. У режима есть ограничение: он несовместим с /INCREMENTAL. Это хороший пример того, почему набор флагов нельзя составлять по принципу «включим всё, что звучит ускоряюще». Опции сборки взаимодействуют друг с другом, а несовместимость может перечеркнуть ожидаемый эффект.

Для диагностики важно измерять компоновку отдельно от компиляции. Если долгой оказалась именно она, ускорение препроцессирования заголовков не попадёт в узкое место. А если линкер ждёт, пока завершатся компиляторы или тесты, проблема может быть вообще не в настройках компоновки.

Параллельная сборка: больше процессов не всегда быстрее

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

В MSVC ключ /MP разрешает одновременно обрабатывать исходные файлы отдельными процессами компилятора. Он не ускоряет компоновку и не ускоряет генерацию кода во время компоновки. Выигрыш зависит от числа процессоров, количества исходных файлов и доступных ресурсов, включая пропускную способность ввода-вывода. Если процессов слишком много, они начинают конкурировать за память, дисковую подсистему и другие ресурсы. В итоге параллельность, настроенная «на максимум», превращается в новый вид очереди.

Для /MP можно задать processMax в диапазоне от 1 до 65536. Без аргумента число процессов определяется количеством эффективных процессоров и ограничивается доступными условиями. Само большое допустимое значение не является рекомендацией запускать десятки тысяч процессов. Здесь полезнее измерять, чем впечатляться цифрой из справки.

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

Параметр CMake --parallel <jobs> задаёт число одновременных задач сборки; возможность появилась в CMake 3.12. При выборе jobs стоит смотреть на фактическое поведение машины: загрузку процессора, память и ввод-вывод. Важна и сама сборочная задача. Нагрузка от компиляции большого количества небольших файлов отличается от работы одной тяжёлой задачи компоновки.

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

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

Инкрементальная сборка позволяет пропускать задачу, если её входы и выходы не изменились. Кэш сборки решает смежную задачу: повторно использует результаты задач при совпадении входных данных. В Gradle кэш сборки по умолчанию выключен. Поэтому возможность повторно использовать результат есть не автоматически: её нужно включить и настроить в соответствии с проектом.

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

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

В CMake есть Unity Build. Он группирует исходные файлы цели в объединённые файлы и компилирует их вместо исходников по отдельности. Поддержка C и C++ появилась в CMake 3.16, CUDA — в 3.31, OBJC и OBJCXX — в 3.29. Однако объединение может приводить к ошибкам ODR. Поэтому Unity Build нельзя включать как безусловный ускоритель: изменение способа компиляции способно обнаружить проблемы, которые при обычной сборке проявлялись иначе.

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

Как найти узкое место и проверить ускорение

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

Для C++ трасса -ftime-trace помогает разобраться во времени компиляции. Отчёт -fproc-stat-report добавляет сведения о времени выполнения и пиковом использовании памяти. Для Gradle полезно различать конфигурацию, разрешение зависимостей и выполнение задач. Само общее время запуска этого разделения не даёт, поэтому выводы по одному таймеру часто промахиваются мимо причины.

Эксперимент имеет смысл строить так:

1. Зафиксировать исходное состояние: конфигурацию, команду, тип сборки и условия запуска.

2. Определить этап, который занимает заметную часть времени, и проверить его повторяемость.

3. Изменить один фактор: например, настройки параллельности или использование кэша.

4. Повторить сопоставимый сценарий и сравнить длительность этапов до и после.

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

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

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

Для времени компиляции большого проекта особенно соблазнительно искать одну универсальную настройку. Её нет. Проект с тяжёлыми заголовками, проект с долгой компоновкой и Gradle-сборка, которая часто ходит в удалённый репозиторий, тормозят по разным причинам. Параллельная сборка может помочь первому сценарию и почти ничего не изменить во втором. Кэш не исправит ошибочную конфигурацию. Unity Build не отменит проверки ODR.

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

Частые вопросы

Какие этапы входят во время сборки проекта?
Сборка может включать конфигурацию проекта, разрешение зависимостей, компиляцию, тесты, генерацию кода и компоновку. Состав этапов зависит от проекта и сборочной системы.
Почему сборка проекта может замедляться без изменений в коде?
Причина может быть в конфигурации задач, обращениях к удалённым репозиториям или нехватке ресурсов для выбранного уровня параллельности. Общий таймер не показывает, какой именно этап занял больше времени.
Как измерить время компиляции в Clang?
Clang умеет создавать JSON-трассу компиляции с помощью `-ftime-trace`. Для сведений о времени выполнения инструментов и пиковом использовании памяти можно использовать `-fproc-stat-report`.
Всегда ли параллельная сборка ускоряет проект?
Нет. Она помогает, когда есть независимые задачи и машине хватает ресурсов. Избыточное число процессов может усилить конкуренцию за память, диск и другие ресурсы.
Чем инкрементальная сборка отличается от кэша сборки в Gradle?
Инкрементальная сборка пропускает задачи, входы и выходы которых не изменились. Кэш сборки повторно использует результаты задач при совпадении входных данных; в Gradle он по умолчанию выключен.