5

我正在维护一个大型模板类库,这些模板类基于任一floatdouble类型执行代数计算。许多类都有访问器方法(getter 和 setter)和其他运行少量代码的函数,因此当编译器找到它们的定义时,这些函数需要被限定为内联。相比之下,其他成员函数包含复杂的代码,因此最好调用而不是内联。

函数定义的很大一部分位于标头中,实际上位于标头包含的 .inl 文件中。但是也有许多类的函数定义通过显式实例化而愉快地存在于 .cpp 文件中floatdouble,这在库的情况下是一件好事(这里解释了原因)。最后,有相当多的类的函数定义在 .inl 文件(访问器方法)和 .cpp 文件(构造函数、析构函数和繁重的计算)中被破坏,这使得它们都很难维护。

只有当我知道一种可靠的方法来防止某些函数被内联时,我才会将我的所有类实现放在 .inl 文件中,或者如果inline关键字可以强烈建议编译器内联某些函数,则在 .cpp 文件中,当然,它才不是。我真的希望库中的所有函数定义都驻留在 .cpp 文件中,但是由于访问器方法在整个库中被广泛使用,我必须确保它们在被引用时被内联,而不是被调用。

因此,在这方面,我的问题是:

  1. 标记模板函数的定义是否有意义inline,因为正如我最近在这里了解到的那样,无论它是否标记为,编译器都会自动将其限定为内联inline

  2. 最重要的是,由于我希望将模板类的所有成员函数的定义聚集在一个文件中,无论是 .inl 还是 .cpp(在 .cpp 的情况下使用显式实例化),最好仍然能够提示编译器(MSVC 和 GCC)哪些函数应该被内联哪些不应该被内联,确定模板函数是否可以实现这样的事情,我该如何实现这一点,或者,如果真的没有办法(我希望有),什么是最优化的折衷方案?

----------

EDIT1:我知道该inline关键字只是对编译器内联函数的建议。

EDIT2:我真的知道。我喜欢向编译器提出建议。

EDIT3:我仍然知道。这不是问题所在。

----------

鉴于一些新的信息,还有第三个问题与第二个问题并驾齐驱。

3.如果编译器现在非常聪明,他们可以更好地选择应该内联哪个函数,应该调用哪个函数,并且能够生成链接时代码和链接时优化,这可以有效地让他们查看 .cpp -在链接时定位函数定义以决定其被内联或调用的命运,那么也许一个好的解决方案是将所有定义移动到相应的 .cpp 文件中?

----------

那么结论是什么?

首先,我感谢 Daniel Trebbien 和 Jonathan Wakely 的结构化和有根据的回答。两者都赞成,但只能选择一个。然而,没有一个给出的答案对我来说是一个可以接受的解决方案,所以选择的答案恰好是比其他人更能帮助我做出最终决定的答案,接下来会为感兴趣的人解释其细节。

好吧,由于我一直更重视代码的性能,而不是维护和开发的便利程度,在我看来,最可接受的折衷方案是移动每个访问器方法和其他轻量级成员函数将模板类放入相应标头包含的 .inl 文件中,用inline关键字标记这些函数,以尝试为编译器提供良好的提示(或为内联强制提供关键字),并将其余函数移动到相应的.cpp 文件。

将所有成员函数定义都放在 .cpp 文件中会阻碍轻量级函数的内联,同时释放链接时优化的一些问题,正如 Daniel Trebbien 为 MSVC(在较早的开发阶段)和 Jonathan Wakely 为 GCC 确定的那样(在目前的发展阶段)。并且将所有函数定义都放在头文件(或 .inl 文件)中并不会超过将每个类的实现分类到 .inl 和 .cpp 文件中以及该决定的额外副作用的总结好处:它将确保库的客户端只能看到原始访问器方法的代码,而二进制文件中隐藏了更多有趣的东西(确保这不是主要原因,但是对于任何熟悉软件库的人来说,这一点都很明显)。inline鼓励函数的内联状态(还不知道关键字应该在两个地方还是在这种特殊情况下只在一个地方)。

4

4 回答 4

8

简而言之:将模板代码放在头文件中。如果优化器无法做出关于内联的正确决策,请使用编译器特定forceinline或关键字。noinline


您可以并且应该将模板成员的定义放入头文件中。这确保编译器在发现实际模板参数是什么时可以在使用点访问定义,并且能够执行隐式实例化。

关键字对模板的inline影响很小,因为模板函数已经免除了单一定义的要求(单一定义规则仍然要求所有定义都相同)。这是对编译器的提示,该函数应该被内联。您可以省略它作为提示编译器不要内联函数。所以就这样用吧。但是优化器仍然会考虑其他因素(函数大小)并在内联上做出自己的选择。

一些编译器有特殊的关键字,比如__attribute__(always_inline)or__declspec(noinline)来覆盖优化器的选择。

