链接模板类时出现编译性能问题



我编写了一组模板实用程序,允许创建类型的编译时列表,并以函数式编程的风格对其进行操作。

代码已经工作了,但我对它的接口并不满意(下面解释),所以我尝试重构它,虽然新版本仍在工作,但它编译太慢,无法使用。

旧的工作代码

我将尝试在不复制组成真实版本的数百行代码的情况下模拟它的外观。

#include <stddef.h>
namespace typeList
{

template<class ...TYPES>
class List
{
public:
TypeList() = delete;
static constexpr size_t size = sizeof...(TYPES);
};

template<class LIST, size_t INDEX>
using get = /*Some elaborate implementation*/;

template<class LIST, size_t INDEX, class TYPE>
using insert = /*Some elaborate implementation*/;

template<class LIST, template<class> class MAPPER>
using map = /*Some elaborate implementation*/;

// There goes much more such "functions" but you should get the gist by now.

}

使用这个看起来或多或少像这样:

using initialList = typeList::List<bool, char>;
using tmp0 = typeList::insert<initialList, 1, void>; // List<bool, void, char>
using tmp1 = typeList::map<tmp0, SomeClass>; // List<SomeClass<bool>, SomeClass<char>, SomeClass<char>>
...
using finalResult = typeList::get<tmp26, 0>;

基于该版本的程序在20-30秒的合理时间内编译。带有所有生成类型的预编译头文件大约需要100MB。

新代码

我对必须创建大量的即时、一次性类型感到不高兴,因为这会浪费代码,而且只是令人讨厌的样板。我试着重写我的模板,这样他们就可以把它们链接起来。

#include <stddef.h>
namespace typeList
{

template<class ...TYPES>
class List
{
public:
TypeList() = delete;
static constexpr size_t size = sizeof...(TYPES);

template<size_t INDEX>
using get = /*Some elaborate implementation*/;

template<size_t INDEX, class TYPE>
using insert = /*Some elaborate implementation*/;

template<template<class> class MAPPER>
using map = /*Some elaborate implementation*/;

// There goes much more such "functions" but you should get the gist by now.
};

}

使用新版本看起来是这样的:

using initialList = typeList::List<bool, char>;
using finalResult = initialList
::insert<1, void> // List<bool, void, char>
::map<SomeClass> // List<SomeClass<bool>, SomeClass<char>, SomeClass<char>>
...
::get<0>;

我的重构尝试成功了一半。它确实进行了编译,并且给出了与旧版本完全相同的结果。然而,现在大约需要3-10分钟(通常是10分钟),并且预编译的头文件有2GB。(所以基本上两个值都提高了20倍)此外,编译器在工作时占用高达8GB的RAM。

问题

这个问题有两个部分:

  1. 为什么在这两种情况下编译器的性能存在如此大的差异
  2. 有可能在不丢失与模板交互的好方法的情况下修复第二个版本的性能吗

我不知道为什么编译器会有这样的差异。代码的语法发生了一些变化,但其含义基本相同。看起来编译器必须对20倍以上的数据执行20倍以上计算!

我最初的猜测是GCC试图急切地实例化List类中using子句的所有类型,但它们中的大多数都是template,所以我不知道编译器如何尝试。事实上,在代码的完整版本中,我有一个using,它不是模板,它让编译器陷入了实例化它的进一步结果的循环中。添加一个伪模板参数解决了这个问题,这意味着templateusings是按需实例化的。


还有一件事。我正在为AVR编译程序,虽然我有最新版本的GCC,但我无法访问C++标准库。如果解决方案涉及C标准库之外的任何内容,我将不胜感激,但我仍然更喜欢基于纯C++的解决方案。

免责声明:以下所有内容都是我的一般观察结果,我对它们的正确性没有超过90%的把握

我已经设法完成了重构,保持了更合理的编译性能。现在它大约是原始时间和内存使用量的2倍(比上次尝试少了10倍)。

我没有一个全面的解决方案来解决这个问题,我怀疑是否存在,但有一个关于如何控制这种代码的通用指南。


问题的原因

据我所知,编译缓慢的唯一原因是GCC非常渴望尽快实例化任何和所有模板,无论它们是否被使用。它在较短的程序中并不明显,而且似乎只有当代码足够复杂时才明显。

