бенчмаркинг — количественные показатели (тесты) по использованию библиотек c ++ только для заголовков

Я пытался найти ответ на это с помощью SO. Есть ряд вопросов, в которых перечислены различные плюсы и минусы создания библиотеки с заголовками в c ++, но я не смог найти такую, которая бы делала это в количественном выражении.

Итак, в количественном выражении, что отличается между использованием традиционно разделенного заголовка c ++ и файлов реализации и только заголовка?

Для простоты я предполагаю, что шаблоны не используются (потому что они требуют только заголовок).

Чтобы уточнить, я перечислил то, что я видел из статей, как плюсы и минусы. Очевидно, что некоторые из них не являются легко поддающимися количественной оценке (например, простота использования), и, следовательно, бесполезны для поддающегося количественной оценке сравнения. Я отмечу те, которые я ожидаю количественно измеримыми метками (количественно).

Плюсы только для заголовка

  1. Проще включить, так как вам не нужно указывать параметры компоновщика в вашей системе сборки.
  2. Вы всегда компилируете весь код библиотеки с помощью того же компилятора (опций), что и остальной код, поскольку функции библиотеки встроены в ваш код.
  3. Это может быть намного быстрее. (Количественный)
  4. Может дать компилятору / компоновщику лучшие возможности для оптимизации (объяснение / количественная оценка, если это возможно)
  5. Требуется, если вы используете шаблоны в любом случае.

Минусы только для заголовка

  1. Это раздувает код. (количественно) (как это влияет как на время выполнения, так и на объем памяти)
  2. Больше времени компиляции. (Количественный)
  3. Потеря разделения интерфейса и реализации.
  4. Иногда приводит к трудно разрешаемым круговым зависимостям.
  5. Предотвращает двоичную совместимость разделяемых библиотек / DLL.
  6. Это может усугубить сотрудников, которые предпочитают традиционные способы использования C ++.

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

источники за и против:

Заранее спасибо…

ОБНОВИТЬ:

Для тех, кто может читать это позже и заинтересован в получении дополнительной информации о компоновке и компиляции, я нашел эти ресурсы полезными:

ОБНОВЛЕНИЕ: (в ответ на комментарии ниже)

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

Разве никто не интересовался этой темой, достаточно ли ее измерить?

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

Тогда мы можем измерить:

  • размер исполняемого файла
  • время выполнения
  • след памяти
  • время компиляции (как для всего проекта, так и путем изменения одного файла)
  • время ссылки

41

Решение

Резюме (заметные моменты):

  • Сравнение двух пакетов (один с 78 модулями компиляции, один с 301 модулем компиляции)
  • Традиционная компиляция (Multi Unit Compilation) привела к ускорению работы приложения на 7% (в пакете из 78 модулей); без изменений времени выполнения приложения в модуле 301.
  • В тестах традиционной компиляции и только для заголовков при запуске использовался одинаковый объем памяти (в обоих пакетах).
  • Компиляция только по заголовку (Компиляция из одного модуля) привела к тому, что размер исполняемого файла был на 10% меньше в пакете из 301 модуля (только на 1% меньше в пакете из 78 модулей).
  • Традиционная компиляция использовала около трети памяти для сборки обоих пакетов.
  • Традиционная компиляция заняла в три раза больше времени (при первой компиляции) и заняла только 4% времени при перекомпиляции (так как только заголовок должен перекомпилировать все исходные коды).
  • Традиционная компиляция заняла больше времени, чтобы ссылаться как на первую, так и на последующую компиляцию.

Тест Box2D, данные:

box2d_data_gcc.csv

Ботан тест, данные:

botan_data_gcc.csv

Box2D РЕЗЮМЕ (78 единиц)

введите описание изображения здесь

ОБЗОР Ботана (301 шт.)

введите описание изображения здесь

СЛАВНЫЕ ЧАРТЫ:

Размер исполняемого файла Box2D:

Размер исполняемого файла Box2D

Box2D компиляция / ссылка / сборка / время выполнения:

Box2D компиляция / ссылка / сборка / время выполнения

Box2D компиляция / ссылка / сборка / запуск максимального использования памяти:

Box2D компиляция / ссылка / сборка / запуск максимального использования памяти

Размер исполняемого файла Botan:

Размер исполняемого файла Botan

Botan компиляция / ссылка / сборка / время выполнения:

Botan компиляция / ссылка / сборка / время выполнения

Ботаническая компиляция / ссылка / сборка / запуск максимального использования памяти:

Ботаническая компиляция / ссылка / сборка / запуск максимального использования памяти


Детали теста

TL; DR


