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

问题描述

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

代码可以正常工作,但是我对它的接口(在下面解释)并不满意,因此我尝试对其进行重构,尽管新版本仍在工作,但编译速度太慢,无法使用。

旧的有效代码

我将尝试模拟外观,而不复制构成真实版本的数百行代码

#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-30s的合理时间内编译。具有所有生成类型的预编译头文件大约需要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,char>
    ::map<SomeClass> // List<SomeClass<bool>,SomeClass<char>>
    ...
    ::get<0>;

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

问题

问题有两个部分:

  1. 在这两种情况下,为什么编译器的性能存在如此大的差异?
  2. 是否可以在不失去与模板交互的好方法的情况下修复第二版的性能

我不知道为什么编译器会有如此不同。该代码的语法稍有变化,但其含义基本相同。看来编译器必须对20倍以上的数据执行20倍以上的计算!

我最初的猜测是,GCC试图从using类的List子句中实例化所有类型,但是无论如何它们大多数都是template,所以我看不出编译器如何可以尝试一下。实际上,在完整版本的代码中,我有一个using而不是模板,并且编译器陷入了实例化其结果的循环中。添加虚拟模板参数解决了该问题,该问题表明template using是按需实例化的。


还有一件事。我正在为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倍。 (我无法通过使用其他形式引起处理器/内存负载的方式来重现这种效果。)

将预编译头文件的大小用作附加指示符是一个好主意。它非常稳定,并且与时间相对接近。

拆分模板类

与其使用所需的所有内容来创建一个大类,请尝试按照以下方式将其分为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。这并不是很重要,因为类元素数量的增加似乎仅对所需的内存产生线性影响,而对编译速度的影响甚至可能更小,但这仍然是一个明显的区别。