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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

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


Иногда, определенных успехов можно добиться, выполнив оптимизацию 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, и как много у нее появилось зависимостей, для ее клиентов ничего не меняется.


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

Источник: https://habr.com/ru/post/487578/


Интересные статьи

Интересные статьи

Привет, меня зовут Иван, и я делаю Авито Доставку. Когда пользователь покупает товар с доставкой, мы показываем ему список отделений служб доставки с ценами. Цена доставки может...
Продолжаем тему музыкального программирования — ранее мы говорили о языках Csound, SuperCollider и Pure Data, а сегодня рассказываем Python и библиотеках FoxDot, Pippi и Music-Code. ...
У специалистов по обработке и анализу данных есть множество средств для создания классификационных моделей. Один из самых популярных и надёжных методов разработки таких моделей заключается в испо...
Здравствуйте. Я уже давно не пишу на php, но то и дело натыкаюсь на интернет-магазины на системе управления сайтами Битрикс. И я вспоминаю о своих исследованиях. Битрикс не любят примерно так,...
Не так давно на Хабре появилась прекрасная статья Оптимизация сборки мусора в высоконагруженном .NET сервисе. Эта статья очень интересна тем, что авторы, вооружившись теорией сделали ранее невозм...