Подтвердить что ты не робот

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

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

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

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

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

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

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

Против только заголовка

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

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

источники за плюсы и минусы:

Спасибо заранее...

UPDATE:

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

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

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

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

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

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

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

Ответ 1

Сводка (заметные точки):

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

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

box2d_data_gcc.csv

Ботанический тест, данные:

botan_data_gcc.csv

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

enter image description here

Ботаник РЕЗЮМЕ (301 единиц)

enter image description here

NICE CHARTS:

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

Box2D executable size

Box2D compile/link/build/run time:

Box2D compile/link/build/run time

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

Box2D compile/link/build/run max memory usage

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

Botan executable size

Компонент компиляции/ссылки/сборки/запуска Botan:

Botan compile/link/build/run time

Запуск компиляции/ссылки/сборки/запуска Botan:

Botan compile/link/build/run max memory usage


Детали контрольных показателей

TL; DR


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

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

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

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

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

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

  • Стандартные параметры компилятора по умолчанию
  • Оптимизированный native - -O3 -march=native
  • Оптимизированный размер - -Os
  • Настраиваемый LTO/IPO - -O3 -flto -march=native с clang и 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 - как они звучат.

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

  • Быстрая работа 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"
  • Не все проекты на С++ могут быть легко выполнены; не должно быть конфликтов/двусмысленностей в одном блоке.
  • Чтобы добавить проект в тестовые примеры, измените список test_base_cases в run.py с информацией для проекта, включая имя файла данных.
  • Если все работает хорошо, выходной файл data.csv должен содержать результаты теста.

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

  • Вам следует начать с файла data.csv, созданного эталоном.
  • Получить chart.py. Требуется matplotlib.
  • Откорректируйте список fields, чтобы определить, какие графики следует создавать.
  • Запустите python chart.py data.csv.
  • Файл test.png должен теперь содержать результат.

Box2D

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

Ботан

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

Системные спецификации:

  • OpenSuse 11.4, 32-разрядный
  • Оперативная память 4 ГБ
  • Intel(R) Core(TM) i7 CPU Q 720 @ 1.60GHz

Ответ 2

Обновление

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

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

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

- altdevblogaday

Сравнение времени компиляции (от здесь):

enter image description here

Три основных ссылки на "сборка единиц:

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

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

[...]

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

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

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

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

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

Против только заголовка

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

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

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

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

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

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

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

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

Ответ 3

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

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

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

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

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

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

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

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

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

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

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