20220226 inline
明昧 Lv7

2022/02/26 inline相关

总结

  • inline的添加是一个向编译器提出的申请

  • inline的优化是在编译时期发生的

  • 太过复杂的函数(例如:带有循环或者递归的函数)
    所有的virtual函数不会被inline优化成功

  • 新的问题:

    在内存层面inline是怎样实现函数的式的插入,

    或者我们换一个问题,在内存层面,一个函数是怎样调用另外一个函数的

1、inline函数的优点

看起来像函数

动作像函数

比宏要好

没有函数调用所需要的开销

可以享受编译器语境的优化(一般编译器不会对outlined函数调用执行最优化 )

2、inline的函数缺点

可能造成程序体积较大,即使拥有虚拟内存,inling的代码膨胀也会导致额外的换页行为(paging),降低高速缓存装置的击中率(?),以及伴随而来的效率损失。

3、关于inline的一些规则

  • 总规则:inline只是对编译器的一个申请,不是强制命令。

  • 规则一:inline的两种方式:隐喻方式,明确提出方式。

隐喻方式,即在class声明内定义函数,这样的函数通常是成员函数。friend 函数也可以写在class定义内。
明确声明,即在函数定义前加关键字inline。

  • 规则二:什么函数不会被inline

    太过复杂的函数(例如:带有循环或者递归的函数)
    所有的virtual函数。(virtual运行期才能知道调用哪个版本,而大部分编译器inline是在编译器完成的。)

注意:一般你申请inline的函数,编译器没有实现inline的话,会给出一条警告信息。

  • 规则三:什么时候编译器会既inline函数,又生成outline版本的函数

    情况一:你使用指针调用了该函数
    情况二:你没有使用指针调用该函数,但是编译器生成的函数使用指针(例如:构造和析构函数)调用了该函数

  • 规则四:为什么派生类的构造函数和析构函数通常并不是inline很好的候选者**(?)**

因为C在对象创建和销毁时做了各种各样的保证,例如一个对象创建了一半后引发了异常,那么C需要把创建好的那部分给析构掉。这实际上都是有代码在执行的,这些编译器自己生成的代码往往被编译器放在构造函数和析构函数中。而一个派生类就容易发生这样的异常。因此,派生类的构造和析构函数并不是inline的很好的候选者。

  • 规则五:为什么基类的构造函数和析构函数通常并不是inline很好的候选者

如果base构造函数被写成inline 的,那么所有的替换base构造函数的代码会被插入到派生类构造函数的调用内。

4、对于inline的一个误解

(1)inline和function template是没关系的

由于function template函数和inline函数一般都位于头文件内,误使得我们认为function template一定是inline 的。这完全是错误的,inline和function template是没关系的。 下面解释为什么二者通常都放在头文件内。

(2)为什么inline函数需要放在头文件内?

大多数的编译环境在编译期过程中进行inling,而为了将一个“函数调用”替换为“被调用函数本体”,编译器需要知道函数长什么样。(有些特别的编译环境在连接器甚至是运行期进行inling,但是大多数编译器都是在编译期。)

(3)为什么function Templates 通常也被置于头文件内?

因为它一旦被使用,编译器为了将它局现化,需要知道它长什么样子。(但是Templates 的具现化也不一定都在编译期,有些在连接期。)

(4)什么时候function template需要inline?

如果你写的function template 具现化的函数都应该是inlined的,那么需要将此function template 写成inline形式。如果你写的function template 具现化的函数不都是inlined 的就不应该写成inline形式。

5、inline带来的其他影响*

(1)程序库设计者必须评估将函数声明为inline 的冲击:代码升级

inline函数无法随着程序库的升级而升级。

也就是说一样inline函数升级了,所有用到inline函数的客户端代码都要重新编译。

但是如果此函数时outinlined 那么只需要重新连接就行了

如果此函数是动态连接的,那么可以无声无息的被应用程序吸纳

(2)程序开发人员对于inline 的注意:代码调试

大部分调试器都对于inline函数束手无策,

例如你不能在一个不存在的函数内设置断点。大部分调试器禁止在调试程序中发生inlining。

6、最后:对于inline的整体策略

一开始不要写任何inline,或者说一定可以inline的才inline。

然后根据20-80法则,对于20那部分代码,竭尽所能的将其inline或瘦身,以提高代码速度。

原文链接:https://blog.csdn.net/lintianyi9921/article/details/103882776

inline和template

