Когда библиотеки только для заголовка приемлемы?

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

Я задавался вопросом, сколько истина там к этим требованиям (то о чрезмерном увеличении размера)?

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

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

P.S. Да это - очень неопределенный и субъективный вопрос, я знаю, и таким образом, я отметил его как таковой.

26
задан TemplateRex 28 February 2012 в 11:11
поделиться

4 ответа

Я работаю в компании, у которой есть собственный отдел "Middleware" для обслуживания нескольких сотен библиотек, которые обычно используются большим количеством команд.

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

По общему мнению, выигрыш в производительности (если он есть) не будет стоить проблем.

Более того, так называемый "code-bloat" может оказать негативное влияние на производительность, так как большее количество кода, загружаемого в кэш, подразумевает большее количество промахов в кэше, и это убийцы производительности.

В идеальном мире я полагаю, что компилятор и компоновщик могут быть достаточно умны, чтобы НЕ генерировать эти правила "множественных определений", но пока это не так, я (лично) предпочту:

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

Почему бы вам не протестировать ? Подготовьте две библиотеки (один только заголовок, а другой без встраивания методов на пару строк) и проверить их соответствующую производительность в ВАШЕМ случае.

EDIT:

Это было указано на "jalf" (спасибо), что я должен уточнить, что я имел в виду именно бинарную совместимость.

2 версии данной библиотеки называются бинарной совместимостью, если вы можете (обычно) соединяться с той или иной без каких-либо изменений вашей собственной библиотеки.

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

MyLib --> Lib1 (v1), Lib2 (v1)
Lib1 (v1) --> Target (v1)
Lib2 (v1) --> Target (v1)

Теперь, скажем, что нам нужно исправление в Target для свойства, используемого только в Lib2, мы поставляем новую версию (v2). Если (v2) является бинарной совместимой с (v1), то мы можем сделать:

Lib1 (v1) --> Target (v2)
Lib2 (v1) --> Target (v2)

Однако если это не так, то мы сделаем это:

Lib1 (v2) --> Target (v2)
Lib2 (v2) --> Target (v2)

Да, вы правильно прочитали, несмотря на то, что Lib1 не требовало исправления, вы направляетесь на перестройку с новой версией Target, потому что эта версия обязательна для обновленной Lib2, а Executable может ссылаться только на одну версию Target.

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

7
ответ дан 28 November 2019 в 17:25
поделиться

Over-inflining - это, вероятно, то, к чему следует обратиться вызывающему абоненту, настраивая его опции компилятора, а не вызывающему абоненту, пытающемуся управлять им с помощью очень грубых инструментов ключевого слова inline и определений в заголовках. Например, GCC имеет -finline-limit и друзей, поэтому вы можете использовать разные правила встраивания для разных единиц перевода. То, что для вас является перевыполнением, может не быть перевыполнением для меня, в зависимости от архитектуры, размера и скорости кэша инструкций, того, как используется функция и т.д. Не то, чтобы мне когда-либо приходилось делать такую настройку: на практике, когда об этом стоило беспокоиться, стоило переписывать, но это может быть совпадением. В любом случае, если я являюсь пользователем библиотеки, то при прочих равных условиях я бы предпочёл иметь возможность встраивать (с учётом моего компилятора, и который я, возможно, не возьму на себя), чем быть неспособным к встраиванию.

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

Честно говоря, я не знаю, насколько обоснован этот страх. Всё, над чем я работал, либо было настолько сильно ограничено в памяти, что мы использовали inline только как функции static inline настолько маленькие, что мы не ожидаем, что inlined-версия будет заметно больше, чем код для вызова, и не возражаем против дубликатов, либо настолько слабо ограничены, что нас не волнуют никакие дубликаты нигде. Я еще никогда не попадал на середину пути поиска и подсчета дубликатов на разных компиляторах. Иногда я слышал от других, что это проблема с кодом шаблонов, так что я верю, что в утверждениях есть правда.

Придумывая это сейчас, я думаю, что если вы поставляете библиотеку только для заголовков, пользователь всегда может испортить её, если она ему не нравится. Напишите новый заголовок, объявляющий все функции, и новый блок перевода, включающий определения. Функции, определённые в классах, должны будут быть перемещены во внешние определения, так что если вы хотите поддержать это использование без необходимости вилки вашего кода пользователем, вы можете избежать этого и предоставить два заголовка:

// declare.h
inline int myfunc(int);

class myclass {
    inline int mymemberfunc(int);
};

// define.h
#include "declare.h"
int myfunc(int a) { return a; }

int myclass::mymemberfunc(int a) { return myfunc(a); }

Вызовы, которые беспокоятся о раздутом коде, вероятно, могут перехитрить свой компилятор, включив declare.h во все свои файлы, а затем написав:

// define.cpp
#include "define.h"

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

Абоненты, которые не беспокоятся о раздувании кода, могут использовать файл define.h во всех своих файлах.

1
ответ дан 28 November 2019 в 17:25
поделиться

По моему опыту, раздувание не было проблемой:

  • Библиотеки только для заголовков дают компиляторам большую возможность встраивания, но они не заставляют компиляторы встраиваться - многие компиляторы рассматривают ключевое слово inline как не более чем команду игнорировать несколько одинаковых определений.

  • У компиляторов обычно есть параметры для оптимизации, чтобы контролировать объем встраивания; / Os на компиляторах Microsoft.

  • Обычно лучше позволить компилятору решать вопросы, связанные со скоростью и размером. Вы увидите раздувание только тех вызовов, которые фактически были встроены, и компилятор будет встраивать их только в том случае, если его эвристика показывает, что встраивание улучшит производительность.

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

6
ответ дан 28 November 2019 в 17:25
поделиться

Согласен, встроенные библиотеки намного проще использовать.

Inline bloat в основном зависит от платформы разработки, с которой вы работаете - в частности, от возможностей компилятора / компоновщика. Я бы не ожидал, что это будет большой проблемой с VC9, за исключением нескольких угловых случаев.

Я видел некоторые заметные изменения в окончательном размере в некоторых местах большого проекта VC6, но трудно дать конкретный "приемлемый, если...". Вам нужно попробовать с вашим кодом в вашем devenv.

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

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


Я увидел следующий механизм, дающий пользователю возможность выбора:

// foo.h
#ifdef MYLIB_USE_INLINE_HEADER
#define MYLIB_INLINE inline
#else 
#define MYLIB_INLINE 
#endif

void Foo();  // a gazillion of declarations

#ifdef MYLIB_USE_INLINE_HEADER
#include "foo.cpp"
#endif

// foo.cpp
#include "foo.h"
MYLIB_INLINE void Foo() { ... }
3
ответ дан 28 November 2019 в 17:25
поделиться
Другие вопросы по тегам:

Похожие вопросы: