Как я могу уменьшить объем памяти во время компиляции больших шаблонов?

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

Чтобы попытаться найти решение, я написал следующую программу, которая иллюстрирует упрощенную версию кода, которая приводит к этим выбросам:

// 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, если вы хотите, чтобы разные уровни имели другую структуру. Это просто для демонстрационных целей.)