【问题标题】:Code organization across files that has to deal with template functions and inlining跨文件的代码组织,必须处理模板函数和内联
【发布时间】:2012-07-10 19:48:18
【问题描述】:

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

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

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

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

  1. inline 标记模板函数的定义是否有意义,因为我最近了解到here,它将被自动限定为内联编译器不管它是否标有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 文件中以及该决定的额外副作用的总结好处:它将确保库的客户端只能看到原始访问器方法的代码,而二进制文件中隐藏了更多有趣的东西(确保这不是主要原因,但是对于任何熟悉软件库的人来说,这一点都很明显)。并且任何不需要被库的包含文件公开并由其类私有使用的轻量级成员函数都可以在类的 .cpp 文件中具有其定义,而其声明/定义带有@987654333 @ 鼓励函数的内联状态(还不知道关键字应该在两个地方还是在这种特殊情况下只在一个地方)。

【问题讨论】:

  • 简单地搜索“内联”和“模板”似乎已经引起了很多讨论,其中的问题已经敲定。例如:stackoverflow.com/questions/10535667/…
  • @HostileFork 谢谢,但是,它是那些罕见的 SO 页面之一,其中最重要的答案是矛盾的。希望这个话题有另一个机会在这里得到一些共识。
  • ..在没有提供任何有价值的信息或分享经验的情况下相互矛盾,真的。
  • 我想我很困惑。问题是“束缚我的编译器的内联策略是否有意义?”这是一个关于优化的问题,您需要“可信和/或官方”的回复。我认为对此类问题唯一可能的“可信和/或官方”回答必须是“视情况而定”。
  • @ybungalobill: 链接加速绝对是你通过整体程序优化无法获得的。

标签: c++ templates compiler-construction inline


【解决方案1】:

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


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

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

一些编译器有特殊的关键字,如@9​​87654324@ 或__declspec(noinline) 来覆盖优化器的选择。

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

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

【讨论】:

  • 注: GCC 属性拼写为always_inline
  • @BenVoigt 如果没有诸如“将模板代码放入头文件”和“您可以并且应该将定义放入头文件”之类的命令性指令,此答案将很有用,但会讨论为什么我应该选择头文件超过 .cpp 文件。并且没有提及显式实例化.. 如果您提出特定于编译器的forceinline,那么我可以对 .cpp 文件中的任何函数使用这样的关键字,这可能会对支持的链接时代码生成功能产生影响由一些现代编译器,您的答案中没有涵盖。
  • @Desmond:命令式建议是您问题 #2 的答案。此外,使用显式实例化而不是隐式实例化是有正当理由的,但内联控制不是其中之一。显式实例化完全是本问答的题外话。
  • 感谢@JonathanWakely,已修复
  • 如果有人得到了很好的答案并且正在考虑是否值得发布它们,因为无论如何我都会接受这个,请知道我不会这样做。
【解决方案2】:

1。使用 inline 标记模板函数的定义是否有意义,因为正如我最近了解到的那样,无论它是否标记为 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 &lt;typename FloatT&gt; my_class&lt;FloatT&gt; 的定义之外,它不会自动成为 内联 函数:

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 属性分别用于强制和防止内联。您应该参考编译器的文档了解详细信息,包括当编译器无法按要求内联函数时如何启用诊断。

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

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


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

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

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

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

【讨论】:

  • 对问题 #1 的详细回答,谢谢。如果您有兴趣,我还添加了问题 #3。
【解决方案3】:
  1. 我不知道您是从哪里了解到的,但模板不是“无论是否标记为内联,编译器都会自动将其限定为内联”。模板和内联函数都具有有时称为“vague linkage”的含义,这意味着它们的定义可以存在于多个对象中而不会出错,并且链接器将使用其中一个定义并丢弃其他定义。但事实上模板和内联函数都有模糊的链接并不意味着模板是自动内联的。狮子和老虎都是大猫,但这并不意味着狮子就是老虎。

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

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

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

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

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

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

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

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

【讨论】:

  • 嗯,我想要实现的是函数声明与其实现的经典分离。大约 95% 的模板函数定义最好对我的库的客户端程序/用户隐藏。但这足以让我考虑 .cpp 方向的第一个原因..
  • .. 或 .inl 文件的方向。
  • .inl 文件只是标题的另一个名称。将标头分成两部分用于声明和定义当然可以帮助使代码更清晰,并且无论您将其称为.inl.tcc 还是其他名称都没有关系。在所有声明之后,我个人经常只是将定义放在文件的底部。但是无论你怎么做,你都不能用模板完全实现接口和实现的经典分离,因为要求模板实现总是可见的。
  • 但正如我在回答中所说,模板的作者事先知道模板将被实例化的所有类型并不总是正确的(事实上我会说这并不常见),所以 一般而言您不能依赖显式实例化,只允许您将模板定义放在.cpp 文件中。
  • (哦,我编辑了这个问题,引用了source,它认为模板函数是自动内联的。错误的假设。马克·兰森(Mark Ransom)在表达他的信念时不太确定他们是。)
【解决方案4】:

这不是一个完整的答案。

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-01
    • 2019-07-05
    • 1970-01-01
    • 1970-01-01
    • 2013-07-14
    • 1970-01-01
    相关资源
    最近更新 更多