Как сократить время компиляции с помощью шаблонов C ++

Я нахожусь в процессе изменения части своего приложения C ++ с использования более старого массива типа C на шаблонный класс контейнера C ++. См. этот вопрос, чтобы узнать подробности. Хотя решение работает очень хорошо, каждое небольшое изменение, которое я вношу в шаблонный код, вызывает очень большой объем перекомпиляции и, следовательно, резко замедляет время сборки. Есть ли способ получить код шаблона из заголовка и обратно в файл cpp, чтобы незначительные изменения реализации не вызывали серьезных перестроек?


person SmacL    schedule 13.05.2010    source источник


Ответы (6)


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

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

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

person UncleBens    schedule 13.05.2010
comment
+100 Вы не тестируете шаблон в большом несвязанном проекте. Шаблон должен быть как можно более свободным от ошибок, прежде чем он попадет в несвязанный проект. - person jmucchiello; 13.05.2010
comment
Упс, не видел вашего ответа до того, как я опубликовал свой, хотя, думаю, я был немного более откровенен ... на пути к отметке 10k;)? - person Matthieu M.; 13.05.2010
comment
+1, и примерно такой вывод, к которому я пришел. Просто немного поленился разбить код на меньшую тестовую программу;) - person SmacL; 13.05.2010

Несколько подходов:

  • Ключевое слово export мог бы помочь, но он плохо поддерживался и был официально удален в C ++ 11.
  • Явное создание шаблона (см. здесь или здесь) - самый простой подход, если вы можете заранее предсказать, какие экземпляры вам понадобятся (и если вы не против вести этот список).
  • шаблоны Extern, которые уже поддерживаются некоторыми компиляторами в качестве расширений. Насколько я понимаю, шаблоны extern не обязательно позволяют перемещать определения шаблонов из файла заголовка, но они ускоряют компиляцию и компоновку (за счет уменьшения количества экземпляров кода шаблона, которые необходимо создавать и связывать).
  • В зависимости от дизайна вашего шаблона вы можете перенести большую часть его сложности в файл .cpp. Стандартный пример - это типобезопасный векторный шаблонный класс, который просто обертывает типизированный вектор void*; Вся сложность заключается в векторе void*, который находится в файле .cpp. Скотт Мейерс приводит более подробный пример в Эффективный C ++ (пункт 42, «Разумно используйте частное наследование», во 2-м издании).
person Josh Kelley    schedule 13.05.2010
comment
Разумно использовать частное наследование - 39 в моем экземпляре (3-е издание), но спасибо за указатель. Мне действительно следует перечитать две эффективные книги Майерса еще раз. - person SmacL; 13.05.2010
comment
Эффективное третье издание C ++ очень непохоже на новое. По сути, это новая книга, в которую вошли несколько наиболее актуальных статей из выпусков 1 и 2. - person deft_code; 13.05.2010

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

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

Возьмем пример:

// "bigArray/bigArray.hpp"

template <class T, class Allocator>
class BigArray
{
public:
  size_t size() const;

  T& operator[](size_t index);
  T const& operator[](size_t index) const;

  T& at(size_t index);
  T const& at(size_t index);

private:
  // impl
};

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

// "bigArray/at.hpp"

template <class Container>
typename Container::reference_type at(Container& container,
                                      typename Container::size_type index)
{
  if (index >= container.size()) throw std::out_of_range();
  return container[index];
}

template <class Container>
typename Container::const_reference_type at(Container const& container,
                                            typename Container::size_type index)
{
  if (index >= container.size()) throw std::out_of_range();
  return container[index];
}

Хорошо, это немного меняет вызов:

// From
myArray.at(i).method();

// To
at(myArray,i).method();

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

Пример надуманный, но общая мысль остается в силе. Обратите внимание, что из-за его универсальности at.hpp никогда не приходилось включать bigArray.hpp, и он по-прежнему будет производить такой жесткий код, как если бы это был метод-член, просто мы можем вызывать его в других контейнерах, если захотим.

И теперь пользователю BigArray не нужно включать at.hpp, если он не использует его ... таким образом уменьшая ее зависимости и не затрагиваясь, если вы измените код в этом файле: например, измените вызов std::out_of_range, чтобы указать имя файла и номер строки, адрес контейнера, его размер и индекс, к которому мы пытались получить доступ.

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

Это рекомендуется многими авторами, такими как Херб Саттерс в Кодировании на C ++. Стандарты:

Правило 44: Предпочитайте писать функции, не являющиеся членами группы, и не являющиеся друзьями.

и широко использовался в Boost ... Но вам нужно изменить свои привычки кодирования!

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

