内联函数对性能是否还有任何影响?

12 浏览
0 Comments

内联函数对性能是否还有任何影响?

我曾经认为inline已经过时,因为我在这里读到过:

无论你如何将函数标记为inline,这只是一个请求,编译器可以选择忽略:编译器可能会在调用标记为inline的函数的地方进行一些、全部或不进行内联展开。

然而,Angew似乎懂得一些我不懂的东西。在这个问题中,他和我争论了很多次,讨论的是inline是否仍然有用。

这个问题不是关于:

请记住,编译器可以随意进行内联,所以inline在这方面没有帮助:在哪些地方可以使用inline来强制改变编译代码,而不仅仅是建议?

0
0 Comments

C++中的关键字`inline`有两个完全不同的概念。一个是编译器通过在调用点直接重复函数体来替换函数调用的能力,称为函数内联。另一个是在多个翻译单元(即多个`.cpp`文件)中定义函数的可能性,这是`inline`关键字的目的。

历史上,`inline`关键字也是对编译器的强烈建议,告诉它应该将标记为`inline`的函数内联。随着编译器优化能力的提高,这种功能已经逐渐减弱,使用`inline`关键字作为内联函数建议已经过时。编译器会忽略它,并在找到更好的优化时选择完全不同的内联。

`inline`关键字的实际目的是:标记为`inline`的函数可以在多个翻译单元中定义,而不会违反“一个定义规则”(ODR)。如果没有标记为`inline`,在多个翻译单元中定义相同函数将导致链接错误。

所以,`inline`关键字的作用是告诉编译器和链接器:“你们确保对这个函数的多个相同定义不会导致任何错误!”

为了能够将函数定义写入头文件中,必须将其标记为`inline`,否则会导致多重定义错误。

另外,为了能够内联一个函数,编译器必须拥有这个函数的定义。C++遵循分离编译模型,在编译器生成的对象文件之外,编译器无法访问其他对象文件。因此,为了能够内联一个函数,它的定义必须是当前翻译单元的一部分。如果要在多个翻译单元中都能内联它,它的定义必须在所有翻译单元中都存在。通常,这会导致多重定义错误。因此,如果将函数放在头文件中,并在程序的任何地方包含其定义,就必须将其标记为`inline`,以防止多重定义错误。

编译器无法做到这一点,但链接器可以。现代优化技术包括链接时代码生成(Link-Time Code Generation,LTCG),优化器在实际链接之前对所有目标文件进行处理。在此步骤中,所有函数定义当然是可用的,并且完全可以进行内联,而无需在程序中使用任何`inline`关键字。但是,这种优化通常在构建时间上代价高昂,特别是对于大型项目而言。考虑到这一点,仅依靠LTCG进行内联可能不是最佳选择。

总结一下,`inline`关键字的主要作用是允许在多个翻译单元中定义函数,并通过标记函数为`inline`来避免多重定义错误。虽然现代编译器可以自动决定是否内联函数,但仍然需要使用`inline`关键字来启用编译器进行内联的能力。此外,`inline`关键字还允许将函数放在头文件中,从而实现头文件库的存在。

需要注意的是,尽管编译器可以根据需要内联任何函数,但它仍然需要访问函数的定义。因此,虽然`inline`关键字不是必需的提示“请内联此函数”,但如果编译器选择进行内联,您可能仍然需要使用它来使编译器能够进行内联。没有它,您可能无法将定义放入翻译单元中,而没有定义,编译器根本无法内联函数。

最后需要说明的是,`inline`关键字的作用实际上不是为了函数内联,所以为什么它被称为`inline`?原因很简单:要内联一个函数(即在调用点上重复函数体以替换调用),编译器必须首先拥有函数的定义。

总结一下,`inline`关键字的作用是允许在多个翻译单元中定义函数,并通过标记函数为`inline`来避免多重定义错误。尽管现代链接器可以进行函数内联,但完全依赖LTCG进行内联可能不是最佳选择。此外,`inline`关键字还允许将函数放在头文件中,并实现头文件库的存在。

0
0 Comments

在现在,inline主要是一个外部链接说明符,正如你所说的那样。所以,它确实有用,但它的用途与实际内联函数不同。它允许你在编译单元之间多次定义相同的方法,并正确地将它们链接在一起,而不会出现多次定义错误。

强制函数内联实际上取决于编译器,有些编译器可能通过特定的attributepragma(或__forceinline)来支持内联。简单来说,它允许你在头文件中定义函数而不会破坏ODR(One Definition Rule)...

为什么它能够始终适用于外部链接?这是因为编译器/链接器看到否则会发生定义错误。

很多链接器支持"弱定义"的概念,如果一个符号被强定义,链接器将忽略任何弱定义,并在第二个强定义时发出警告。但如果它只是弱定义,链接器将任意选择一个弱定义,如果存在多个定义也不会报错。这是一个非常有用的概念,也可以用于平滑API的差异(虽然它没有被充分利用)。例如,在具有FPU的ARM上,通过FPU寄存器传递浮点值将比通过主CPU寄存器传递更快...

...但在没有FPU的ARM上,必须通过主CPU寄存器传递。可以通过指定接受FPU寄存器中的数据的函数必须使用修改过的名称来使所有调用者都能使用所有方法,并弱定义一个使用未修改名称的函数,该函数将CPU寄存器加载到FPU寄存器中并调用修改过的版本。期望将数据传递给另一个函数的代码将调用具有修改名称的函数,并弱定义一个将数据加载到CPU寄存器中并调用未修改函数的修改版本。对于不了解FPU的代码,它只需使用未修改的名称并期望数据在CPU寄存器中。在这种情况下,任何类型的代码都可以调用任何类型的方法。就我所知,对于ARM来说,没有ABI实际上会像你描述的那样处理FPU的使用,但是这种方法在平滑类似问题时通常很有用。

我不确定是否理解你的意思...你是否可以提出一个单独的问题,并自己回答它?我认为一个你说的例子将非常有帮助,因为我不确定我是否理解"弱定义",所以随后的内容变得越来越模糊。

0