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

发布于 2024-09-02 03:06:32 字数 311 浏览 6 评论 0原文

我正在将我的 C++ 应用程序的一部分从使用旧的 C 类型数组更改为模板化的 C++ 容器类。请参阅此问题了解详细信息。虽然该解决方案运行良好,但我对模板化代码所做的每一个微小更改都会导致发生大量重新编译,从而大大减慢构建时间。有没有什么方法可以将模板代码从标头中取出并返回到 cpp 文件中,以便较小的实现更改不会导致重大重建?

I'm in the process of changing part of my C++ app from using an older C type array to a templated C++ container class. See this question for details. While the solution is working very well, each minor change I make to the templated code causes a very large amount of recompilation to take place, and hence drastically slows build time. Is there any way of getting template code out of the header and back into a cpp file, so that minor implementation changes don't cause major rebuilds?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

随梦而飞# 2024-09-09 03:06:32

几种方法:

  • export 关键字理论上可以提供帮助,但它的支持很差,并在 C++11 中被正式删除。
  • 显式模板实例化(请参阅此处或< a href="https://msdn.microsoft.com/en-us/library/by56e477%28v=vs.140%29.aspx" rel="nofollow noreferrer">此处)是最直接的方法,如果您可以提前预测您将需要哪些实例化(并且如果您不介意维护此列表)。
  • 外部模板,已经被多个编译器支持作为扩展。据我了解,外部模板不一定让您将模板定义移出头文件,但它们确实使编译和链接速度更快(通过减少模板代码必须实例化和链接的次数)。
  • 根据您的模板设计,您也许能够将其大部分复杂性转移到 .cpp 文件中。标准示例是类型安全向量模板类,它仅包装 void* 类型不安全向量;所有的复杂性都集中在 .cpp 文件中的 void* 向量中。 Scott Meyers 在 Effective C++ 中给出了更详细的示例(第 42 项,“明智地使用私有继承”,第二版)。

Several approaches:

  • The export keyword could theoretically help, but it was poorly supported and was officially removed in C++11.
  • Explicit template instantiation (see here or here) is the most straightforward approach, if you can predict ahead of time which instantiations you'll need (and if you don't mind maintaining this list).
  • Extern templates, which are already supported by several compilers as extensions. It's my understanding that extern templates don't necessarily let you move the template definitions out of the header file, but they do make compiling and linking faster (by reducing the number of times that template code must be instantiated and linked).
  • Depending on your template design, you may be able to move most of its complexity into a .cpp file. The standard example is a type-safe vector template class that merely wraps a type-unsafe vector of void*; all of the complexity goes in the void* vector that resides in a .cpp file. Scott Meyers gives a more detailed example in Effective C++ (item 42, "Use private inheritance judiciously", in the 2nd edition).
成熟稳重的好男人 2024-09-09 03:06:32

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

另外,尝试使标头快速进入稳定状态,也许可以针对较小的测试程序对它们进行测试,这样当集成到较大的程序中时,它们就不需要(太多)更改。

(与任何优化一样,在处理模板时优化编译器的速度可能不太值得,而不是首先找到一种可以大幅减少工作负载的“算法”优化。)

I think the general rules apply. Try to reduce coupling between parts of the code. Break up too large template headers into smaller groups of functions used together, so the whole thing won't have to be included in each and every source file.

Also, try to get the headers into a stable state fast, perhaps testing them out against a smaller test program, so they wouldn't need changing (too much) when integrated into a larger program.

(As with any optimization, it might be less worth to optimize for the compiler's speed when dealing with templates, rather than finding an "algorithmic" optimization that reduces the work-load drastically in the first place.)

南城旧梦 2024-09-09 03:06:32

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

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

Chiel 规则

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

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

  • SFINAE
  • 实例化函数模板
  • 实例化类型
  • 调用别名
  • 向类型添加参数
  • 向别名调用添加参数
  • 查找记忆的类型

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

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

减少模板实例化

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

template< bool B, typename T, typename F >
struct conditional;

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

struct first{};
struct second{};

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

using type1 = conditional<true, first, second>;
using type2 = conditional<true, second, first>;
std::is_same_v<type1, type2>; // it’s false

using type3 = conditional<false, first, second>;
using type4 = conditional<false, second, first>;
std::is_same_v<type1, type2>; // it’s false

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

template <bool>
struct conditional{
     template <typename T, typename F>
     using type = T;
};

template <>
struct conditional<false>{
     template <typename T, typename F>
     using type = F;
};

在这种情况下,编译器将仅创建类型的两个实例化所有可能的论点都是“有条件的”。有关此示例的更多详细信息,请查看Odin Holmes 关于 Kvasir 库的演讲

创建显式模板实例化

每当您怀疑某个模板实例将被经常使用时,显式实例化它是一个好主意。通常,std::stringstd::basic_string 的显式实例化。

为编译时算法创建专业化

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

Using templates as a problem solving technique can create compilation slowdowns. A classical example of this is the std::sort vs. qsort function from C. The C++ version of this function takes longer to compile because it needs to be parsed in every translation unit and because almost every use of this function creates a different instance of this template (assuming that closure types are usually provided as sorting predicate).

Although these slowdowns are to be expected, there are some rules that can help you to write efficient templates. Four of them are described below.

The Rule of Chiel

The Rule of Chiel, presented below, describes which C++ constructs are the most difficult ones for the compiler. If possible, it’s best to avoid those constructs to reduce compilation times.

The following C++ features/constructs are sorted in descending order by compile time:

  • SFINAE
  • Instantiating a function template
  • Instantiating a type
  • Calling an alias
  • Adding a parameter to a type
  • Adding a parameter to an alias call
  • Looking up a memorized type

Optimizations based on the above rules were used when Boost.TMP was designed and developed. As much as possible, avoid top constructs for quick template compilation.

Below are some examples illustrating how to make use of the rules listed above.

Reduce Template Instantiations

Let's have a look at std::conditional. Its declaration is:

template< bool B, typename T, typename F >
struct conditional;

Whenever we change any of three arguments given to that template, the compiler will have to create a new instance of it. For example, imagine the following types:

struct first{};
struct second{};

Now, all the following will end up in instantiations of different types:

using type1 = conditional<true, first, second>;
using type2 = conditional<true, second, first>;
std::is_same_v<type1, type2>; // it’s false

using type3 = conditional<false, first, second>;
using type4 = conditional<false, second, first>;
std::is_same_v<type1, type2>; // it’s false

We can reduce the number of instantiations by changing the implementation of conditional to:

template <bool>
struct conditional{
     template <typename T, typename F>
     using type = T;
};

template <>
struct conditional<false>{
     template <typename T, typename F>
     using type = F;
};

In this case, the compiler will create only two instantiations of type “conditional” for all possible arguments. For more details about this example, check out Odin Holmes' talk about the Kvasir library.

Create Explicit Template Instantiations

Whenever you suspect that an instance of a template is going to be used often, it’s a good idea to explicitly instantiate it. Usually, std::string is an explicit instantiation of std::basic_string<char>.

Create Specializations for Compile-time Algorithms

Kvasir-MPL specializes algorithms for long lists of types to speed them up. You can see an example of this here. In this header file, the sorting algorithm is manually specialized for a list of 255 types. Manual specialization speeds up compilations for long lists.

╰ゝ天使的微笑 2024-09-09 03:06:32

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

然而,我怀疑真正的问题是您使用泛型编程就像使用典型的面向对象编程一样,最终会得到一个臃肿的类。

举个例子:

// "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
};