person Matthieu M.    schedule 13.05.2010
comment
Спасибо за ответ, и вы совершенно правы насчет раздутого кода шаблона. Первоначально я основывал его на MFC CArray, который, как я с тех пор обнаружил, страдает болезненным ожирением! Какой статический анализатор кода отображает неиспользуемые заголовки? В настоящее время я использую PC-LINT и не видел этой функции. - person SmacL; 14.05.2010
comment
Я думал, что это обычное дело, но боюсь, что перепутал это с другим языком: / Это странно, потому что (например) даже раскраситель C ++ из Eclipse проверяет наличие перегрузок (и раскрашивает только при обнаружении правильной перегрузки). извините за то, что ввёл вас в заблуждение. - person Matthieu M.; 14.05.2010
comment
Я знаю этот трюк, и его тоже рекомендует Скотт Мейерс. Однако я думаю, что это негативно скажется на читабельности вашего кода. Также визуализаторы, такие как Visual Studio, не используют глобальные функции, и они также могут ускорить вашу разработку. Граф ускорения почти полностью разработан таким образом (т.е. бесплатные функции, работающие с концепциями абстрактного графа), и, хотя он очень гибкий и мощный, новичкам очень сложно в нем разобраться. - person gast128; 10.03.2014
comment
@ gast128: Думаю, это действительно зависит от твоего языка. Если вы пришли из; ОО - единственная парадигма, в которой функция привязана к объекту, поэтому это трудно понять людям, которые ранее сталкивались с ОО и не знакомы (или мало) с другими парадигмами (процедурными, функциональными, ...) - person Matthieu M.; 10.03.2014

  • Вы можете получить компилятор, поддерживающий ключевое слово export, но это не очень вероятно, продлится.

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

  • Если вы можете исключить шаблонные типы из своего алгоритма, вы можете поместить его в отдельный файл .cc.

  • Я бы не предлагал этого, если это не является серьезной проблемой, но: Вы можете предоставить интерфейс контейнера шаблона, который реализуется с помощью вызовов реализации void*, которую вы можете изменить по своему желанию.

person Stephen    schedule 13.05.2010
comment
export будет удален в C ++ 0x. Не стоит даже думать об использовании сейчас. - person pmr; 13.05.2010
comment
+1 для явного создания экземпляра, я не сталкивался с этим раньше и думаю, что это может очень помочь. - person SmacL; 14.05.2010

Использование шаблонов в качестве метода решения проблем может замедлить компиляцию. Классическим примером этого является функция std :: sort vs. qsort из C. Версия этой функции для C ++ требует больше времени для компиляции, потому что ее нужно анализировать в каждой единице перевода и потому что почти каждое использование этой функции создает другой экземпляр. этого шаблона (при условии, что типы закрытия обычно предоставляются как предикат сортировки).

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

Правило Чиля

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

Следующие функции / конструкции C ++ отсортированы в порядке убывания времени компиляции:

  • СФИНАЕ
  • Создание экземпляра шаблона функции
  • Создание экземпляра типа
  • Вызов псевдонима
  • Добавление параметра к типу
  • Добавление параметра к вызову псевдонима
  • Поиск заученного шрифта

Оптимизация, основанная на вышеуказанных правилах, использовалась при проектировании и разработке Boost.TMP. По возможности избегайте верхних конструкций для быстрой компиляции шаблона.

Ниже приведены несколько примеров, иллюстрирующих, как использовать правила, перечисленные выше.

Уменьшите количество экземпляров шаблонов

Давайте посмотрим на std :: conditional. Его декларация:

template< bool B, typename T, typename F >
struct conditional;

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

struct first{};
struct second{};

Теперь все нижеприведенное превратится в экземпляры разных типов:

using type1 = conditional<true, first, second>;
using type2 = conditional<true, second, first>;
std::is_same_v<type1, type2>; // it’s false

using type3 = conditional<false, first, second>;
using type4 = conditional<false, second, first>;
std::is_same_v<type1, type2>; // it’s false

Мы можем уменьшить количество экземпляров, изменив реализацию условного выражения на:

template <bool>
struct conditional{
     template <typename T, typename F>
     using type = T;
};

template <>
struct conditional<false>{
     template <typename T, typename F>
     using type = F;
};

В этом случае компилятор создаст только два экземпляра типа «условный» для всех возможных аргументов. Подробнее об этом примере читайте в выступлении Одина Холмса о библиотеке Kvasir.

Создание явных экземпляров шаблона

Если вы подозреваете, что экземпляр шаблона будет часто использоваться, рекомендуется создать его явно. Обычно std::string является явным экземпляром std::basic_string<char>.

Создание специализаций для алгоритмов времени компиляции

Квасир-МПЛ специализирует алгоритмы для длинных списков типов, чтобы ускорить их. Вы можете увидеть пример здесь. В этом заголовочном файле алгоритм сортировки вручную специализирован для списка из 255 типов. Ручная специализация ускоряет компиляцию длинных списков.

person asafk_IB    schedule 17.09.2020

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

person AareP    schedule 13.05.2010