首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >链接模板类时编译性能问题

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

Stack Overflow用户
提问于 2020-08-10 21:51:53
回答 1查看 62关注 0票数 0

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

代码已经起作用了,但我对它的界面(在下面解释)并不满意,所以我尝试重构它,当新版本还在工作时,它的编译速度太慢,无法使用。

旧的,有用的代码

我将尝试模仿它的外观,而不复制构成真正版本的数百行代码。

代码语言:javascript
运行
复制
#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.
    
}

使用它大致如下所示:

代码语言:javascript
运行
复制
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。

新代码

我不喜欢创建大量即时的、单一用途的类型,因为它乱扔代码,而且它只是烦人的样板。我试图重写我的模板,这样它们就允许将它们链接起来。

代码语言:javascript
运行
复制
#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.
    };
    
}

使用新版本如下所示:

代码语言:javascript
运行
复制
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。

问题是

问题分为两部分:

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

我不知道为什么编译器会有这么大的区别。代码的语法略有变化,但其含义基本相同。不过,看起来编译器必须对20倍多的数据执行20倍多的计算!

我最初的猜测是GCC试图从using类中的List子句中急切地实例化所有类型,但其中大多数都是templates,所以我不知道编译器如何能够做到这一点。实际上,在代码的完整版本中,我有一个不是模板的using,编译器被困在一个循环中,以进一步实例化它的结果。添加一个虚拟模板参数解决了这个问题,这意味着template usings是按需实例化的。

还有一件事。我正在为AVR编写程序,虽然我有最新版本的GCC,但我无法访问C++标准库。除了C标准库之外,任何涉及其他内容的解决方案都会受到欢迎,但我仍然更喜欢基于纯C++的解决方案。

EN

回答 1

Stack Overflow用户

发布于 2020-11-01 12:57:53

免责声明:下面的每件事都是我的一般观察,我从来没有超过90%的人确信它们是正确的。

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

对于这个问题,我没有一个全面的解决方案,我也怀疑是否存在这样的问题,但是对于如何控制这类代码,我有一个一般性的指南。

问题的原因

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

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

代码语言:javascript
运行
复制
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个层次。)

代码语言:javascript
运行
复制
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 )编写一些操作,但这是个坏主意。最好的办法是独立写出每一件基本的东西。

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

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

在没有任何附加指令的情况下,尽量不要写出任何可以无限期展开的东西。总是有某种结束的条件。

例如。

代码语言:javascript
运行
复制
template<class ...TYPES>
class List
{
public:
    using recursion = List<List<TYPES...>>;
};

在这里,编译器可能会执行实例化List<List<List<List<List<List<...,甚至不需要询问。

相反,写:

代码语言:javascript
运行
复制
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。这并不重要,因为增加类元素的数量似乎只会对所需内存产生线性影响,对编译速度的影响可能更小,但这仍然是一个明显的差异。

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/63348501

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档