何时在C语言中使用"inline"是无效的?

15

有些人喜欢在 C 中使用 inline 关键字,并将大函数放在头文件中。你认为什么情况下这样使用是无效的?我有时候觉得这样使用甚至很让人恼火,因为这很不寻常。

我的原则是,inline 应该用于访问非常频繁的小函数,或者为了进行实际类型检查。不管怎样,我的品味指导我,但我不确定如何最好地解释为什么对于大函数来说,inline 不是那么有用。

这个问题中,有人建议编译器可以更好地猜测正确的操作方式。这也是我的假设。当我尝试使用这个参数时,人们回复说它在使用不同对象的函数时行不通。嗯,我不知道(例如,使用 GCC)。

感谢您的回答!


我怀疑就像“register”在20世纪80年代末期变得过时一样,“inline”已经过时了好几年。 - Paul Tomblin
内联绝对不过时。你有没有读过现代内核的源代码?对于编写应用程序的人来说,内联是无关紧要的,但这些人现在也不应该使用C语言了,而对于系统编程来说,它仍然像以往一样相关。 - nosatalian
12个回答

26

inline有两个作用:

  1. 免除“单一定义规则”的限制(见下文)。这个作用总是存在。
  2. 给编译器一个提示,避免函数调用。编译器可以选择忽略这个提示。

#1非常有用(例如,如果定义很短,可以将其放在头文件中),即使#2被禁用也是如此。

实际上,编译器通常会更好地自行计算何时进行内联(特别是如果可用的话,进行基于性能分析的优化)。


以上两点均源自ISO/ANSI标准(ISO/IEC 9899:1999(E),通常称为“C99”)。

在§6.9“外部定义”,第5段中:

外部定义是同时也是函数(不是内联定义)或对象的定义的外部声明。如果具有外部链接的标识符在表达式中使用(除了作为sizeof运算符的操作数之一,其结果是整数常量),则整个程序中必须恰好有一个标识符的外部定义;否则,就不应该有超过一个。

虽然C++中的等效定义明确命名为单一定义规则(ODR),但它具有相同的目的。外部变量(即非“static”,因此仅局限于单个翻译单元 - 通常是单个源文件)只能定义一次,除非它是一个函数并且是内联的。

在§6.7.4“函数说明符”中,定义了内联关键字:

将函数设置为内联函数意味着对该函数的调用尽可能快。[118]这样的建议的有效程度是实现定义的。

脚注(非规范性),但提供了澄清:

通过使用替代通常的函数调用机制(例如“内联替换”),可以实现上述目标。内联替换不是文本替换,也不会创建新函数。因此,在函数体中使用宏扩展时,使用的定义是函数体出现时的定义,而不是函数被调用的地方;标识符也引用了发生在该函数体范围内的声明。同样,函数只有一个地址,不管外部定义之外有多少内联定义。

总结:大多数C和C++用户期望的内联功能并非他们所得到的。它的表面主要目的是避免功能调用开销,但这是完全可选的。但为了允许单独编译,需要放宽单一定义。

(标准中的所有强调都是引用)


编辑2:一些注意事项:

  • 外部内联函数有各种限制。您不能在函数中有静态变量,并且不能引用静态TU范围的对象/函数。
  • 刚刚在VC++的 "整个程序优化" 上看到了这一点,这是编译器自己进行内联处理的示例,而不是作者。

我甚至没有考虑过#1,但你是对的 - 非常有用!感谢您的提示。 - Mike
1
@Christoph:那只是gcc。我刚在C99标准文档中检查过了。 - Richard
据我所知,C语言没有像C++那样的一次定义规则。在C语言中并不像C++那么清晰(C++有ODR),它说内联函数定义实际上可以出现在头文件中,并且可以被包含在每个编译单元中并使用。 - Johannes Schaub - litb
例如,在C99中规定,如果您在一个TU中有内联函数定义,则仍必须在另一个TU中具有外部函数定义。如果调用该函数,则未指定使用哪个版本(外部或内联函数定义)。 - Johannes Schaub - litb
Richard,我认为TC2的6.9“外部定义”(我这里有2005年的n1124)是合适的。对于内联,6.7.4是合适的。我喜欢C++的ODR,因为它将所有东西都总结在一个地方 :) C似乎只在各自的章节中定义了这个。 - Johannes Schaub - litb
显示剩余8条评论

