如何使用C++模板减少编译时间

Sma*_*acL 34 c++ templates compile-time

我正在将我的C++应用程序的一部分从使用旧的C类型数组更改为模板化的C++容器类.有关详情,请参阅此问题.虽然解决方案运行良好,但我对模板化代码所做的每一个小改动都会导致进行大量的重新编译,从而大大减慢构建时间.有没有办法从标题中取出模板代码并返回到cpp文件,这样小的实现更改不会导致重大的重建?

Jos*_*ley 26

几种方法:

  • 出口关键字理论上可以帮助,但它受到了不好的支持,在C++ 11被正式删除.
  • 如果您可以提前预测您需要的实例化(如果您不介意维护此列表),则显式模板实例化(请参阅此处此处)是最直接的方法.
  • 外部模板,已被多个编译器作为扩展支持.我的理解是,extern模板不一定允许您将模板定义移出头文件,但它们确实可以更快地进行编译和链接(通过减少模板代码必须实例化和链接的次数).
  • 根据您的模板设计,您可以将其大部分复杂性转移到.cpp文件中.标准示例是一个类型安全的矢量模板类,它只包含一个类型不安全的矢量void*; 所有的复杂性都void*存在于.cpp文件中的向量中.Scott Meyers在Effective C++中给出了一个更详细的例子(第42版"明智地使用私有继承").


Unc*_*ens 19

我认为一般规则适用.尝试减少代码部分之间的耦合.将太大的模板标题分解为一起使用的较小的函数组,因此整个事物不必包含在每个源文件中.

另外,尝试快速将标头置于稳定状态,也许可以针对较小的测试程序对其进行测试,因此当集成到更大的程序中时,它们不需要更改(太多).

(与任何优化一样,在处理模板时优化编译器的速度可能不太值得,而不是找到一个"算法"优化,从而首先大幅减少工作量.)

  • +100您不在巨大的无关项目中测试模板.在进入不相关的项目之前,模板应该尽可能没有错误. (6认同)

小智 10

使用模板作为解决问题的技术可能会导致编译速度减慢。一个经典的例子是 C 语言中的 std::sort 与 qsort 函数。该函数的 C++ 版本需要更长的时间来编译,因为它需要在每个翻译单元中进行解析,而且几乎每次使用该函数都会创建一个不同的实例该模板的(假设闭包类型通常作为排序谓词提供)。

\n

尽管这些速度下降是可以预料的,但有一些规则可以帮助您编写高效的模板。下面描述其中的四个。

\n

基尔法则

\n

下面介绍的 Chiel 规则描述了哪些 C++ 结构对于编译器来说是最困难的。如果可能,最好避免这些构造以减少编译时间。

\n

以下 C++ 功能/结构按编译时间降序排序:

\n
    \n
  • 新加坡金融学会
  • \n
  • 实例化函数模板
  • \n
  • 实例化类型
  • \n
  • 调用别名
  • \n
  • 向类型添加参数
  • \n
  • 向别名调用添加参数
  • \n
  • 查找记忆的类型
  • \n
\n

Boost.TMP在设计和开发时采用了基于上述规则的优化。尽可能避免使用顶级构造来快速编译模板。

\n

下面是一些示例,说明如何利用上面列出的规则。

\n

减少模板实例化

\n

让我们看一下 std::conditional。其声明是:

\n
template< bool B, typename T, typename F >\nstruct conditional;\n
Run Code Online (Sandbox Code Playgroud)\n

每当我们更改赋予该模板的三个参数中的任何一个时,编译器都必须创建它的新实例。例如,想象以下类型:

\n
struct first{};\nstruct second{};\n
Run Code Online (Sandbox Code Playgroud)\n

现在,以下所有内容都将以不同类型的实例化结束:

\n
using type1 = conditional<true, first, second>;\nusing type2 = conditional<true, second, first>;\nstd::is_same_v<type1, type2>; // it\xe2\x80\x99s false\n\nusing type3 = conditional<false, first, second>;\nusing type4 = conditional<false, second, first>;\nstd::is_same_v<type1, type2>; // it\xe2\x80\x99s false\n
Run Code Online (Sandbox Code Playgroud)\n

我们可以通过将条件的实现更改为以下来减少实例化的数量:

