【问题标题】:Deciding where to place a function implementation决定函数实现的位置
【发布时间】:2019-08-06 03:49:12
【问题描述】:

首先声明,我知道inline 并不意味着编译器会一直内联函数...

在 C++ 中,非templateconstexpr 函数实现确实有两个地方可以使用:

  1. 一个标题,定义应该是内联的
  2. 源文件

将实现放在其中一个有好处/坏处:

  1. inline函数定义
    • 编译器可以内联函数
    • 由于必须解析定义和包含实现依赖项,编译器时间变慢。
    • 同一站点上的多个用户之间的函数的多个副本
  2. 源文件定义
    • 编译器永远不能内联函数(LTO 可能不是这样?)
    • 如果文件没有改变,可以避免重新编译
    • 每个站点一份

我正在编写一个可重用的数学库,其中内联可以显着提高速度。我现在只有测试代码和 sn-ps 可以使用,所以分析不是帮助我做出决定的选项。是否有任何规则——或者只是经验法则——决定在哪里定义函数?是否有某些类型的函数(例如具有异常的函数)总是会生成大量应该归类到源文件的代码?

【问题讨论】:

  • 在您的代码工作之前,您正专注于微优化。首先让它工作,然后分析任何需要解决的热点。您不会解决任何担心是否建议对编译器进行内联而不是专注于以可维护的方式分解代码的问题。将代码分成函数的功能组(如果您的代码对于单个文件来说太长,则在单独的标头标头和源代码中)。让它工作,然后配置文件。无论您是内联还是创建单独的源都不会靠近您的主要问题区域。
  • @David 为什么你认为我的代码不起作用?
  • 好吧,如果你有工作代码,同样适用。配置文件(您可以在建议内联和单独的来源之间移动),但是除了最罕见的情况外,在所有情况下产生可测量差异的可能性很小。担心您是内联还是拆分为单独的源的关键是您要担心的最后一个(如果有的话)考虑因素之一。我并不是要假设您的代码不起作用,但在要考虑的问题中没有提到任何代码。
  • @ktb :因为可以分析工作代码并且可以对替代方法进行基准测试。你没有在你的问题中证明任何这一点,因此假设。
  • 在分析方面我只能做出这么多的假设。目前,我正在开发一个数学库,我面前没有所有潜在的用例,但我必须为最终用户做出决定。在我对向量进行操作的情况下,在一些明显的情况下内联非常重要。我主要是在为你不知道的时候投票的指导方针。

标签: c++


【解决方案1】:

如果您没有数据,请保持简单。

开发糟糕的库不会完成,使用糟糕的库也不会被使用。所以默认拆分h/cpp;这使得构建时间更慢,开发速度更快。


然后获取数据。编写测试,看看你是否从内联中获得了显着的加速。然后去学习如何分析和实现虚假的加速,并编写更好的测试。

在一本书的一章和一本书的长度之间,如何分析和确定什么是虚假的,什么是微基准噪声。阅读有关 C++ 性能的 SO 问题,您至少会了解 10 种最常见的不准确的微基准测试方法。


对于一般规则,紧密循环中的少量代码可以从内联中受益,外部向量化是合理的,错误的别名可能会阻碍编译器优化。

您通常可以通过提供向量操作将内联的好处提升到您的库中。

【讨论】:

    【解决方案2】:

    一般来说,如果您是静态链接(与 DLL/DSO 方法相反),那么编译器/链接器将基本上忽略内联并执行合理的操作。

    旧的经验法则(似乎每个人都忽略了)是内联只能用于小函数。内联的一个问题是,我经常看到人们做一些定时测试,例如

    auto startTime = getTime();
    for(int i = 0; i < BIG_NUM; ++i)
    {
      doThing();
    }
    auto endTime = getTime();
    

    该测试的直接结论是内联对任何地方的性能都有好处。但事实并非如此。

    内联还会增加已编译 exe 的大小。这有一个令人讨厌的副作用,因为它增加了指令和 uop 缓存的负担,这可能会导致性能损失。因此,在大型应用程序的情况下,您通常会发现从常用函数中删除内联实际上可以提高性能。

    内联最严重的问题之一是,如果将其应用于错误的方法,则很难让分析器指出热点 - 它只是比代码库中多个点所需的温度略高。

    我的经验法则 - 如果方法的代码可以放在一行中,则将其内联。如果代码不适合一行,则将其放入 cpp 文件中,直到分析器指示将其移至标头将是有益的。

    【讨论】:

    • 自相矛盾。如果inline 被“忽略”,那么它的不利影响就不会发生。此外,由于inline 实际上是一个链接说明符,所以这些都不适用。这一切都是为了避免多重定义,而不是强制内联发出代码。
    • 您忽略的警告是“如果您是静态链接”。我们中的许多人都在处理稍微太大而无法在整个代码库中静态链接的应用程序。
    • 然而,即使不是静态链接,也有少数人设法做到这一点。而且我没有忽略任何事情,您只是用一种似乎适用于所有答案的方式来表达它。
    【解决方案3】:

    我的经验法则很简单:头文件中没有函数定义,源文件中没有所有函数定义,除非我有特定的理由不这样做。

    一般来说,如果接口与实现明确分离,C++ 代码(如许多语言中的代码)更易于维护。维护工作(通常)是重要程序的成本驱动因素,因为它转化为开发人员的时间和工资成本。在 C++ 中,接口由函数声明(无定义)、类型声明、structclass 定义等表示,即通常放置在标题中的东西,如果意图在不止一个中使用它们源文件。更改接口(例如更改函数的参数类型或返回类型,将成员添加到class 等)意味着必须重新编译依赖于该接口的所有内容。从长远来看,通常头文件比源文件需要更改的频率更少——只要接口与实现保持分离。每当标头更改时,必须重新编译使用该标头的所有源文件(即 #include 它)。如果头文件没有改变,但函数定义发生了变化,那么只需要重新编译包含改变的函数定义的源文件。

    在大型项目中(例如,有数百个源文件和头文件),这种事情可能会导致增量构建需要几秒钟或几分钟来重新编译一些更改的源文件,而重新编译一个大的文件需要更长的时间源文件的数量,因为它们都依赖的标头已更改。

    然后,重点可以放在让代码正常工作上——在给定一组输入的情况下产生相同的可观察输出,满足其功能要求,并通过合适的测试用例。

    一旦代码足够正确地工作,注意力就可以转向其他程序特性,例如性能。如果分析显示一个函数被多次调用并代表一个性能热点,那么您可以查看提高性能的选项。一个可能与提高程序性能相关的选项是选择性地内联函数。但是,每次这样做,就等于决定接受更大的维护负担以获得性能。但有必要证明需要(例如通过分析)。

    与大多数经验法则一样,也有例外。例如,C++ 中的模板化函数或类通常需要内联,因为编译器通常需要查看它们的定义才能实例化模板。但是,这不是内联所有内容的理由(也不是将每个类或函数都转换为模板的理由)。

    如果没有分析或其他证据,我很少会费心内联函数。内联是对编译器的提示,编译器很可能会忽略它,因此内联的努力甚至可能不值得。在没有证据的情况下做这样的事情可能一无所获——在这种情况下,它只是过早的优化。

    【讨论】:

      猜你喜欢
      • 2021-06-29
      • 1970-01-01
      • 1970-01-01
      • 2019-03-21
      • 1970-01-01
      • 2022-11-23
      • 2019-10-09
      • 2022-01-21
      • 1970-01-01
      相关资源
      最近更新 更多