但是,大多数情况下,编译器足够聪明,不会内联“作为函数调用更有意义的复杂代码”。您不必担心它,只需让优化器完成它的工作即可。

可移植的内联控制没有好处,因为内联的权衡是非常特定于平台的。优化器应该已经意识到那些特定于平台的权衡,如果您确实需要覆盖编译器的选择,请在每个平台的基础上这样做。

于 2012-07-10T19:51:52.747 回答
5

1. 用 inline 标记模板函数的定义是否有意义,因为正如我最近了解到的那样,无论它是否标记为 inline,编译器都会自动将其限定为 inline或不?行为是编译器特定的吗?

我认为您指的是在其类定义中定义的成员函数始终是内联函数的事实。这是根据 C++ 标准,自首次发布以来一直如此:

9.3 成员函数

...

成员函数可以在其类定义中定义(8.4),在这种情况下,它是内联成员函数(7.1.2)

因此,在以下示例中,template <typename FloatT> my_class<FloatT>::my_function()始终是内联函数:

template <typename FloatT>
class my_class
{
public:
    void my_function() // `inline` member function
    {
        //...
    }
};

template <>
class my_class<double> // specialization for doubles
{
public:
    void my_function() // `inline` member function
    {
        //...
    }
};

但是,通过将 的定义移到 的定义my_function()之外template <typename FloatT> my_class<FloatT>,它不会自动成为内联函数:

template <typename FloatT>
class my_class
{
public:
    void my_function();
};

template <typename FloatT>
void my_class<FloatT>::my_function() // non-`inline` member function
{
    //...
}

template <>
void my_class<double>::my_function() // non-`inline` member function
{
    //...
}

inline在后一个示例中,将说明符与定义一起使用确实有意义(例如,它不是多余的) :

template <typename FloatT>
inline void my_class<FloatT>::my_function() // `inline` member function
{
    //...
}

template <>
inline void my_class<double>::my_function() // `inline` member function
{
    //...
}

2. 最重要的是,因为我希望将模板类的所有成员函数的定义聚集在一个文件中,无论是 .inl 还是 .cpp(在 .cpp 的情况下使用显式实例化),最好还是能够提示编译器(MSVC 和 GCC)哪些函数应该被内联,哪些函数不应该被内联,确定模板函数是否可以实现这样的事情,我该如何实现这一点,或者,如果真的没有办法(我希望有),什么是最优化的折衷方案?

如您所知,编译器可能会选择内联函数,无论它是否具有说明inline符;说明inline符只是一个提示。

没有强制内联或阻止内联的标准方法;但是,大多数 C++ 编译器都支持语法扩展来实现这一点。MSVC 支持一个__forceinline关键字来强制内联并#pragma auto_inline(off)阻止它。G++ 支持always_inlinenoinline属性分别用于强制和阻止内联。您应该参考编译器的文档以获取详细信息,包括当编译器无法按要求内联函数时如何启用诊断。

如果您使用这些编译器扩展,那么您应该能够向编译器提示函数是否内联。

一般来说,我建议将所有“简单”的成员函数定义聚集在一个文件(通常是标题)中,我的意思是,如果成员函数不需要比定义所需的 s#include集更多的 s#include类/模板。有时,例如,成员函数定义将需要#include <algorithm>,但不太可能需要<algorithm>包含类定义才能进行定义。您的编译器能够跳过它不使用的函数定义,但是更多的#includes 会显着延长编译时间,而且您不太可能想要内联这些非“简单”函数。


3. 如果编译器现在非常聪明,他们可以更好地选择应该内联哪个函数,应该调用哪个函数,并且能够生成链接时代码和链接时优化,这可以有效地让他们查看 .cpp -在链接时定位函数定义以决定其被内联或调用的命运,那么也许一个好的解决方案是将所有定义移动到相应的 .cpp 文件中?

如果您将所有函数定义都放入 CPP 文件中,那么您将依赖 LTO 进行大部分函数内联。这可能不是您想要的,原因如下:

  1. 至少使用 MSVC 的 LTCG,您放弃了强制内联的能力(请参阅inline、 __inline、__forceinline 。)
  2. 如果 CPP 文件链接到共享库,那么与共享库链接的程序将不会受益于库函数的 LTO 内联。这是因为编译器中间语言 (IL)(LTO 的输入)已被丢弃,并且在 DLL 或 SO 中不可用。
  3. 如果幕后:链接时代码生成仍然正确,“无法优化对静态库中函数的调用”。
  4. 链接器将执行所有内联,这可能比让编译器在编译时执行一些内联​​要慢得多。
  5. 编译器的 LTO 实现可能存在导致它不内联某些函数的错误。
  6. 使用 LTO 可能会对使用您的库的项目施加某些限制。例如,根据Under The Hood: Link-time Code Generation,“预编译头文件和 LTCG 不兼容”。/ LTCG (链接时代码生成) MSDN 页面有其他注释,例如“/LTCG 不适用于 /INCREMENTAL”。