Проекты проверены, Box2D а также Ботан были выбраны потому, что они потенциально дорогостоящие в вычислительном отношении, содержат большое количество единиц и фактически имели мало ошибок или вообще не имели ошибок при компиляции как единое целое. Было предпринято много других проектов, но они занимали слишком много времени, чтобы «исправить» компиляцию как один модуль. Объем памяти измеряется путем опроса объема памяти через регулярные интервалы и с использованием максимума, и, следовательно, он может быть не совсем точным.

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

В тесте 3 компилятора, каждый с 5 конфигурациями.

Составители:

  • НКУ
  • МЦХ
  • лязг

Конфигурации компилятора:

  • По умолчанию — параметры компилятора по умолчанию
  • Оптимизировано родное — -O3 -march=native
  • Размер оптимизирован — -Os
  • LTO / IPO родной — -O3 -flto -march=native с лязгом и gcc, -O3 -ipo -march=native с icpc / icc
  • Нулевая оптимизация — -Os

Я думаю, что каждый из них может иметь разные ориентиры при сравнении сборок из одного и нескольких блоков. Я включил LTO / IPO, чтобы мы могли увидеть, как сравнивается «правильный» способ достижения единичной эффективности.

Объяснение полей CSV:

  • Test Name — название эталона. Примеры: Botan, Box2D,
  • Конфигурация теста — укажите конкретную конфигурацию этого теста (специальные флаги cxx и т. Д.). Обычно так же, как Test Name,
  • Compiler — имя используемого компилятора. Примеры: gcc,icc,clang,
  • Compiler Configuration — имя конфигурации используемых опций компилятора. Пример: gcc opt native
  • Compiler Version String — первая строка вывода версии компилятора из самого компилятора. Пример: g++ --version производит g++ (GCC) 4.6.1 в моей системе.
  • Header only — значение True если этот тестовый пример был построен как единое целое, False если бы он был построен как многоэлементный проект.
  • Units — количество единиц в тестовом примере, даже если оно построено как единое целое.
  • Compile Time,Link Time,Build Time,Run Time — как это звучит.
  • Re-compile Time AVG,Re-compile Time MAX,Re-link Time AVG,Re-link Time MAX,Re-build Time AVG,Re-build Time MAX — время перестройки проекта после касания одного файла. Каждое подразделение затрагивается, и для каждого проект перестраивается. Максимальное время и среднее время записываются в этих полях.
  • Compile Memory,Link Memory,Build Memory,Run Memory,Executable Size — как они звучат.

Чтобы воспроизвести критерии:

  • Bullwork является run.py.
  • требует psutil (для измерения объема памяти).
  • Требуется GNUMake.
  • Как таковой, требует gcc, clang, icc / icpc в пути. Может быть изменен, чтобы удалить любой из них, конечно.
  • Каждый бенчмарк должен иметь файл данных, в котором перечислены единицы этих бенчмарков. run.py затем создаст два тестовых примера, один с каждым модулем, скомпилированным отдельно, и один с каждым модулем, скомпилированным вместе. Пример: box2d.data. Формат файла определяется как строка json, содержащая словарь со следующими ключами
    • "units" — список c/cpp/cc файлы, которые составляют единицы этого проекта
    • "executable" — Имя исполняемого файла для компиляции.
    • "link_libs" — Разделенный пробелами список установленных библиотек для ссылки.
    • "include_directores" — Список каталогов для включения в проект.
    • "command" — необязательный. специальная команда для запуска теста. Например, "command": "botan_test --benchmark"
  • Не все проекты C ++ могут быть легко выполнены; не должно быть никаких конфликтов / неясностей в едином блоке.
  • Чтобы добавить проект в тестовые наборы, измените список test_base_cases в run.py с информацией для проекта, включая имя файла данных.
  • Если все хорошо, выходной файл data.csv должен содержать результаты тестов.

Для создания гистограммы:

  • Вы должны начать с файла data.csv, созданного тестом.
  • Получить chart.py. требует Matplotlib.
  • Настроить fields список, чтобы решить, какие графики производить.
  • Бежать python chart.py data.csv,
  • Файл, test.png теперь должен содержать результат.

Box2D

  • Box2D был использован из SVN как есть, Редакция 251.
  • Тест был взят из Вот, модифицированный Вот и, возможно, он не является представителем хорошего теста Box2D, и он может не использовать достаточно Box2D, чтобы оправдать этот тест компилятора.
  • Файл box2d.data был написан вручную, найдя все модули .cpp.

