Сокращение шаблона чрезмерно увеличивается в размерах с наследованием

У кого-либо есть опыт при сокращении чрезмерного увеличения размера кода шаблона посредством наследования?

Я колеблюсь, переписывая наши контейнеры этот путь:

class vectorBase
{
  public:
    int size();
    void clear();
    int m_size;
    void *m_rawData;
    //....
};

template< typename T > 
class vector : public vectorBase
{
    void push_back( const T& );
    //...

};

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

10
задан Jon Harper 29 October 2019 в 12:23
поделиться

7 ответов

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

Также мало что можно сделать с void * m_rawData , не зная типов вещей внутри него, в основном все операции с ним, по крайней мере, должны знать размер хранимого типа. . Единственное, о чем я могу думать, это то, что вы можете free () его, если знаете, что он не содержит элементов (если он содержит элементы, вы должны вызывать их деструкторы). Распределение, установка и доступ к элементам не работают, если вы не знаете, где начинаются и заканчиваются отдельные элементы. Также реализация всех методов была бы намного чище и проще, если бы вместо m_rawData был правильно набран T * .

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

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

3
ответ дан 4 December 2019 в 03:15
поделиться

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

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

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

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

1
ответ дан 4 December 2019 в 03:15
поделиться

IIRC, Qt использует (или использовали?) Аналогичный подход для своих QList et al.

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

Это определенно окупается, когда вы можете абстрагироваться от некоторого параметра шаблона (например, set :: iterator не должен знать о компараторе набора) или если вы можете приготовить полную реализацию для большого класса типов (например, с тривиальным copy-ctor и dtor).

0
ответ дан 4 December 2019 в 03:15
поделиться

Некоторые реализации действительно используют (форму) вышеупомянутого подхода. Вот GCC

  template<typename _Tp, typename _Alloc = std::allocator<_Tp> >
    class vector : protected _Vector_base<_Tp, _Alloc>
    {
    ... 
    }

. В этом случае цель состоит в том, чтобы делегировать управление памятью _Vector_base . Если вы все же решите тратить свое время на изобретение STL заново, сообщите о своих результатах здесь. Возможно, ваши усилия помогут положить конец старым воплям о «раздувании кода», которые время от времени слышны.

1
ответ дан 4 December 2019 в 03:15
поделиться

Вкратце:

Да, этот подход [вероятно] будет работать в ограниченных, специализированных обстоятельствах. Я не подозреваю, что std :: vector (или остальная часть STL) входит в число этих обстоятельств.

Вкратце:

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

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

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

Как указывалось в некоторых ответах, от std :: vector особо нечего абстрагироваться, что сделало бы его более справедливым в вашем примере. Конечно, вам нужно иметь возможность создавать и уничтожать отдельные элементы, а создание виртуальных методов снизит производительность во время выполнения (что более важно, чем производительность во время компиляции).Кроме того, компилятор потеряет возможность оптимизировать библиотеку void * для шаблонного кода, что может привести к большой потере производительности.

Меня больше интересуют более сложные структуры - выиграет ли std :: map от абстракции? Что, если бы вы удалили красно-черное дерево (реализация SGI) и соединили его с библиотекой красно-черного дерева. Вам (вероятно) потребуется хранить элементы вне std :: map , поэтому не нужно вызывать деструкторы, но это может (снова) вызвать потерю производительности во время выполнения из-за удваивая вашу косвенность.

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

Если бы вы лучше знали структуры данных, которые вы храните, или у вас был очень специфический шаблонный тип (не обязательно контейнер), вы, вероятно, могли бы улучшить свою производительность. Но тогда возникает вопрос - как часто вы будете использовать этот новый шаблонный тип, чтобы затраты на его компиляцию были заметно уменьшены? Конечно, это поможет время компиляции для std :: vector , но, возможно, не для my_domain_specific_ptr_container . Если вы сэкономите 50% времени компиляции для my_domain_specific_ptr_container , сколько раз вам придется использовать его, чтобы заметить достаточно значительный прирост сборки, чтобы оправдать добавленную сложность класса (и уменьшенные возможности отладки).

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

Если, с другой стороны, вы попробуете это, и это сработает для контейнеров STL ...пожалуйста, отправьте ответ. Я хочу более быструю сборку! ;)

0
ответ дан 4 December 2019 в 03:15
поделиться

Я понимаю ваш подход.

Честно говоря, я использовал его... хотя, конечно, не для контейнеров STL: их код практически не имеет ошибок и оптимизирован, и я вряд ли смогу самостоятельно придумать лучшую реализацию!

Меня мало волнует время компиляции: это неловко параллельная проблема (кроме link), а distcc и т.д. позаботятся обо всех проблемах, которые могут возникнуть даже с большой кодовой базой. И я имею в виду большую, я работаю в компании, которой потребовался новый компилятор от HP, потому что версия, которая у нас была, не поддерживала более 128Ko... в командной строке компоновщика. И это было только одно из приложений, и это было несколько лет назад, и с тех пор они, к счастью, разделили его на несколько частей".

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

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

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

Однако я бы точно не стал использовать наследование для этой работы. Наследование - это IS-A отношения: оно определяет интерфейс, а не реализацию. Либо используйте Composition, либо просто свободные функции, которые вы храните в пространстве имен утилиты (detail, как в Boost ?).

1
ответ дан 4 December 2019 в 03:15
поделиться

Приведенный вами код просто неверен. Если класс, который вы храните в векторе, имеет деструктор, то этот деструктор не будет вызван, потому что компилятор vectorBase потерял всю информацию о том, когда вызывать деструктор, приведя его к void*.

Чтобы сделать это правильно, вызвав нужный деструктор, нужно сгенерировать разные копии кода, каждая из которых вызывает нужный деструктор, и эта работа упрощается при использовании шаблонов.

(Чтобы использовать ваш подход с нешаблонным базовым классом, вам нужно сгенерировать столько же машинного кода, но вам нужно написать гораздо больше C++ кода вручную.)

Вот почему этот подход действительно не имеет смысла.

0
ответ дан 4 December 2019 в 03:15
поделиться
Другие вопросы по тегам:

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