5
一个例子来说明inline的好处。sinCos.h:
int16 sinLUT[ TWO_PI ]; 

static inline int16_t cos_LUT( int16_t x ) {
    return sin_LUT( x + PI_OVER_TWO )
}

static inline int16_t sin_LUT( int16_t x ) {
    return sinLUT[(uint16_t)x];
}

当进行大量数学计算时,如果您想避免浪费时间计算sin/cos,则可以用LUT替换sin/cos。

如果您在不使用inline的情况下编译,则编译器不会优化循环,输出的.asm文件将显示以下内容:

;*----------------------------------------------------------------------------*
;*   SOFTWARE PIPELINE INFORMATION
;*      Disqualified loop: Loop contains a call
;*----------------------------------------------------------------------------*

当你使用inline编译时,编译器会知道循环中发生的事情并进行优化,因为它清楚地知道正在发生什么。
输出.asm将有一个经过优化的“流水线”循环(即它将尝试充分利用处理器的所有ALU,并尝试保持处理器的流水线充满而不需要NOPS)。
在这种特定情况下,我能够将我的性能提高约2倍或4倍,这使我达到了实时截止日期所需的要求。
附言:我正在使用固定点处理器...任何浮点运算如sin/cos都会降低我的性能。

5

内联声明的重要之处在于它不一定会执行任何操作。编译器可以自由决定是否内联未声明的函数,并链接已声明为内联的函数。


即使您链接的内联函数来自不同的.o对象,也可以吗? - elmarco
inline修饰符只是一个提示。如果编译器选择,它可能会决定一个函数不能被内联,但链接器仍然可以决定将函数调用重写为内联。使用inline没有任何行为保证。 - SingleNegationElimination

5

不应该在大型函数中使用内联的另一个原因是库。每次更改内联函数时,可能会丢失ABI兼容性,因为已经针对旧版本头文件编译的应用程序仍然内联了旧版本的函数。如果将内联函数用作类型安全的宏,那么很有可能在库的生命周期内永远不需要更改该函数。但对于大型函数来说,这很难保证。

当然,这个论点仅适用于函数是公共API的情况。


4

当您使用函数指针时,内联是无效的。


3

仅在以下情况下内联函数才是有效的:当您遇到性能问题时,使用真实数据运行分析器,并发现某些小函数的函数调用开销非常大。

除此之外,我无法想象为什么要使用它。


哎呀,我抄袭了你的答案。但这事本身就表明你提供了很有见地的意见,所以点个赞吧。 :-) - T.E.D.
有时候你不需要运行分析器就能知道开销很大。例如,对我来说,原子增量函数应该是内联的,这也会减小代码大小,这是相当明显的。 - Dietrich Epp

2

没错。在大型函数中使用内联会增加编译时间,并且对应用程序的性能带来很少的额外提升。 内联函数用于告诉编译器要包含一个函数而不需要调用,因此它们应该是重复多次的小代码。 换句话说:对于大型函数来说,与自身函数实现成本相比,进行函数调用的成本可以忽略不计。


正如之前提到的,inline 用于“建议”编译器在没有调用的情况下包含函数。但并不能保证它实际上会这样做。 - Peter

2

内联函数可以用于小型和频繁使用的功能,例如 getter 或 setter 方法。对于大型函数,不建议使用内联,因为它会增加可执行文件的大小。此外,对于递归函数,即使您将其设置为内联,编译器也会忽略它。


2

我主要使用内联函数作为类型安全的宏。有关于GCC添加链接时优化支持的讨论已经进行了一段时间,特别是自LLVM出现以来。不过我不知道实际上已经有多少被实现了。


2

个人认为在没有经过代码分析证明某一例程会存在瓶颈,且通过内联可以部分缓解该瓶颈时,不应该进行内联。

这又是Knuth所警告的过早优化问题的一个案例。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接