假设我有一个包含大个其他类声明的类。是否有可能以某种方式分摊这些成本,以便编译时内存消耗不会对嵌套类型以二次方式增长?如果需要,我愿意在编译时受到打击,如果可以选择,我很乐意将其分成不同的翻译单元。
为了尝试找到解决方案,我已经编写了以下程序,该程序说明了导致这些井喷的代码类型的简化版本:
// 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
以获得峰值驻留内存使用率。以下是仅使用默认标志的结果:
这个结果对我来说似乎过分,不是因为形状(预计是O(N ^ 3)并且似乎遵循那个形状),而是因为它的大小。对于 Big 的每个实例化而言,Small
似乎正在展开,并且 Big 正在针对每个实例化进行扩展。 巨大。在模板量较少的代码中,通常会使用extern
关键字声明某种类型的泛型特化,因此会避免这种&#34;嵌套扩展&#34;,但这些是类型 ,而不是值;这种结构是否存在类似的东西?
内存爆裂的原因是什么?如果不更改Small
,Big
和{{1}的类型,我可以采取哪些措施来减少内存占用 }?
答案 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
。它T
是Break
。然后我们将两个人的力量建立到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 ++,我的建议实际上并不是一种改进。
编辑:从我下面的原始答案中可以看出,通过在每个级别引入一个新类,可以防止完整的嵌套扩展,其中下一个嵌套级别只是一个成员变量。但我刚刚发现同样的东西也适用于继承。类型Small
,Big
和Huge
并未完全保留。您丢失了类型标识,但保留了功能(运行时)等效。所以,这比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
。它仅用于演示目的。)