Я нахожусь в процессе изменения части своего приложения C ++ с использования более старого массива типа C на шаблонный класс контейнера C ++. См. этот вопрос, чтобы узнать подробности. Хотя решение работает очень хорошо, каждое небольшое изменение, которое я вношу в шаблонный код, вызывает очень большой объем перекомпиляции и, следовательно, резко замедляет время сборки. Есть ли способ получить код шаблона из заголовка и обратно в файл cpp, чтобы незначительные изменения реализации не вызывали серьезных перестроек?
Как сократить время компиляции с помощью шаблонов C ++
Ответы (6)
Я думаю, что применяются общие правила. Постарайтесь уменьшить взаимосвязь между частями кода. Разбейте слишком большие заголовки шаблонов на более мелкие группы функций, используемых вместе, чтобы все это не нужно было включать в каждый исходный файл.
Кроме того, постарайтесь быстро привести заголовки в стабильное состояние, возможно, протестируйте их с помощью небольшой тестовой программы, чтобы их не нужно было изменять (слишком много) при интеграции в более крупную программу.
(Как и в случае с любой другой оптимизацией, при работе с шаблонами, возможно, менее целесообразно оптимизировать скорость компилятора, чем искать «алгоритмическую» оптимизацию, которая в первую очередь резко снижает рабочую нагрузку.)
Несколько подходов:
- Ключевое слово export Теоретически a> мог бы помочь, но он плохо поддерживался и был официально удален в C ++ 11.
- Явное создание шаблона (см. здесь или здесь) - самый простой подход, если вы можете заранее предсказать, какие экземпляры вам понадобятся (и если вы не против вести этот список).
- шаблоны Extern, которые уже поддерживаются некоторыми компиляторами в качестве расширений. Насколько я понимаю, шаблоны extern не обязательно позволяют перемещать определения шаблонов из файла заголовка, но они ускоряют компиляцию и компоновку (за счет уменьшения количества экземпляров кода шаблона, которые необходимо создавать и связывать).
- В зависимости от дизайна вашего шаблона вы можете перенести большую часть его сложности в файл .cpp. Стандартный пример - это типобезопасный векторный шаблонный класс, который просто обертывает типизированный вектор
void*
; Вся сложность заключается в вектореvoid*
, который находится в файле .cpp. Скотт Мейерс приводит более подробный пример в Эффективный C ++ (пункт 42, «Разумно используйте частное наследование», во 2-м издании).
Прежде всего, для полноты, я расскажу о простом решении: используйте шаблонный код только тогда, когда это необходимо, и основывайте его на не шаблонном коде (с реализацией в собственном исходном файле).
Однако я подозреваю, что настоящая проблема заключается в том, что вы используете универсальное программирование, как если бы вы использовали типичное объектно-ориентированное программирование, и в итоге получили бы раздутый класс.
Возьмем пример:
// "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 ++, которые сообщают о включенных, но неиспользуемых файлах заголовков, которые могут помочь выяснить это.
Вы можете получить компилятор, поддерживающий ключевое слово export, но это не очень вероятно, продлится.
Вы можете использовать явное создание экземпляра, но, к сожалению, это требует от вас прогнозирования типы шаблонов, которые вы будете использовать заранее.
Если вы можете исключить шаблонные типы из своего алгоритма, вы можете поместить его в отдельный файл .cc.
Я бы не предлагал этого, если это не является серьезной проблемой, но: Вы можете предоставить интерфейс контейнера шаблона, который реализуется с помощью вызовов реализации
void*
, которую вы можете изменить по своему желанию.
export
будет удален в C ++ 0x. Не стоит даже думать об использовании сейчас.
- person pmr; 13.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 типов. Ручная специализация ускоряет компиляцию длинных списков.
Вы можете определить базовый класс без шаблонов и переместить туда большую часть реализации. Шаблонный массив тогда будет определять только прокси-методы, которые для всего используют базовый класс.