【问题标题】:what is/are the purpose(s) of inline?内联的目的是什么?
【发布时间】:2010-09-05 17:44:15
【问题描述】:

关于关键字inline,我有一个discussionJohannes Schaub。 那里的代码是这样的:

namespace ... {
    static void someFunction() {
        MYCLASS::GetInstance()->someFunction();
    }
};

他说:

把它作为一个内联函数可能 在可执行文件中保存代码大小

但根据我的发现 herehere 是不需要的,因为:

  • 只有在编译器的成本/收益分析表明它是有利可图的情况下才会发生[内联]
  • 主流 C++ 编译器(如 Microsoft Visual C++ 和 GCC)支持一个选项,允许编译器自动内联任何合适的函数,即使是那些未标记为内联函数的函数。

然而,Johannes 指出,明确指定它还有其他好处。不幸的是,我不明白他们。例如,他说并且“内联”允许您在程序中多次定义函数。,我很难理解(并找到参考)。

所以

  1. inline 只是对编译器的建议吗?
  2. 当你有一个小功能时是否应该明确说明(我猜是1-4条指令?)
  3. inline还有什么其他好处?
  4. 是否需要声明 inline 以减少可执行文件的大小,即使编译器(根据维基百科 [我知道,错误的参考])应该自己找到这些函数?

我还有什么遗漏的吗?

【问题讨论】:

  • 仅供参考,内联通常增加可执行文件大小,因为每次内联时都会复制代码。希望获得的收益是更快的执行速度,而不是减小文件大小。
  • 我所说的是与“静态”相比。请在回答之前阅读该评论线程和该静态函数:如果您将静态函数放入标题中并以无法使用内联的方式使用它,并且如果您将标记为“内联”的非静态函数放入标头并以无法使用内联的方式使用它,那么内联版本将在最终程序中占用更少的空间,因为标准要求 函数在任何地方都具有相同的地址 ,所以在最终程序中,只有一个代码副本,而不是“静态”版本。
  • @John:但是内联还允许优化器一次“看到”更多代码,让它有更多的时间去咀嚼。通常这实际上会导致更小代码。对于模板元编程尤其如此,其中内联通常会消除(编译时)递归函数调用。
  • @Johannes Schaub :该标准仅要求将函数的地址两次获取等效值,而不是代码的单个副本。事实上,内联的全部意义在于将多个代码副本嵌入到其他函数中(从而占用代码空间)
  • @MSalters 我很难想象这样的实现,因为它实际上会复制完全相同的代码。这与 C99 的语义不同,其中函数的内联定义和外部定义可以有不同的行为,但仍需要具有相同的地址。因此,C99 在获取地址时会退回到外部定义,否则可能会使用内联定义(但不一定内联调用)。但是,C++ 中不存在这样的东西。这里的内联函数只有一种行为。请注意,我假设调用是 not 内联的。

标签: c++ compiler-construction inline


【解决方案1】:

重申我在那些小评论框中所说的话。特别是,我从来没有谈论过内联

// foo.h:
static void f() {
  // code that can't be inlined
}

// TU1 calls f
// TU2 calls f

现在,TU1 和 TU2 都有自己的 f 副本 - f 的代码在可执行文件中出现了两次。

// foo.h:
inline void f() {
  // code that can't be inlined
}

// TU1 calls f
// TU2 calls f

两个 TU 都将发出特别标记的 f 版本,链接器通过丢弃除其中一个之外的所有内容来有效地合并这些版本。 f 的代码在可执行文件中只存在一次。

因此我们节省了可执行文件中的空间

