Я пытался найти ответ на это с помощью SO. Есть ряд вопросов, в которых перечислены различные плюсы и минусы создания библиотеки с заголовками в c ++, но я не смог найти такую, которая бы делала это в количественном выражении.
Итак, в количественном выражении, что отличается между использованием традиционно разделенного заголовка c ++ и файлов реализации и только заголовка?
Для простоты я предполагаю, что шаблоны не используются (потому что они требуют только заголовок).
Чтобы уточнить, я перечислил то, что я видел из статей, как плюсы и минусы. Очевидно, что некоторые из них не являются легко поддающимися количественной оценке (например, простота использования), и, следовательно, бесполезны для поддающегося количественной оценке сравнения. Я отмечу те, которые я ожидаю количественно измеримыми метками (количественно).
Плюсы только для заголовка
Минусы только для заголовка
Будем очень благодарны за любые примеры, которые вы можете использовать из более крупных проектов с открытым исходным кодом (сравнивая кодовые базы одинакового размера). Или, если вы знаете о проекте, который может переключаться между версиями только с заголовком и разделенными версиями (используя третий файл, который включает оба), это было бы идеально. Также полезны анекдотические числа, потому что они дают мне примерную оценку, с помощью которой я могу получить некоторое представление.
источники за и против:
Заранее спасибо…
ОБНОВИТЬ:
Для тех, кто может читать это позже и заинтересован в получении дополнительной информации о компоновке и компиляции, я нашел эти ресурсы полезными:
ОБНОВЛЕНИЕ: (в ответ на комментарии ниже)
То, что ответы могут быть разными, не означает, что измерения бесполезны. Вы должны начать измерять как некоторую точку. И чем больше у вас измерений, тем четче будет картина. В этом вопросе я прошу не всю историю, а проблеск картины. Конечно, любой может использовать числа, чтобы исказить аргумент, если он хотел неэтично продвигать свою предвзятость. Однако, если кому-то интересно узнать о различиях между двумя вариантами и опубликовать эти результаты, я думаю, что эта информация полезна.
Разве никто не интересовался этой темой, достаточно ли ее измерить?
Я люблю проект перестрелки. Мы могли бы начать с удаления большинства этих переменных. Используйте только одну версию gcc для одной версии linux. Используйте только одно и то же оборудование для всех тестов. Не компилировать с несколькими потоками.
Тогда мы можем измерить:
Резюме (заметные моменты):
Тест Box2D, данные:
Ботан тест, данные:
Box2D РЕЗЮМЕ (78 единиц)
ОБЗОР Ботана (301 шт.)
СЛАВНЫЕ ЧАРТЫ:
Размер исполняемого файла Box2D:
Box2D компиляция / ссылка / сборка / время выполнения:
Box2D компиляция / ссылка / сборка / запуск максимального использования памяти:
Размер исполняемого файла Botan:
Botan компиляция / ссылка / сборка / время выполнения:
Ботаническая компиляция / ссылка / сборка / запуск максимального использования памяти:
TL; DR
Проекты проверены, Box2D а также Ботан были выбраны потому, что они потенциально дорогостоящие в вычислительном отношении, содержат большое количество единиц и фактически имели мало ошибок или вообще не имели ошибок при компиляции как единое целое. Было предпринято много других проектов, но они занимали слишком много времени, чтобы «исправить» компиляцию как один модуль. Объем памяти измеряется путем опроса объема памяти через регулярные интервалы и с использованием максимума, и, следовательно, он может быть не совсем точным.
Кроме того, этот тест не выполняет автоматическую генерацию зависимостей заголовка (для обнаружения изменений заголовка). В проекте, использующем другую систему сборки, это может добавить время ко всем тестам.
В тесте 3 компилятора, каждый с 5 конфигурациями.
Составители:
Конфигурации компилятора:
-O3 -march=native
-Os
-O3 -flto -march=native
с лязгом и gcc, -O3 -ipo -march=native
с icpc / icc-Os
Я думаю, что каждый из них может иметь разные ориентиры при сравнении сборок из одного и нескольких блоков. Я включил LTO / IPO, чтобы мы могли увидеть, как сравнивается «правильный» способ достижения единичной эффективности.
Объяснение полей CSV:
Test Name
— название эталона. Примеры: Botan, Box2D
,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
— как они звучат.Чтобы воспроизвести критерии:
"units"
— список c/cpp/cc
файлы, которые составляют единицы этого проекта"executable"
— Имя исполняемого файла для компиляции."link_libs"
— Разделенный пробелами список установленных библиотек для ссылки."include_directores"
— Список каталогов для включения в проект."command"
— необязательный. специальная команда для запуска теста. Например, "command": "botan_test --benchmark"
test_base_cases
в run.py с информацией для проекта, включая имя файла данных.data.csv
должен содержать результаты тестов.Для создания гистограммы:
fields
список, чтобы решить, какие графики производить.python chart.py data.csv
,test.png
теперь должен содержать результат../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.Intel(R) Core(TM) i7 CPU Q 720 @ 1.60GHz
Обновить
Это был оригинальный ответ Реального Слава. Его ответ выше (принятый) является его второй попыткой. Я чувствую, что его вторая попытка полностью отвечает на вопрос. — Homer6
Что ж, для сравнения вы можете посмотреть на идею «единства сборки» (ничего общего с графическим движком). По сути, «единство сборки» — это то, где вы включаете все файлы cpp в один файл и компилируете их все как один модуль компиляции. Я думаю, что это должно обеспечить хорошее сравнение, поскольку AFAICT, это эквивалентно тому, чтобы сделать ваш проект только заголовком. Вы будете удивлены 2-м «жуликом», который вы перечислили; весь смысл «построения единства» заключается в снижение время компиляции. Предположительно, единство компилируется быстрее, потому что они:
.. являются способом сокращения накладных расходов на сборку (в частности, открытием и закрытием файлов и сокращением времени компоновки за счет сокращения числа создаваемых объектных файлов) и, как таковые, используются для существенного ускорения времени сборки.
Сравнение времени компиляции (от Вот):
Три основных ссылки на «Единство построения»:
Я полагаю, вам нужны причины плюсов и минусов.
Плюсы только для заголовка
[…]3) Это может быть намного быстрее. (Количественный)
Код может быть оптимизирован лучше. Причина в том, что, когда блоки разделены, функция является просто вызовом функции, и поэтому должна быть оставлена таковой. Информация об этом звонке неизвестна, например:
Кроме того, если функция внутреннего кода является известно, что может быть целесообразно встроить его (то есть выгрузить его код непосредственно в вызывающую функцию). Встраивание позволяет избежать накладных расходов на вызов функции. Встраивание также позволяет выполнять целый ряд других оптимизаций (например, постоянное распространение; например, мы называем factorial(10)
теперь, если компилятор не знает код factorial()
, он вынужден оставить это так, но если мы знаем исходный код factorial()
мы можем на самом деле переменные переменных в функции и заменить их на 10, и, если нам повезет, мы можем даже получить ответ во время компиляции, вообще ничего не запуская во время выполнения). Другие оптимизации после встраивания включают устранение мертвого кода и (возможно) лучшее предсказание ветвления.
4) Может дать компилятору / компоновщику лучшие возможности для оптимизации (объяснение / количественно, если возможно)
Я думаю, что это следует из (3).
Минусы только для заголовка
1) Раздувает код. (количественно) (как это влияет как на время выполнения, так и на объем памяти)
Только заголовки могут раздуть код несколькими способами, которые я знаю.
Первое — это раздувание шаблона; где компилятор создает ненужные шаблоны типов, которые никогда не используются. Это относится не только к заголовкам, но скорее к шаблонам, и современные компиляторы улучшили это, чтобы сделать его минимальным.
Второй, более очевидный способ — это (чрезмерное) встраивание функций. Если большая функция встроена везде, где она используется, эти вызывающие функции будут увеличиваться в размере. Возможно, это беспокоило размер исполняемых файлов и объем памяти исполняемых образов много лет назад, но пространство на жестком диске и объем памяти выросли, что делает его практически бесполезным. Более важная проблема заключается в том, что этот увеличенный размер функции может испортить кэш команд (так что теперь более крупная функция не помещается в кэш, и теперь кэш должен быть пополнен, поскольку ЦПУ выполняет эту функцию). Давление в регистре будет увеличиваться после встраивания (существует ограничение на количество регистры, оперативная память, с которой процессор может работать напрямую). Это означает, что компилятор должен будет манипулировать регистрами в середине теперь более крупной функции, потому что там слишком много переменных.
2) Более длительное время компиляции. (Количественный)
Что ж, компиляция только с заголовком может логически привести к более длительному времени компиляции по многим причинам (несмотря на производительность «сборок единства»; логика не обязательно является реальной, где задействованы другие факторы). Одной из причин может быть то, что если весь проект только для заголовков, мы теряем инкрементные сборки. Это означает, что любое изменение в любой части проекта означает, что весь проект должен быть перестроен, в то время как с отдельными модулями компиляции изменения в одном cpp просто означают, что объектный файл должен быть перестроен, а проект перекомпонован.
По моему (неподтвержденному) опыту, это большой успех. Только заголовки в некоторых особых случаях значительно повышают производительность, но с точки зрения производительности это обычно не стоит. Когда вы начинаете получать большую кодовую базу, время компиляции с нуля может занять> 10 минут каждый раз. Перекомпиляция на крошечном изменении начинает становиться утомительной. Вы не знаете, сколько раз я забыл «;» и мне пришлось ждать 5 минут, чтобы услышать об этом, только чтобы вернуться и исправить это, а затем подождать еще 5 минут, чтобы найти что-то еще, что я только что представил, исправив «;».
Производительность отличная, производительность намного лучше; это потратит большую часть вашего времени и лишит мотивации / отвлечет вас от вашей цели программирования.
Редактировать: Я должен отметить, что межпроцедурная оптимизация (смотрите также оптимизация времени соединения, а также оптимизация всей программы) пытается реализовать преимущества оптимизации «единства построения». Реализация этого все еще немного шатка в большинстве компиляторов AFAIK, но в конечном итоге это может преодолеть преимущества производительности.
Я надеюсь, что это не слишком похоже на то, что сказал Realz.
Размер исполняемого файла (/ объекта): (исполняемый файл 0% / объект увеличивается до 50% только в заголовке)
Я бы предположил, что определенные функции в заголовочном файле будут скопированы в каждый объект. Когда дело доходит до генерации исполняемого файла, я бы сказал, что довольно просто вырезать дублирующиеся функции (понятия не имею, какие компоновщики делают / не делают этого, я полагаю, большинство это делают), поэтому (вероятно) никакой реальной разницы в размер исполняемого файла, но хорошо в размер объекта. Разница должна в значительной степени зависеть от того, сколько кода на самом деле находится в заголовках по сравнению с остальной частью проекта. Не то, чтобы размер объекта действительно имел значение в наши дни, за исключением времени ссылки.
Время выполнения: (1%)
Я бы сказал, что в основном идентично (адрес функции — это адрес функции), за исключением встроенных функций. Я ожидаю, что встроенные функции будут иметь различие в вашей средней программе менее чем на 1%, потому что вызовы функций имеют некоторые накладные расходы, но это ничто по сравнению с накладными расходами на фактические действия с программой.
Объем памяти: (0%)
Те же вещи в исполняемом файле = тот же объем памяти (во время выполнения), при условии, что компоновщик удаляет дублирующиеся функции. Если дублирующиеся функции не вырезаны, это может иметь большое значение.
Время компиляции (как для всего проекта, так и путем изменения одного файла): (на целых до 50% быстрее для одного, одного на 99% быстрее для не только заголовка)
Огромная разница. Изменение чего-либо в заголовочном файле вызывает перекомпиляцию всего, что включает его, в то время как изменения в файле cpp просто требуют пересоздания этого объекта и повторной ссылки. И на 50% медленнее для полной компиляции только для библиотек с заголовками. Тем не менее, с предварительной компиляцией заголовков или сборок Unity полная компиляция с библиотеками только для заголовков, вероятно, будет быстрее, но одно изменение, требующее перекомпиляции большого количества файлов, является огромным недостатком, и я бы сказал, что это того не стоит , Полные перекомпиляции часто не нужны. Кроме того, вы можете включить что-то в файл cpp, но не в его заголовочный файл (это может часто случаться), поэтому в правильно спроектированной программе (древовидная структура зависимостей / модульность) при изменении объявления функции или чего-либо (всегда требуется изменения в файл заголовка), только заголовок может привести к перекомпиляции многих вещей, но с не только заголовком вы можете значительно ограничить это.
Время соединения: (до 50% быстрее только для заголовков)
Объекты, вероятно, больше, поэтому их обработка займет больше времени. Вероятно, линейно пропорционально тому, насколько больше файлы. Из моего ограниченного опыта в больших проектах (где время компиляции + ссылки достаточно велико, чтобы действительно иметь значение), время линковки практически ничтожно по сравнению со временем компиляции (если вы не будете вносить небольшие изменения и строить, тогда я ожидаю, что вы почувствуете это что, я полагаю, может случиться часто).