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

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

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

Мне было интересно, насколько правдоподобны эти утверждения (один о раздувании)?

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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 can только ссылку на одну версию Target.

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

Ответ 2

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

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

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

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

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

Ответ 3

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

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() { ... }

Ответ 4

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

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

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

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

// 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 во всех своих файлах.