\n
template <bool>\nstruct conditional{\n     template <typename T, typename F>\n     using type = T;\n};\n\ntemplate <>\nstruct conditional<false>{\n     template <typename T, typename F>\n     using type = F;\n};\n
Run Code Online (Sandbox Code Playgroud)\n

在这种情况下,编译器将为所有可能的参数仅创建两个 \xe2\x80\x9cconditional\xe2\x80\x9d 类型的实例。有关此示例的更多详细信息,请查看Odin Holmes 关于 Kvasir 库的演讲

\n

创建显式模板实例化

\n

每当您怀疑模板的实例将被经常使用时,显式实例化它\xe2\x80\x99 是一个好主意。通常,std::string是 的显式实例化std::basic_string<char>

\n

创建编译时算法的专业化

\n

Kvasir-MPL 专门针对长类型列表的算法来加速它们。您可以在此处查看相关示例。在此头文件中,排序算法是手动专门针对 255 种类型的列表。手动专门化可以加快长列表的编译速度。

\n


Ste*_*hen 6

  • 您可以获得支持export关键字的编译器,但这不太可能持续.

  • 您可以使用显式实例化,但不幸的是,这需要您提前预测您将使用的模板类型.

  • 如果您可以从算法中分解模板化类型,则可以将其放在自己的.cc文件中.

  • 我不建议这样做,除非这是一个主要问题,但是:您可以提供一个模板容器接口,该接口是通过调用void*可随意更改的实现来实现的.

  • `export`将在C++ 0x中删除.你现在不应该考虑使用它. (4认同)
  • +1为显式实例化,我以前没有遇到过,认为它可以提供很多帮助。 (2认同)

Mat*_* M. 5

首先,为了完整起见,我将介绍直接的解决方案:仅在必要时使用模板化代码,并将其基于非模板代码(在其自己的源文件中实现).

但是,我怀疑真正的问题是你使用泛型编程,因为你会使用典型的OO编程并最终得到一个膨胀的类.

我们来举个例子:

// "bigArray/bigArray.hpp"

template <class T, class Allocator>
class BigArray
{
public:
  size_t size() const;

  T& operator[](size_t index);
  T const& operator[](size_t index) const;

  T& at(size_t index);
  T const& at(size_t index);

private:
  // impl
};
Run Code Online (Sandbox Code Playgroud)

这让你震惊吗?可能不是.毕竟它看起来很简约.问题是,事实并非如此.at可以在不失一般性的情况下考虑这些方法:

// "bigArray/at.hpp"

template <class Container>
typename Container::reference_type at(Container& container,
                                      typename Container::size_type index)
{
  if (index >= container.size()) throw std::out_of_range();
  return container[index];
}

template <class Container>
typename Container::const_reference_type at(Container const& container,
                                            typename Container::size_type index)
{
  if (index >= container.size()) throw std::out_of_range();
  return container[index];
}
Run Code Online (Sandbox Code Playgroud)

好的,这稍微改变了调用:

// From
myArray.at(i).method();

// To
at(myArray,i).method();
Run Code Online (Sandbox Code Playgroud)

但是,感谢Koenig的查找,只要将它们放在同一个名称空间中,就可以将它们称为不合格,所以这只是习惯问题.

这个例子是人为的,但总的来说就是这样.请注意,由于它的通用性,at.hpp从来没有必须包含bigArray.hpp并且仍然会产生紧密的代码,就好像它是一个成员方法一样,只是我们可以根据需要在其他容器上调用它.

现在,如果用户不使用它,BigArray则不需要包括at.hpp...因此,如果更改该文件中的代码,则减少其依赖性并且不受影响:例如,更改std::out_of_range调用功能文件名和行号,容器的地址,大小和我们尝试访问的索引.

另一个(不那么明显)的优点是,如果BigArray违反了完整性约束,那么at显然是不合理的,因为它不会弄乱类的内部,从而减少了嫌疑人的数量.

这是许多作者推荐的,例如C++编码标准中的 Herb Sutters :

第44项:更喜欢写非会员非友好职能

并且已被广泛用于Boost ......但你必须改变你的编码习惯!

当然,您只需要包含您所依赖的内容,应该有静态C++代码分析器报告包含但未使用的头文件,这可以帮助解决这个问题.