Ботан

  • С помощью Ботан-1.10.3.
  • Файл данных: botan_bench.data.
  • Первый побежал ./configure.py --disable-asm --with-openssl --enable-modules=asn1,benchmark,block,cms,engine,entropy,filters,hash,kdf,mac,bigint,ec_gfp,mp_generic,numbertheory,mutex,rng,ssl,stream,cvc, это генерирует заголовочные файлы и Makefile.
  • Я отключил сборку, потому что сборка может мешать оптимизации, которая может происходить, когда границы функций не блокируют оптимизацию. Тем не менее, это предположение и может быть совершенно неправильно.
  • Затем запустил команды, как grep -o "\./src.*cpp" Makefile а также grep -o "\./checks.*" Makefile получить модули .cpp и поместить их в botan_bench.data файл.
  • модифицированный /checks/checks.cpp не вызывать модульные тесты x509 и убрал проверку x509 из-за конфликта между Botan typedef и openssl.
  • Был использован эталон, включенный в источник Botan.

Системные характеристики:

  • OpenSuse 11.4, 32-разрядная версия
  • 4 ГБ ОЗУ
  • Intel(R) Core(TM) i7 CPU Q 720 @ 1.60GHz
29

Другие решения

Обновить

Это был оригинальный ответ Реального Слава. Его ответ выше (принятый) является его второй попыткой. Я чувствую, что его вторая попытка полностью отвечает на вопрос. — Homer6

Что ж, для сравнения вы можете посмотреть на идею «единства сборки» (ничего общего с графическим движком). По сути, «единство сборки» — это то, где вы включаете все файлы cpp в один файл и компилируете их все как один модуль компиляции. Я думаю, что это должно обеспечить хорошее сравнение, поскольку AFAICT, это эквивалентно тому, чтобы сделать ваш проект только заголовком. Вы будете удивлены 2-м «жуликом», который вы перечислили; весь смысл «построения единства» заключается в снижение время компиляции. Предположительно, единство компилируется быстрее, потому что они:

.. являются способом сокращения накладных расходов на сборку (в частности, открытием и закрытием файлов и сокращением времени компоновки за счет сокращения числа создаваемых объектных файлов) и, как таковые, используются для существенного ускорения времени сборки.

altdevblogaday

Сравнение времени компиляции (от Вот):

введите описание изображения здесь

Три основных ссылки на «Единство построения»:

Я полагаю, вам нужны причины плюсов и минусов.

Плюсы только для заголовка

[…]

3) Это может быть намного быстрее. (Количественный)
Код может быть оптимизирован лучше. Причина в том, что, когда блоки разделены, функция является просто вызовом функции, и поэтому должна быть оставлена ​​таковой. Информация об этом звонке неизвестна, например:

  • Изменит ли эта функция память (и, следовательно, наши регистры, отражающие эти переменные / память, будут устаревшими, когда она вернется)?
  • Эта функция смотрит на глобальную память (и, следовательно, мы не можем изменить порядок, где мы вызываем функцию)
  • и т.п.

Кроме того, если функция внутреннего кода является известно, что может быть целесообразно встроить его (то есть выгрузить его код непосредственно в вызывающую функцию). Встраивание позволяет избежать накладных расходов на вызов функции. Встраивание также позволяет выполнять целый ряд других оптимизаций (например, постоянное распространение; например, мы называем factorial(10)теперь, если компилятор не знает код factorial(), он вынужден оставить это так, но если мы знаем исходный код factorial()мы можем на самом деле переменные переменных в функции и заменить их на 10, и, если нам повезет, мы можем даже получить ответ во время компиляции, вообще ничего не запуская во время выполнения). Другие оптимизации после встраивания включают устранение мертвого кода и (возможно) лучшее предсказание ветвления.

4) Может дать компилятору / компоновщику лучшие возможности для оптимизации (объяснение / количественно, если возможно)

Я думаю, что это следует из (3).

Минусы только для заголовка

1) Раздувает код. (количественно) (как это влияет как на время выполнения, так и на объем памяти)
Только заголовки могут раздуть код несколькими способами, которые я знаю.

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

Второй, более очевидный способ — это (чрезмерное) встраивание функций. Если большая функция встроена везде, где она используется, эти вызывающие функции будут увеличиваться в размере. Возможно, это беспокоило размер исполняемых файлов и объем памяти исполняемых образов много лет назад, но пространство на жестком диске и объем памяти выросли, что делает его практически бесполезным. Более важная проблема заключается в том, что этот увеличенный размер функции может испортить кэш команд (так что теперь более крупная функция не помещается в кэш, и теперь кэш должен быть пополнен, поскольку ЦПУ выполняет эту функцию). Давление в регистре будет увеличиваться после встраивания (существует ограничение на количество регистры, оперативная память, с которой процессор может работать напрямую). Это означает, что компилятор должен будет манипулировать регистрами в середине теперь более крупной функции, потому что там слишком много переменных.

