如何减少大型模板的编译时内存占用?

时间:2014-10-17 00:08:51

标签: c++ templates

假设我有一个包含个其他类声明的类。是否有可能以某种方式分摊这些成本,以便编译时内存消耗不会对嵌套类型以二次方式增长?如果需要,我愿意在编译时受到打击,如果可以选择,我很乐意将其分成不同的翻译单元。

为了尝试找到解决方案,我已经编写了以下程序,该程序说明了导致这些井喷的代码类型的简化版本:

// 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;                                                                   
}

正如Yakk所指出的那样,这些Append行动非常低效。但是,在此代码的实际版本中,修改这些代码需要对代码进行基本的重组。

我将N从10改为70,并使用GCC 4.8.1将此结果编译为我的程序。我还运行time -v make以获得峰值驻留内存使用率。以下是仅使用默认标志的结果:

N^3 nested tuple compile-time memory usage

这个结果对我来说似乎过分,不是因为形状(预计是O(N ^ 3)并且似乎遵循那个形状),而是因为它的大小。对于 Big 的每个实例化而言,Small似乎正在展开,并且 Big 正在针对每个实例化进行扩展。 巨大。在模板量较少的代码中,通常会使用extern关键字声明某种类型的泛型特化,因此会避免这种&#34;嵌套扩展&#34;,但这些是类型 ,而不是;这种结构是否存在类似的东西?

内存爆裂的原因是什么?如果不更改SmallBig和{{1}的类型,我可以采取哪些措施来减少内存占用 }?

3 个答案:

答案 0 :(得分:3)

Append生成Seq<T> Seq<T,T> ... Seq<T,...,T>。哪个问题少于它生成Append<n-1, T, Seq<T>>的事实,这是每个N 每个递归的独特且唯一的类型。

它生成N个唯一类型的总名称长度O(n^2*|T|),输出类型的大小为O(n*|T|)

然后我们将其链接起来。

Big生成总大小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(2500万)总类型长度。

我们可以用较少的脑死亡来做得更好。分三步完成。

transcribe需要t<Ts...>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并将其分解为2的幂,输出 v<0,1,3,4>的{​​{1}}(2^0+2^1+2^3+2^4)。

26

Build采用无符号值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; N。它TBreak。然后我们将两个人的力量建立到N s。然后我们追加它们。

然后我们获取输出并转录到t<T,T,T,...,T>

这会生成Seq<...>种类型。所以对于大N可能会更好。此外,生成的大多数类型都很小而且简单,这是一个优势。

充其量这会使你的负荷减少10倍。值得一试。

答案 1 :(得分:1)

同意 - 查看代码,它似乎具有N ^ 3的复杂性。

我不认为那里的编译器非常聪明,可以找出&#34; base&#34;巨大的一类,将与小的一样。编译器实际上必须解决这个问题,从&#34;自下而上&#34;开始,以一种说法,弄清楚Huge中的内容。一旦完成,它将发现基类将是相同的,但我不认为聪明人将会在那里找到它,直到开始。因此,它必须烧毁内存和CPU,才能得出结论。

如果不是O(N ^ 3)复杂度,该图似乎显示至少O(N ^ 2)。毫无疑问,其中一些与模板有关。编译器在模板方面几乎没有余地。如果你要编制N和N * 2普通类声明所需的时间和内存,我敢打赌,观察到的复杂性不会是2 ^ 3,而是线性的。

答案 2 :(得分:1)

在编辑之后进行编辑:整个问题的实现定义的性能让我反击。实际上,我只看到下面提到的clang的改进。我只是用g ++ 4.8.2尝试了同样的事情,并且编译时和内存使用与clang的改进值相当(无论我是使用继承还是原始类型定义)。例如,N = 70只需要大约3GB的内存,而不是OP的情况下的12GB内存。因此,对于g ++,我的建议实际上并不是一种改进。

编辑:从我下面的原始答案中可以看出,通过在每个级别引入一个新类,可以防止完整的嵌套扩展,其中下一个嵌套级别只是一个成员变量。但我刚刚发现同样的东西也适用于继承。类型SmallBigHuge并未完全保留。您丢失了类型标识,但保留了功能(运行时)等效。所以,这比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 to”不改变Small,Big,Huge的逻辑结构“,你可以通过使用类来减少编译时间,其中嵌套类型是成员,而不是只是嵌套类型本身。我想这是因为,在这种情况下,编译器实际上不必嵌套类型。在每个级别上,元组成员只包含许多“嵌套&lt; [...]&gt;”类型,编译器不能/不需要进一步扩展。当然,这是有代价的:初始化的特殊方式,访问级别的特殊方式(基本上每个调用都附加“.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;
}

(当然,如果您希望不同级别具有不同的结构,也可能有单独的Small,Big,Huge类而不是通用模板Nested。它仅用于演示目的。)