这让你震惊吗?可能不会。毕竟它看起来非常简约。问题是,事实并非如此。 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];
}

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

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

// To
at(myArray,i).method();

但是,由于 Koenig 的查找,只要将它们放在同一个命名空间中,您就可以将它们称为非限定的,所以这只是一个习惯问题。

这个例子是人为的,但总体观点是成立的。请注意,由于其通用性,at.hpp 永远不必包含 bigArray.hpp,并且仍然会生成像成员方法一样紧凑的代码,只是我们可以如果我们愿意,可以在其他容器上调用它。

现在,BigArray 的用户如果不使用 at.hpp,则不需要包含它...从而减少了她的依赖性,并且如果您更改了则不会受到影响该文件中的代码:例如 alter std::out_of_range 调用以显示文件名和行号、容器的地址、其大小以及我们尝试访问的索引。

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

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

第 44 项:更喜欢编写非成员非友元函数

,并已在 Boost 中广泛使用...但你必须改变你的编码习惯!

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

First of all, for completeness, I'll cover the straightforward solution: only use templated code when necessary, and base it on non-template code (with implementation in its own source file).

However, I suspect that the real issue is that you use generic programming as you would use typical OO-programming and end up with a bloated class.

Let's take an example:

// "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
};

Does this shock you ? Probably not. It seems pretty minimalist after all. The thing is, it's not. The at methods can be factored out without any loss of generality:

// "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];
}

Okay, this changes the invocation slightly:

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

// To
at(myArray,i).method();

However, thanks to Koenig's lookup, you can call them unqualified as long as you put them in the same namespace, so it's just a matter of habit.

The example is contrived but the general point stands. Note that because of its genericity at.hpp never had to include bigArray.hpp and will still produce as tight code as if it were a member method, it's just that we can invoke it on other containers if we wish.

And now, a user of BigArray does not need to include at.hpp if she does not uses it... thus reducing her dependencies and not being impacted if you change the code in that file: for example alter std::out_of_range call to feature the file name and line number, the address of the container, its size and the index we tried to access.

The other (not so obvious) advantage, is that if ever integrity constraint of BigArray is violated, then at is obviously out of cause since it cannot mess with the internals of the class, thus reducing the number of suspects.

This is recommended by many authors, such as Herb Sutters in C++ Coding Standards:

Item 44: Prefer writing nonmember nonfriend functions

and has been extensively used in Boost... But you do have to change your coding habits!

Then of course you need to only include what you do depend on, there ought to be static C++ code analyzers that report included but unused header files which can help figuring this out.

纵情客 2024-09-09 03:06:32
  • 您可以使用显式实例化;但是,只有您实例化的模板类型才会提前编译。

  • 您也许能够利用 c++20 的模块 .

  • 如果您可以从算法中提取模板化类型,则可以将其放入自己的 .cc 文件中。

  • 我不会建议这样做,除非这是一个主要问题,但是:您也许能够提供一个模板容器接口,该接口是通过调用 void* 实现来实现的,您可以在会的。

  • 在 c++11 之前,您可以使用支持导出 关键字。


  • You can use explicit instantiation; however, only the template types you instantiate will compile ahead of time.

  • You might be able to take advantage of c++20's modules.

  • If you can factor out the templated types from your algorithm, you can put it in its own .cc file.

  • I wouldn't suggest this unless it's a major problem but: you may be able to provide a template container interface that is implemented with calls to a void* implementation that you are free to change at will.

  • Before c++11 you could use a compiler that supports the export keyword.

你又不是我 2024-09-09 03:06:32

您可以定义一个没有模板的基类,并将大部分实现移至那里。然后,模板化数组将仅定义代理方法,该方法对所有内容都使用基类。

You can define a base class without templates and move most of the implementation there. The templated array would then define only proxy methods, that use base class for everything.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文