由于inline函数和template函数之间有些相同的特点,因此在学习C的时候经常弄混inline函数和template函数的一些特点,读过Effective C后对两者的概念有了较清楚的了解,在此记下来,方便以后查阅:

  • 相同点:inline函数和template函数通常都被定义于头文件内。
    原因:Inline函数通常一定被内置于头文件内,因为大多数建置环境(build environments)在编译过程中进行inlining(内联展开),而为了将一个“函数调用”替换为“被调用函数的本体”,编译器必须知道那个函数长什么样子。
    Template通常也被置于头文件内,因为它一旦被调用,编译器为了将它具现化,需要知道它长什么样。

  • 不同点:Template的具现化与inlining无关。
    原因:如果你正在写一个template而你认为所有根据此template具现出来的函数都应该inlined,请将此template声明为inline;但如果你写的template没有理由要求它所具现的每一个函数都是inlined,就应该避免将这个template声明为inline(不论显示或隐式),因为inlining需要成本,比如引发代码膨胀等。

    据说现代编译器会通过判断函数被调用的频率和函数的复杂程度决定是否需要进行inline优化

内存层面inline的理解

在内存层面,函数调用和内联函数的实现涉及多个步骤,包括函数地址的计算、栈的操作和代码的跳转。以下是关于函数调用和内联函数展开的详细解释:

函数调用的内存层面实现

当一个函数调用另一个函数时,涉及以下几个步骤:

  1. 保存当前环境
    • 调用函数前,CPU 会保存当前的执行环境,包括当前指令地址(程序计数器)、当前函数的局部变量和一些寄存器的内容。通常,这些信息会被压入栈中。
  2. 传递参数
    • 被调用函数的参数会被传递到栈上或通过寄存器传递。具体传递方式取决于编译器和目标平台的调用约定(calling convention)。
  3. 跳转到被调用函数
    • CPU 跳转到被调用函数的入口地址。这个地址通常是通过一个指令实现的,例如在 x86 架构中使用 CALL 指令。
  4. 执行被调用函数
    • 被调用函数执行其代码。当函数执行完毕后,会通过 RET 指令返回到调用函数。这条指令会从栈中弹出返回地址,并跳转到该地址继续执行。
  5. 恢复环境
    • CPU 恢复之前保存的执行环境,包括程序计数器和寄存器的内容。

函数调用示例(汇编)

假设我们有以下简单的 C++ 代码:

1
2
3
4
5
6
7
8
9
int add(int a, int b) {
return a + b;
}

int main() {
int result = add(5, 10);
return 0;
}

对应的汇编代码(x86 架构)可能如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
main:
push ebp
mov ebp, esp
sub esp, 16
mov DWORD PTR [ebp-4], 5
mov DWORD PTR [ebp-8], 10
mov eax, DWORD PTR [ebp-4]
mov edx, DWORD PTR [ebp-8]
push edx
push eax
call add
add esp, 8
mov DWORD PTR [ebp-12], eax
leave
ret

add:
push ebp
mov ebp, esp
mov eax, DWORD PTR [ebp+8]
mov edx, DWORD PTR [ebp+12]
add eax, edx
pop ebp
ret

内联函数的内存层面实现

内联函数是在编译时由编译器直接将函数的代码插入到每个调用该函数的地方,而不是生成一个函数调用指令。这消除了调用和返回的开销。

假设我们将上面的 add 函数标记为 inline

1
2
3
4
5
6
7
8
9
inline int add(int a, int b) {
return a + b;
}

int main() {
int result = add(5, 10);
return 0;
}

编译器在处理时,会直接将 add 函数的代码插入到 main 函数中,从而避免了函数调用的开销。对应的汇编代码可能如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
main:
push ebp
mov ebp, esp
sub esp, 16
mov DWORD PTR [ebp-4], 5
mov DWORD PTR [ebp-8], 10
mov eax, DWORD PTR [ebp-4]
mov edx, DWORD PTR [ebp-8]
add eax, edx <====看这里,函数入口被直接放在这里了
mov DWORD PTR [ebp-12], eax
leave
ret

可以看到,add 函数的代码直接插入到了 main 函数中,消除了 callret 指令,从而提高了执行效率。

结论

在内存层面,常规函数调用涉及保存和恢复执行环境、传递参数、跳转和返回地址等操作。而 inline 函数则通过在编译时将函数代码直接插入调用点,避免了这些开销。这种优化适用于小而频繁调用的函数,可以显著提高程序的性能。

 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View