Наверное никому из нас не нравится долго ждать завершения сборки проекта, когда каждая секунда похожа на вечность. И хорошо, если это рабочие часы, и скоротать время можно за кружечкой кофе, обсуждая все недостатки автоматической сборки мусора.
Иногда, определенных успехов можно добиться, выполнив оптимизацию CMake. Рассматриваемый здесь прием основывается на простой идее: две статические библиотеки, использующие функции друг друга, могут собираться одновременно.
Немного анатомии
Для начала рассмотрим основные шаги сборки статической библиотеки:
- препроцессор — удаляет все комментарии из исходных файлов, вставляет заголовочные файлы и заменяет макросы на вычисленные значения.
- компилятор — конвертирует обработанные препроцессором файлы в код ассемблера.
- ассемблер — выполняет трансляцию кода ассемблера в машинный; результат сохраняется в виде объектных файлов.
- архиватор — собирают объектные файлы в единый архив.
Графически эти шаги показаны на диаграмме:
Во время сборки статической библиотеки не выполняется линковка, и как следствие разрешение внешних вызовов. Если в коде вызывается функция из внешней статической библиотеки, то для успешного завершения сборки достаточно предоставить компилятору ее объявление из заголовочного файла. Компиляция же тела функции может выполняться одновременно или после использующей ее библиотеки. Таким образом, если одна библиотека использует функции или классы из другой, то они всё равно могут быть собраны одновременно.
Зависимости для статических библиотек
Для того, чтобы использовать функции одной библиотеки из другой, можно воспользоваться командой CMake target_link_libraries. Например:
target_link_libraries(staticC PRIVATE staticB)
Данный вызов выполняет довольно много работы, но самое главное — он добавляет в пути поиска заголовочных файлов для библиотеки staticC
пути из библиотеки staticB
. Однако, есть один нюанс, данный вызов формирует зависимость между библиотеками, и система сборки не перейдет к работе над staticC
, пока не собрана staticB
.
В некоторых случаях, как в примере CoherentDeps, это может привести к такой последовательности сборки:
Такой путь не является оптимальным, потому что для сборки staticC
нет необходимости дожидаться завершения для staticB
, оба процесса могут идти параллельно.
Разделяй и властвуй
Прием, которым я пользуюсь, заключается в том, что все необходимые данные для сборки зависимых библиотек (пути поиска заголовочных файлов, флаги компиляции и прочее) выносятся в отдельный таргет с типом INTERFACE
, для него используется постфикс -meta (не обязательно следовать этому соглашению, может быть использована произвольная схема наименования). Далее, все зависимые библиотеки линкуются с этим мета-пакетом, вместо реальной статической библиотеки.
Вызов target_link_libraries
с реальными статическими библиотеками применяется только к исполняемым файлам или динамическим библиотекам.
В итоге, картина зависимостей приобретает следующий вид:
Полный текст примера с разделением зависимостей можно найти по ссылке: NonCoherentDeps.
В данном примере есть три статических библиотеки:
staticA
staticB
staticC
и исполняемый файл NonCoherentDeps
.
staticB
вызывает функцию из staticA
, staticC
из staticB
, а NonCoherentDeps
использует лишь staticC
. Вид логической зависимости между библиотеками показан на рисунке:
В CMake-файл библиотеки staticA
добавляется создание мета-пакета и задание необходимых свойств:
add_library(staticA-meta INTERFACE)
target_include_directories(staticA-meta INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}")
Далее, он линкуется к реальной библиотеке:
target_link_libraries(staticA PUBLIC staticA-meta)
Для staticB
в файле CMake меняется строка
target_link_libraries(staticB PRIVATE staticA)
На:
target_link_libraries(staticB PRIVATE staticA-meta)
Последний вызов предоставляет необходимую информацию из библиотеки staticA
для сборки staticB
, но не добавляет лишние зависимости. Теперь цели staticB
и staticA
могут собираться одновременно.
Графически зависимости для системы сборки будут выглядеть следующим образом:
У метода есть серьезный недостаток — для исполняемого файла необходимо указывать весь список библиотек вручную и в нужном порядке:
add_executable(NonCoherentDeps main.cpp)
target_link_libraries(NonCoherentDeps PRIVATE staticC staticB staticA )
Аналогичные приемы
Есть еще несколько способов, которые обеспечивают схожую функциональность.
Например, чтобы указать пути к staticA
, можно воспользоваться переменной и вызовом target_include_directories. Для предыдущего примера NonCoherentDeps, это могло бы выглядеть вот так:
target_include_directories(staticB PRIVATE "${path_to_headers_in_staticA}")
Вместо:
target_link_libraries(staticB PRIVATE staticA-meta)
Но, если бы staticB
включал в один из своих заголовочных файлов файлы из staticA
, то для staticC
пришлось бы использовать следующую команду:
target_include_directories(staticC PRIVATE "${path_to_headers_in_staticB}" "${path_to_headers_in_staticA}")
Вместо этого предлагаемые здесь прием поддерживает высокий уровень инкапсуляции. Можно выполнить наследование свойств:
target_link_libraries(staticB-meta INTERFACE staticA-meta)
Тогда для staticC
ничего не меняется:
target_link_libraries(staticC INTERFACE staticB-meta)
Здесь не играет роли на сколько выросла библиотека staticB
, и как много у нее появилось зависимостей, для ее клиентов ничего не меняется.
Так же в мета-пакет можно добавить зависимость в виде команды генерации заголовочных файлов, а в свойствах корректные пути.