Как я могу уменьшить объем памяти во время компиляции больших шаблонов?
Предположим, что у меня есть класс, который содержит большое количество других объявлений класса. Возможно ли каким-либо образом расходовать их стоимость таким образом, чтобы потребление памяти во время компиляции не увеличивалось квадратично для вложенных типов? Я готов принять удар по времени компиляции, если это необходимо, и с удовольствием разделил бы это на разные единицы перевода, если бы это был вариант.
Чтобы попытаться найти решение, я написал следующую программу, которая иллюстрирует упрощенную версию кода, которая приводит к этим выбросам:
// Add T to the list of args of SeqWithArgs N number of times:
template <int N, typename T, typename SeqWithArgs>
struct Append;
template <int N, typename T, template <typename...> class Seq, typename... Args>
struct Append<N, T, Seq<Args...>>
{
using type = typename Append<N-1, T, Seq<Args..., T>>::type;
};
template <typename T, template<typename...> class Seq, typename... Args>
struct Append<0, T, Seq<Args...>>
{
using type = Seq<Args...>;
};
static constexpr const int N = 10;
// Tuple containing N instances of int
using Small = typename Append<N, int, std::tuple<>>::type;
// Tuple containing N instances of Small
using Big = typename Append<N, Small, std::tuple<>>::type;
// Tuple containing N instances of Big
using Huge = typename Append<N, Big, std::tuple<>>::type;
int main()
{
Huge h;
return 0;
}
Как уже отмечалось Якком, эти операции Append
ужасно неэффективны. Однако в реальной версии этого кода для их модификации потребуется фундаментальная реструктуризация кода.
Я изменил N
с 10 до 70 и получил этот результат, компилируя мою программу, используя GCC 4.8.1
. Я также запускал time -v make
для получения максимального использования памяти резидентной памяти. Вот результаты, используя только флаги по умолчанию:
![N^3 nested tuple compile-time memory usage]()
Этот результат кажется чрезмерным для меня, а не из-за формы (ожидается, что это O (N ^ 3) и, похоже, следует этой форме), но из-за величины. Похоже, что Small
расширяется для каждого экземпляра Big, а Big в свою очередь расширяется для каждого экземпляра Huge. В менее тяжелом коде кода обычно объявляется общая специализация какого-либо типа с использованием ключевого слова extern
и поэтому избегает этого "вложенного расширения", но это типы, а не значения; что-то подобное существует для такого типа конструкции?
В чем причина этого выброса в памяти и что я могу сделать для уменьшения этого объема памяти без изменения типа Small
, Big
и Huge
?
Ответы
Ответ 1
Append
генерирует Seq<T>
Seq<T,T>
... Seq<T,...,T>
. Это меньше проблемы, чем то, что он генерирует Append<n-1, T, Seq<T>>
, который является отдельным и уникальным типом для каждого N
и каждой рекурсии.
Он генерирует N
уникальные типы общей длины имени O(n^2*|T|)
, а тип вывода имеет размер O(n*|T|)
.
Затем мы свяжем это.
Большой тип генерирует общий размер O(n^2*O(n*|int|))
с размером конечного типа O(n^2|int|)
. Огромные типы размеров O(n^2*O(n^2|int|))=O(n^4|int|)
.
Это много типов.
70 ^ 4 = 5000 ^ 2 = O (25 миллионов) общая длина типа.
Мы можем сделать лучше с мертвым мертвым Append. Сделайте это в три этапа.
transcribe
принимает a t<Ts...>
и a template<class...>class Seq
и копирует Ts...
.
template<class...>struct t{using type=t;};
template<class src, template<class...>class dest>
struct transcribe;
template<class...Ts, template<class...>class dest>
struct transcribe<t<Ts...>,dest>{
using type=dest<Ts...>;
};
template<class src, template<class...>class dest>
using transcribe_t=typename transcribe<src, dest>::type;
Append
принимает любое число t<...>
и добавляет их.
template<class... ts>
struct append;
template<>struct append<>{using type=t<>;};
template<class...Ts>struct append<t<Ts...>>{using type=t<Ts...>;};
template<class... Ts, class... Us, class... Zs>
struct append<t<Ts...>,t<Us...>,Zs....>:
append<t<Ts...,Us...>,Zs...>
{};
template<class...ts>
using append_t=typename append<ts...>::type;
breaker
принимает значение без знака N
и разрывает его до двух значений, выводя
v<0,1,3,4>
(2^0+2^1+2^3+2^4
) для 26
.
template<unsigned...>struct v{using type=v;};
template<unsigned X, class V=v<>, unsigned B=0, class=void>
struct breaker;
template<unsigned X, unsigned...vs, unsigned B>
struct breaker<X, v<vs...>, B, typename std::enable_if<
X&(1<<B)
>::type>:breaker<X&~(1<<B), v<vs...,B>, B+1>
{};
template<unsigned X, unsigned...vs, unsigned B>
struct breaker<X, v<vs...>, B, typename std::enable_if<
!(X&(1<<B))
>::type>:breaker<X&~(1<<B), v<vs...>, B+1>
{};
template<unsigned X, unsigned...vs, unsigned B>
struct breaker<0, v<vs...>, B, void>
{
using type=v<vs...>;
};
template<unsigned X>
using breaker_t=typename breaker<X>::type;
Сборка принимает значение unsigned N
и T
. Это Break
N
. Затем мы создаем мощность двух в t<T,T,T,...,T>
s. Затем добавим их.
Затем мы берем вывод и записываем на Seq<...>
.
Это генерирует типы O(N*logN*logN)
. Таким образом, для больших N может быть лучше. Кроме того, большинство генерируемых типов небольшие и простые, что является плюсом.
В лучшем случае это уменьшает нагрузку в 10 раз. Стоит попробовать.
Ответ 2
Согласованный - глядя на код, он, похоже, имеет сложности N ^ 3.
Я не думаю, что компилятор достаточно умный, чтобы понять, что "базовый" класс Огромный, будет таким же, как у Small's. Компилятор действительно должен будет его обработать, начиная с "снизу вверх", в манере говорить, выясняя, что в Огромном. Когда это будет сделано, выяснится, что базовые классы будут одинаковыми, но я не думаю, что умники будут здесь, чтобы понять это, прямо с самого начала. Таким образом, для достижения этого нужно будет сжечь память и процессор.
График, по-видимому, показывает, по крайней мере, O (N ^ 2), если не O (N ^ 3) сложность. Некоторые из них, несомненно, связаны с шаблонами. Составители имеют мало возможностей, когда дело доходит до шаблонов. Если бы вы собрали время и память для компиляции деклараций простого класса N и N * 2, я буду держать пари, что наблюдаемая сложность будет не 2 ^ 3, а линейной.
Ответ 3
РЕДАКТИРОВАТЬ после приведенного ниже EDIT: определенная реализацией вся проблема просто ударила меня назад. На самом деле, я вижу только улучшения, упомянутые ниже для clang. Я просто пробовал то же самое с g++ 4.8.2, и там время компиляции и использование памяти сравнимы с улучшенными значениями с clang (независимо от того, использую ли я наследование или исходные определения типов). Например, для N = 70
требуется только 3 ГБ памяти, а не 12 ГБ, как в случае OP. Итак, для g++ мое предложение на самом деле не является улучшением.
EDIT: из моего первоначального ответа ниже было ясно, что полное вложенное расширение может быть предотвращено путем введения нового класса на каждом уровне, где следующий уровень вложенности является только переменной-членом. Но я только что открыл то же самое, что и работает с наследованием. Типы Small
, Big
и Huge
не полностью сохранены. Вы теряете идентификатор типа, но сохраняете функциональную (run-time) эквивалентность. Таким образом, это намного ближе к тому, чего хочет OP, чем трюк участника ниже. С clang это сократило время компиляции для случая N=40
примерно в 7 раз. Не уверен, изменит ли он масштаб. Вот код:
template<typename T>
struct MyType : Append<N, T, std::tuple<>>::type {
typedef typename Append<N, T, std::tuple<>>::type Base;
using Base::Base;
using Base::operator=;
};
int main()
{
MyType<MyType<MyType<int>>> huge;
//You can work with this the same way as with the nested tuples:
std::get<0>(std::get<0>(std::get<0>(huge)));
return 0;
}
Основная идея такая же, как и для трюка участника ниже: давая вещь на каждом уровне новое имя, которое не нуждается/не может быть расширено до самого низкого уровня (в отличие от простого typedef или использования объявления), "вложенность" уменьшается.
Оригинальный ответ:
Итак, по-видимому, компилятор действительно определяет внутренние типы снова и снова (в отличие от того, что я изначально сказал в комментариях), в противном случае следующее не будет работать: если вы расслабляете свое состояние "не меняя типы Small, Big, Huge" to "не меняют логическую структуру Small, Big, Huge", вы можете значительно сократить время компиляции, имея класс, в котором вложенный тип является членом, вместо того, чтобы просто вставлять сами типы. Я предполагаю, что, поскольку в этом случае компилятору фактически не нужно вставлять типы. На каждом уровне член кортежа просто состоит из нескольких типов "Nested < [...] > ", которые компилятор не может/не нуждается в дальнейшем расширении. Конечно, это происходит за счет: специальный способ инициализации, специальный способ доступа к уровням (в основном добавление ".member" к каждому вызову) и т.д.
#include <tuple>
// Add T to the list of args of SeqWithArgs N number of times:
template <int N, typename T, typename SeqWithArgs>
struct Append;
template <int N, typename T, template <typename...> class Seq, typename... Args>
struct Append<N, T, Seq<Args...>>
{
using type = typename Append<N-1, T, Seq<Args..., T>>::type;
};
template <typename T, template<typename...> class Seq, typename... Args>
struct Append<0, T, Seq<Args...>>
{
using type = Seq<Args...>;
};
static constexpr const int N = 40;
template<typename T>
struct Nested {
typename Append<N, T, std::tuple<>>::type member;
};
int main()
{
Nested<Nested<Nested<int>>> huge;
//Access is a little verbose, but could probably
//be reduced by defining clever template
//"access classes/functions"
std::get<0>(std::get<0>(std::get<0>(huge.member).member).member);
return 0;
}
(Конечно, также могут быть отдельные Малые, Большие, Огромные классы вместо общего шаблона Nested
, если вы хотите, чтобы разные уровни имели другую структуру. Это просто для демонстрационных целей.)