【讨论】:

    【解决方案2】:

    内联只是对编译器的建议吗?

    是的。

    7.1.2 函数说明符

    2 带有 inline 说明符的函数声明(8.3.5、9.3、11.4)声明了一个内联函数。内联 说明符向实现指示函数体在调用点的内联替换 优于通常的函数调用机制。执行此操作不需要实现 调用点的内联替换;但是,即使省略了这个内联替换,其他规则 对于 7.1.2 定义的内联函数,仍应遵守。

    例如来自 MSDN:

    编译器将内联扩展选项和关键字视为建议。不能保证函数会被内联。即使使用 __forceinline 关键字,您也不能强制编译器内联特定函数。使用 /clr 编译时,如果函数应用了安全属性,编译器将不会内联函数。

    请注意:

    3.2 一个定义规则

    3 [...]内联函数应在使用它的每个翻译单元中定义。

    4 内联函数应在使用它的每个翻译单元中定义,并且应具有 在每种情况下都具有相同的定义(3.2)。 [注意:对内联函数的调用可能会在其之前遇到 定义出现在翻译单元中。 —尾注] 如果函数的定义出现在翻译中 单元在其第一次声明为内联之前,该程序格式错误。 如果一个具有外部链接的函数是 在一个翻译单元中声明为 inline,则应在其出现的所有翻译单元中声明为 inline; 不需要诊断。具有外部链接的内联函数应具有相同的地址 翻译单位。外部内联函数中的静态局部变量始终引用同一个对象。 外部内联函数主体中的字符串文字是不同翻译单元中的相同对象。 [注意:出现在默认参数表达式中的字符串文字不在内联函数的主体中 仅仅因为该表达式用于该内联函数的函数调用中。 ——尾注] A型 在外部内联函数的主体中定义的类型在每个翻译单元中都是相同的。

    [注:强调我的]

    TU 基本上是一组标头加上一个实现文件 (.cpp),它指向一个目标文件。

    当你有一个小功能时是否应该明确说明(我 猜1-4条指令?)

    当然。为什么不帮助编译器帮助您生成更少的代码呢?通常,如果 prolog/epilog 部分比让它内联强制编译器生成它们产生更多的成本?但是您必须,绝对必须在开始使用内联之前阅读这篇 GOTW 文章:GotW #33: Inline

    内联写作还有什么其他好处?

    • namespaces 也可以是 inline。请注意,类主体本身中定义的成员函数默认情况下是内联的。隐式生成的特殊成员函数也是如此。

    • 函数模板不能在实现文件中定义(参见FAQ 35.12),除非您提供显式实例化(对于使用模板的所有类型——通常是 PITA IMO)。请参阅Moving Templates Out of Header Files 上的 DDJ 文章(如果您感到奇怪,请阅读其他文章中关于 export 关键字的文章,该关键字已从标准中删除。)

    是否需要声明 inline 以减少可执行文件 大小,即使编译器 (根据维基百科 [我知道,不好 参考])应该找到这样的功能 自己?

    同样,正如我所说,作为一名优秀的程序员,你应该尽可能地帮助编译器。但是here's C++ FAQ 必须提供关于inline 的内容。所以要小心。并非所有编译器都会进行此类分析,因此您应该阅读有关其优化开关的文档。例如:GCC 做了类似的事情:

    您还可以使用选项 -finline-functions 指示 GCC 尝试将所有“足够简单”的函数集成到它们的调用者中。

    大多数编译器允许您在某种程度上覆盖编译器的成本/收益比分析。 MSDNGCC 文档值得一读。

    【讨论】:

    • 您在哪里找到“7.1.2 函数说明符”信息?它指出要遵守其他规则(但我不知道在哪里可以找到它)。哪里好回答。谢谢
    【解决方案3】:

    内联只是对编译器的建议吗?

    是的。但是如果函数有多个定义,则链接器需要它(见下文)

    当你有一个小功能时是否应该明确说明(我猜是1-4条指令?)

    在头文件中定义的函数上(通常)需要它。将它添加到小功能中并没有什么坏处(但我不打扰)。注意在类声明中定义的类成员会自动内联声明。

    内联写作还有什么其他好处?

    如果使用正确,它将停止链接器错误。

    是否需要声明 inline 以减少可执行文件的大小,即使编译器(根据维基百科 [我知道,错误的参考])应该自己找到这样的函数?

    没有。编译器对内联每个函数调用进行成本/收益比较,并做出适当的选择。因此,对函数的调用可能在幕后情况下内联,而在其他情况下不内联(取决于编译器算法的工作方式)。

    速度/空间是两个相互竞争的力量,它取决于编译器正在优化什么,这将决定天气函数是否内联以及可执行文件会增长或缩小。

    还要注意,如果使用过分激进的内联导致程序过度扩展,那么引用的局部性就会丢失,这实际上会减慢程序的速度(因为需要将更多的可执行页面放入内存中)。

    多重定义:

    文件:head.h

    // Without inline the linker will choke.
    /*inline*/       int  add(int x, int y) { return x + y; }
    extern void test()
    

    文件:main.cpp

    #include "head.h"
    #include <iostream>
    
    int main()
    {
        std::cout << add(2,3) << std::endl;
        test();
    }
    

    文件:test.cpp

    #include "head.h"
    #include <iostream>
    
    void test()
    {
        std::cout << add(2,3) << std::endl;
    }
    

    这里我们有两个 add() 的定义。一个在main.o,一个在test.o

    【讨论】:

    • 请记住,如果必须在缓存中保留更多代码,内联实际上可能会降低执行速度。
    • 但如果你有#pragma once#ifndef #define,它会工作.. 对..?
    • @Michael:这无济于事,因为每个编译单元都会重置。
    【解决方案4】:
    1. 是的。仅此而已。
    2. 没有。
    3. 您提示编译器这是一个经常被调用的函数,其中跳转到函数部分会占用大量执行时间。 编译器可能决定将函数代码放在它被调用的地方,而不是普通函数所在的地方。但是,如果函数在 x 处内联,则需要 x 倍于普通函数的空间。
    4. 始终相信您的编译器在过早微优化方面比您自己聪明得多。

    【讨论】:

    • 4:看准了,呵呵..这些我都懂,这也是我的理解。但是约翰内斯想告诉我什么呢?我知道他是对的,我可以从他的声誉中看出这一点:) 但我就是想不通..
    • 4: 添加“...除非你自己是编译器编写者”。
    • @Lothar:怎么样:除非是你自己的编译器?
    • 除非我的编译器输出一些 C 代码,这些 C 代码提供给 C 编译器,我之前通过反汇编生成的代码对代码生成进行了深入测试 :-) 而且我不得不说一般大多数人高估了一个编译器(英特尔不得不支付 1000 亿美元的 EPIC Itanium 投资来了解这一点)。
    • 大多数程序员_mis_estimate编译器的聪明程度。在某些领域,人类击败了编译器,而在其他领域,他们没有机会。就像普通人惊讶于一台计算机在数学上完美,却几乎不能写出可以理解的英语。
    【解决方案5】:

    实际上,内联函数可能会增加可执行文件的大小,因为内联函数代码在每个调用该函数的地方都有重复。对于现代 C++ 编译器,内联主要允许程序员相信他编写了高性能代码。编译器自己决定是否使函数内联。所以,内联写作只是让我们感觉更好......

    【讨论】:

    • @Martin York:编译器的启发式方法比绝大多数人类程序员做出的决定要好得多。
    • @DeadMG:绝对可以(永远不要让人类完成编译器的工作)。我只是想指出绝对声明“内联函数可能会增加可执行文件大小”是不准确的。
    • 好吧,当你知道编译器会默默地忽略它时,这个论点就成立了,对吧? :) 然而,Johannes 表示实际上有一个原因必须写它。我不知道为什么会这样。
    【解决方案6】:

    关于这个:

    而“内联”允许你在程序中多次定义函数。

    我能想到一个有用的例子:使复制保护代码更难破解。如果您有一个程序获取用户信息并根据注册密钥对其进行验证,那么内联进行验证的函数将使破解者更难找到该函数的所有重复项。

    关于其他方面:

    1. inline 只是对编译器的建议,但有 #pragma 指令可以强制内联任何函数。
    2. 由于它只是一个建议,因此明确要求它并让编译器覆盖您的建议可能是安全的。但最好完全省略它,让编译器决定。
    3. 上面提到的混淆是内联的一个可能的好处。
    4. 正如其他人所提到的,inline 实际上会增加编译代码的大小。

    【讨论】:

    • @ShaderOp,我相信你关于内联复制保护功能的论点是有缺陷的。即使编译器内联了这样的函数,该函数将在多少个不同的地方被调用?可能没有那么多,甚至可能只是来自一个地方。 (除非您真的在程序每次执行某些操作时验证用户提供的序列号,而不仅仅是在程序开始时。)那么内联或不内联不会有很大的不同。
    • @stakx:我并没有试图将 inining 作为一种抗裂方法。这只是我在其他地方看到的一个可能的用例,作为增加的混淆层。请参阅本文中“让饼干更难”标题下的“A”项:brandonstaggs.com/2007/07/26/…
    • @ShaderOp, 来自您链接到的文章:"[...] 但也为破解者提供了更多检查的地方 [...] ".这正是我所怀疑的。因此,内联函数只有在从多个不同位置调用时才有意义。
    • @stakx:我相信这篇文章的作者提倡这样做。在同一篇文章的“B”项中,他建议“将真实支票洒在各种操作中。”
    【解决方案7】:
    1. 是的,当它认为函数太大或使用不兼容的特性(可能是异常处理)时,它会很容易地忽略它。此外,通常有一个编译器设置让它自动内联它认为值得的函数(MSVC 中的 /Ob2)。

    2. 如果将函数的定义放在头文件中,应该明确说明。这通常是确保多个翻译单元可以利用它的必要条件。并避免多重定义错误。此外,inline 函数放在 COMDAT 部分。这告诉链接器它只能选择多个定义中的一个。相当于 MSVC 中的 __declspec(selectany)。

    3. 内联函数通常不会使可执行文件变小。由于调用操作码通常小于内联的机器代码,除了非常小的属性访问器样式函数。这取决于但更大的结果并不少见。

    【讨论】:

      【解决方案8】:

      当函数使用引用参数时,内联的另一个好处(请注意,实际内联有时与“内联”指令的使用正交)。将两个变量传递给非内联函数以将其第一个操作数添加到第二个操作数将需要推送第一个操作数的值和第二个操作数的地址,然后调用一个必须弹出第一个操作数和第二个操作数地址的函数,然后将前一个值间接添加到弹出的地址。如果函数是内联扩展的,编译器可以简单地将一个变量直接添加到另一个变量。

      【讨论】:

        【解决方案9】:

        实际上内联会导致更大的可执行文件,而不是更小的可执行文件。 就是通过粘贴函数代码来减少一层间接性。

        http://www.parashift.com/c++-faq-lite/inline-functions.html

        【讨论】:

          猜你喜欢
          • 2010-12-05
          • 2019-11-13
          • 1970-01-01
          • 2015-01-10
          • 1970-01-01
          • 2013-02-02
          • 2011-07-20
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多