我编写了一组模板实用程序,允许创建类型的编译时列表,并以函数式编程的方式对其进行操作。
代码已经起作用了,但我对它的界面(在下面解释)并不满意,所以我尝试重构它,当新版本还在工作时,它的编译速度太慢,无法使用。
旧的,有用的代码
我将尝试模仿它的外观,而不复制构成真正版本的数百行代码。
#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年代的合理时间内编译。带有所有生成类型的预编译头文件大约需要100 all。
新代码
我不喜欢创建大量即时的、单一用途的类型,因为它乱扔代码,而且它只是烦人的样板。我试图重写我的模板,这样它们就允许将它们链接起来。
#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。此外,编译器在工作时占用最多8GB的RAM。
问题是
问题分为两部分:
我不知道为什么编译器会有这么大的区别。代码的语法略有变化,但其含义基本相同。不过,看起来编译器必须对20倍多的数据执行20倍多的计算!
我最初的猜测是GCC试图从using
类中的List
子句中急切地实例化所有类型,但其中大多数都是template
s,所以我不知道编译器如何能够做到这一点。实际上,在代码的完整版本中,我有一个不是模板的using
,编译器被困在一个循环中,以进一步实例化它的结果。添加一个虚拟模板参数解决了这个问题,这意味着template
using
s是按需实例化的。
还有一件事。我正在为AVR编写程序,虽然我有最新版本的GCC,但我无法访问C++标准库。除了C标准库之外,任何涉及其他内容的解决方案都会受到欢迎,但我仍然更喜欢基于纯C++的解决方案。
发布于 2020-11-01 12:57:53
免责声明:下面的每件事都是我的一般观察,我从来没有超过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倍。(我无法通过使用其他形式的处理器/内存负载来再现这种效果。)
将预编译头文件的大小作为附加指示符是一个好主意。它是非常稳定的,它与时间关系相对密切。
拆分模板类
与其用所需的一切创建一个大类,不如尝试以下面的方式将其拆分为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
。这并不重要,因为增加类元素的数量似乎只会对所需内存产生线性影响,对编译速度的影响可能更小,但这仍然是一个明显的差异。
https://stackoverflow.com/questions/63348501
复制相似问题