这种行为是病态的,在更复杂的情况下,它可能会导致无限递归。例如,以下代码编译良好,但在更复杂的程序中,类似的模式会导致template instantiation depth exceeds maximum错误。

template<class T>
struct Bar;
template<class T>
struct Foo
{
using goDeeper = Bar<Foo<T>>;
};
template<class T>
struct Bar
{
using goDeeper = Foo<Bar<T>>;
};

我试图通过将所有东西都变成带有伪参数的模板来防止这些急切的实例化,使GCC认为它没有足够的信息来触摸它。它有时有效,但有时GCC似乎足够聪明,可以检测到模板参数未使用。

很难找到导致特定问题的确切代码模式,因为首先,只有当程序足够复杂时,这种情况才会发生,其次,这些行为中的一些似乎是随机的。出于这个原因,我只能就如何编写这种代码给出一些一般性的建议。


保持编译快速的方法

使用预编译的标头

当涉及到编译速度的问题时,这是一个很容易的问题,但它也将有助于下一步的建议。

对每个变更进行基准测试

GCC在编译模板过程中的行为很难预测。有时,一个变化几乎不会产生任何影响,而另一个非常相似的变化可能会彻底破坏性能。因此,每次能够并准备好恢复到以前的版本时,都应该对编译进行基准测试。(保留备份/检查点。)

测量编译时间时要小心。它可能非常不稳定,在构建相同的源时会有几十%的变化。在我的情况下,当我在后台播放视频时,速度大约快了2倍。(我无法通过使用其他形式的处理器/内存负载来复制这种效果。)

最好将预编译头文件的大小作为一个额外的指标。它是非常稳定的,它与时间的核心相对紧密。

拆分模板类

不要用你需要的所有东西创建一个大类,试着用下面的方法把它分成两个或更多。(我建议3个级别。)

template<class ...TYPES>
class VeryBasicList
{
public:
// Only the things that have negligible impact on performance here, e.g. pushBack.
// Nothing that may use any form of recursion.
};
template<class ...TYPES>
class BasicList : public VeryBasicList<TYPES...>
{
public:
// Things that may have some impact but they are commonly used to implement
// more complex operations, e.g. popBack, get<N>.
};
template<class ...TYPES>
class List : public BasicList<TYPES...>
{
public:
// Everything else you want to have.
};

之后,使用您所能使用的最基本的类,尤其是在实现最基本和最常用的操作时,尤其是当它们使用递归时。

这种方法可能会将编译时间减少几倍。

高度优化基本操作

使用频率最高、需要递归级别最多的操作影响最大。

您可能会想从更基本的操作中组合一些操作,比如使用reduce来生成map,但这是个坏主意。最好的办法是独立地写出每一个基本的东西。

优化的最佳方法是简单。例如,在这种情况下,你需要将列表一分为二,这似乎是一个好主意,但当我尝试这样做时,我得到的结果比我第一次从整个列表中切下前半部分,然后再从整个列表切下第二部分时更糟。也许是这样,因为这些更简单的操作也在其他地方使用,所以它们可能与已经实例化的东西重叠。

避免可能导致无限递归的事情

尽量不要写任何可能无限期扩展而没有任何额外说明的内容。总是设置某种结束条件。

例如

template<class ...TYPES>
class List
{
public:
using recursion = List<List<TYPES...>>;
};

在这里,编译器可以在不被要求的情况下实例化List<List<List<List<List<List<...

改为写入:

template<class ...TYPES>
class List;
template<class ORIGINAL_LIST, unsigned DEPTH>
class RecursionHelper
{
public:
using result = typename RecursionHelper<List<ORIGINAL_LIST>, DEPTH-1>::result;
};
template<class ORIGINAL_LIST>
class RecursionHelper<ORIGINAL_LIST, 0>
{
public:
using result = ORIGINAL_LIST;
};
template<class ...TYPES>
class List
{
public:
template<unsigned DEPTH>
using recursion = List<List<TYPES...>>;
};

是的,这需要更多的编写,但至少它不会在某个随机点爆炸,从而产生难以调试的错误。

尽量保持班级规模小

如果你有RemoveIfConditionIsFalse操作,你可能不需要RemoveIfConditionIsTrue。这并不重要,因为类元素数量的增加似乎对所需内存只有线性影响,对编译速度的影响可能更小,但这仍然是一个显著的差异。

最新更新