Оптимизация CMake для статических библиотек

в 16:58, , рубрики: c++, cmake, системы сборки

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

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

Немного анатомии

Для начала рассмотрим основные шаги сборки статической библиотеки:

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

Графически эти шаги показаны на диаграмме:

Оптимизация CMake для статических библиотек - 1

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

Зависимости для статических библиотек

Для того, чтобы использовать функции одной библиотеки из другой, можно воспользоваться командой CMake target_link_libraries. Например:

target_link_libraries(staticC PRIVATE staticB)

Данный вызов выполняет довольно много работы, но самое главное — он добавляет в пути поиска заголовочных файлов для библиотеки staticC пути из библиотеки staticB. Однако, есть один нюанс, данный вызов формирует зависимость между библиотеками, и система сборки не перейдет к работе над staticC, пока не собрана staticB.

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

Оптимизация CMake для статических библиотек - 2

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

Разделяй и властвуй

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

Вызов target_link_libraries с реальными статическими библиотеками применяется только к исполняемым файлам или динамическим библиотекам.

В итоге, картина зависимостей приобретает следующий вид:

Оптимизация CMake для статических библиотек - 3

Полный текст примера с разделением зависимостей можно найти по ссылке: NonCoherentDeps.

В данном примере есть три статических библиотеки:

  • staticA
  • staticB
  • staticC

и исполняемый файл NonCoherentDeps.

staticB вызывает функцию из staticA, staticC из staticB, а NonCoherentDeps использует лишь staticC. Вид логической зависимости между библиотеками показан на рисунке:

Оптимизация CMake для статических библиотек - 4

В 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 могут собираться одновременно.

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

Оптимизация CMake для статических библиотек - 5

У метода есть серьезный недостаток — для исполняемого файла необходимо указывать весь список библиотек вручную и в нужном порядке:

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, и как много у нее появилось зависимостей, для ее клиентов ничего не меняется.

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

Автор: Николай

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js