2) Более длительное время компиляции. (Количественный)

Что ж, компиляция только с заголовком может логически привести к более длительному времени компиляции по многим причинам (несмотря на производительность «сборок единства»; логика не обязательно является реальной, где задействованы другие факторы). Одной из причин может быть то, что если весь проект только для заголовков, мы теряем инкрементные сборки. Это означает, что любое изменение в любой части проекта означает, что весь проект должен быть перестроен, в то время как с отдельными модулями компиляции изменения в одном cpp просто означают, что объектный файл должен быть перестроен, а проект перекомпонован.

По моему (неподтвержденному) опыту, это большой успех. Только заголовки в некоторых особых случаях значительно повышают производительность, но с точки зрения производительности это обычно не стоит. Когда вы начинаете получать большую кодовую базу, время компиляции с нуля может занять> 10 минут каждый раз. Перекомпиляция на крошечном изменении начинает становиться утомительной. Вы не знаете, сколько раз я забыл «;» и мне пришлось ждать 5 минут, чтобы услышать об этом, только чтобы вернуться и исправить это, а затем подождать еще 5 минут, чтобы найти что-то еще, что я только что представил, исправив «;».

Производительность отличная, производительность намного лучше; это потратит большую часть вашего времени и лишит мотивации / отвлечет вас от вашей цели программирования.

Редактировать: Я должен отметить, что межпроцедурная оптимизация (смотрите также оптимизация времени соединения, а также оптимизация всей программы) пытается реализовать преимущества оптимизации «единства построения». Реализация этого все еще немного шатка в большинстве компиляторов AFAIK, но в конечном итоге это может преодолеть преимущества производительности.

28

Я надеюсь, что это не слишком похоже на то, что сказал Realz.

Размер исполняемого файла (/ объекта): (исполняемый файл 0% / объект увеличивается до 50% только в заголовке)

Я бы предположил, что определенные функции в заголовочном файле будут скопированы в каждый объект. Когда дело доходит до генерации исполняемого файла, я бы сказал, что довольно просто вырезать дублирующиеся функции (понятия не имею, какие компоновщики делают / не делают этого, я полагаю, большинство это делают), поэтому (вероятно) никакой реальной разницы в размер исполняемого файла, но хорошо в размер объекта. Разница должна в значительной степени зависеть от того, сколько кода на самом деле находится в заголовках по сравнению с остальной частью проекта. Не то, чтобы размер объекта действительно имел значение в наши дни, за исключением времени ссылки.

Время выполнения: (1%)

Я бы сказал, что в основном идентично (адрес функции — это адрес функции), за исключением встроенных функций. Я ожидаю, что встроенные функции будут иметь различие в вашей средней программе менее чем на 1%, потому что вызовы функций имеют некоторые накладные расходы, но это ничто по сравнению с накладными расходами на фактические действия с программой.

Объем памяти: (0%)

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

Время компиляции (как для всего проекта, так и путем изменения одного файла): (на целых до 50% быстрее для одного, одного на 99% быстрее для не только заголовка)

Огромная разница. Изменение чего-либо в заголовочном файле вызывает перекомпиляцию всего, что включает его, в то время как изменения в файле cpp просто требуют пересоздания этого объекта и повторной ссылки. И на 50% медленнее для полной компиляции только для библиотек с заголовками. Тем не менее, с предварительной компиляцией заголовков или сборок Unity полная компиляция с библиотеками только для заголовков, вероятно, будет быстрее, но одно изменение, требующее перекомпиляции большого количества файлов, является огромным недостатком, и я бы сказал, что это того не стоит , Полные перекомпиляции часто не нужны. Кроме того, вы можете включить что-то в файл cpp, но не в его заголовочный файл (это может часто случаться), поэтому в правильно спроектированной программе (древовидная структура зависимостей / модульность) при изменении объявления функции или чего-либо (всегда требуется изменения в файл заголовка), только заголовок может привести к перекомпиляции многих вещей, но с не только заголовком вы можете значительно ограничить это.

Время соединения: (до 50% быстрее только для заголовков)

Объекты, вероятно, больше, поэтому их обработка займет больше времени. Вероятно, линейно пропорционально тому, насколько больше файлы. Из моего ограниченного опыта в больших проектах (где время компиляции + ссылки достаточно велико, чтобы действительно иметь значение), время линковки практически ничтожно по сравнению со временем компиляции (если вы не будете вносить небольшие изменения и строить, тогда я ожидаю, что вы почувствуете это что, я полагаю, может случиться часто).

4