如果您将可能内联的函数定义保留在头文件中,那么您可以同时使用编译器内联和 LTO。另一方面,将所有函数定义移动到 CPP 文件中会将编译器内联限制为仅在翻译单元内。

于 2012-07-13T12:51:04.870 回答
3
  1. 我不知道您从哪里了解到的,但是模板不是“无论是否标记为内联,编译器都会自动将其限定为内联”。模板和内联函数都具有有时称为“模糊链接”的东西,这意味着它们的定义可以存在于多个对象中而不会出错,并且链接器将使用其中一个定义并丢弃其他定义。但事实上模板和内联函数都有模糊的链接并不意味着模板是自动内联的。狮子和老虎都是大型猫科动物,但这并不意味着狮子是老虎。

  2. 除非您事先知道您正在使用的所有实例化,否则您不能总是使用显式实例化,例如,如果您正在编写模板库供其他人使用,那么您无法提供所有显式实例化,因此您必须.h(或.inl)代码用户可以使用的文件#include。如果您确实事先知道所有实例化,那么在.cpp文件中使用显式实例化具有缩短编译时间的优势,因为编译器仅在包含显式实例化的文件中实例化模板一次,而不是在使用它们的每个文件中。但这与内联无关。对于要内联的函数,其定义必须对调用它的代码可见,因此如果您只在文件中定义函数模板(或类模板的成员函数),.cpp那么它们不能在该文件中的任何地方内联。如果你在一个.cpp文件中定义它们并限定它们,inline那么你可能会在尝试从其他文件中调用它们时出现问题,这些文件看不到inline关键字(如果一个函数在一个翻译单元中声明为内联,则它必须在所有翻译单元中声明为内联它出现的翻译单元,[dcl.fct.spec]/4。)
    对于它的价值,我通常不费心使用.inl文件,我只是直接在.h文件,这样可以少处理一个文件。一切都在一个地方,而且它可以正常工作,所有使用模板的文件都可以看到定义并在需要时选择内联它们。在这种情况下,您仍然可以使用显式实例化来缩短编译时间并减少目标文件大小,而不会牺牲内联机会。

  3. 为什么这比仅仅在它所属的标题中定义模板代码更好?你到底想达到什么目的?如果文件较少,请将模板代码放在头文件中,这将始终有效,编译器可以选择内联所有内容而不需要 LTO,并且每个类模板只有一个文件(并且您仍然可以使用显式实例化来缩短编译时间) . 如果您试图将所有代码移动到.cpp文件中(我认为您过于关注了),那么请继续执行。我认为这是一个坏主意,并且可能会导致问题(链接时优化仍然存在我尝试使用它的唯一编译器的问题,并且肯定不会使编译速度更快)但是如果这是您想要的,请执行任何漂浮你的船。

您的问题似乎围绕着这里的一个误解:

只有当我知道一种防止某些函数被内联的可靠方法时,我才会将我的所有类实现放在 .inl 文件中,

如果你所有的模板定义都在头文件中,你不需要“一种可靠的方法来防止某些函数被内联”......正如我上面所说,模板不会自动inline仅仅因为它们在标题中,如果他们' re 太大而无法内联编译器不会内联它们。第一个问题解决了。第二:

或者在 .cpp 文件中,如果 inline 关键字可以强烈建议编译器内联某些函数,当然,它不会,特别是如果标有 inline 的函数位于 .cpp 文件中。

正如我上面所说,文件中标记的函数inline格式.cpp错误,除非它也在标题中标记为内联,并且从未在任何其他.cpp文件中使用。所以这样做只会让生活变得困难,并可能导致链接器错误。何必。

同样,所有迹象都表明只需将模板定义放在标题中。您仍然可以使用显式实例化(正如std::string您链接到的帖子中提到的 GCC 所做的那样),这样您就可以两全其美了。它唯一没有实现的是对模板的用户隐藏实现,但无论如何这听起来不是你的目标,如果它提供非模板功能API,可以在模板方面实现一个.cpp文件。

于 2012-07-12T23:28:51.307 回答
1

这不是一个完整的答案。

我读到 clang 和 llvm 能够进行非常全面的链接时间优化。这包括链接时间内联!要启用此功能,请在使用 clang++ 时使用优化级别 -O4 进行编译。目标文件将是 llvm 字节码而不是机器码。这就是使这成为可能的原因。因此,此功能应允许您将所有定义放在 cpp 文件中,并知道它们仍将在必要时内联。

顺便说一句,函数体的长度并不是决定它是否被内联的唯一因素。仅从一个位置调用的冗长函数可以很容易地在该位置内联。

于 2012-07-14T07:12:21.103 回答