Когда допустимы только библиотеки заголовков? - PullRequest
26 голосов
/ 01 февраля 2010

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

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

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

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

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

Ответы [ 4 ]

7 голосов
/ 01 февраля 2010

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

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

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

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

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

  • двоичная совместимость
  • без встраивания (для методов, состоящих из нескольких строк)

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

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.

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

6 голосов
/ 01 февраля 2010

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

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

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

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

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

3 голосов
/ 01 февраля 2010

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

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() { ... }
1 голос
/ 01 февраля 2010

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

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

Честно говоря, я не знаю, насколько обоснован этот страх. Все, над чем я работал, либо было настолько сильно ограничено в памяти, что мы использовали 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 во всех своих файлах.

...