10

C语言陷阱与技巧第2节,inline函数介绍,demo详解

 3 years ago
source link: https://blog.popkx.com/c-language-traps-and-skills-section-2-introduction-to-inline-functions-demo-details/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

打开 Linux 内核源代码,会发现内核在定义C语言函数时,有很多都带有 “inline”关键字,请看下图,那么这个关键字有什么作用呢?

9cdb98786d548ab2b88313bf66a587e6.png

inline 关键字的作用

在C语言程序开发中,inline 一般用于定义函数,inline 函数也被称作“内联函数”,C99 和 GNU C 均支持内联函数。那么在C语言中,内联函数和普通函数有什么不同呢?其实,从 inline 这个名字就应该能看出一点它的性质了——内联函数会在它被调用的位置上展开,这一点表现的和 define 宏定义是非常相似的。

将被调用的函数代码展开,操作系统就无需再在为被调用函数做申请栈帧和回收栈帧的工作,而且,由于编译器会把被调用的函数代码和函数本身放在一起优化,所以也有进一步优化C语言代码,提升效率的可能。

每发生一次函数调用,操作系统就要在程序的栈空间申请一块内存区域(栈帧),供被调用函数使用,被调用函数执行完毕后,操作系统还要回收这些内存。

f9c0c2b38977b70974f6cebca51a90cc.png

不过,天下没有免费的午餐,C语言程序要实现内联函数的上述特性是要付出一定的代价的。普通函数只需要编译出一份,就可以被所有其他函数调用,而内联函数没有严格意义上的“调用”,它只是将自身的代码展开到被调用处的,这么做无疑会使整个C语言代码变长,也就意味着占用更多的内存空间,以及更多的指令缓存。

显然,如果滥用内联函数,cpu 的指令缓存肯定是不够用的,这会导致 cpu 缓存命中率降低,反而可能会降低整个C语言程序的效率。因此,建议把那些对时间要求比较高,且C语言代码长度比较短的函数定义为内联函数。如果在C语言程序开发中的某个函数比较大,又会被反复调用,并且没有特别的时间限制,是不适合把它做成内联函数的。

在 Linux 内核中,内联函数常常使用 static 修饰,例如:

static inline void set_value(unsigned int val)
{
    ...
}

需要注意的是,内联函数必须在使用之前就定义好,否则编译器没法把这个函数展开。Linux 内核中经常像下面这样,将内联函数放在调用它的函数前面,请看C语言代码:

static inline void set_value(unsigned int val)
{
    ...
}
int test_inline()
{
    set_value(3);
    ...
}
a79906c11a96b41c22030b5d24735cf8.png

所以,Linux 内核常常把内联函数定义在头文件里,这样在其他C语言代码文件开头包含头文件时,能确保内联函数在文件的最开始,无需再写额外的声明语句。

这也解释了为什么 Linux 内核为何常常使用 static 修饰内联函数,因为可以避免函数的重复定义。

前文提到内联函数的表现有些像 define 宏定义,但是为了类型安全和易读性,应优先使用内联函数而不是复杂的宏。下面通过实例进一步分析 inline 内联函数的特性。

inline内联函数的“展开代码”是什么意思?

使用过 define 写 C语言代码的朋友应该都知道,编译器在编译 C语言代码时,会将 define 定义的宏展开,而不是像普通函数那样使用 call 指令调用,例如下面这段C语言代码:

#include <stdio.h>

#define     d_add(a, b)     ((a)+(b))
int f_add(int a, int b)
{
     return a+b;
}
int main()
{
     int a = d_add(1, 2);
     int b = f_add(1, 2);

     return 0;
}
bd6a11e9460494a9743b4e323656671c.png

使用 gcc -E 编译这段C语言代码,能够得到预处理后的代码如下,显然 define 定义的宏被展开了,请看:
f617bdeb99ddcd548791889aad7a633d.png

使用 gcc -g 命令编译C语言代码,得到可执行文件,然后调用 objdump 命令查看汇编代码,得到如下结果:
# gcc -g t1.c 
# objdump -dS a.out 
e00704b9af0e2a81791784ba36764304.png

从 f_add() 函数的汇编代码也可以看出,程序首先将 2 个参数赋值给寄存器,然后使用 call 指令调用 f_add() 函数。而宏定义 d_add() 就简单了,只有一行汇编代码,这种情况下,使用 define 宏定义显然效率更高。不过,宏定义没有参数的类型检查,使用起来不太安全,好在C语言还有 inline 函数,下面再定义一个 inline 函数,请看C语言代码如下:
static inline int i_add(int a, int b)
{
     return a+b;
}
1fa09cee3783658d9feaf0334af9c58a.png

在 main() 函数中使用 gcc -E 命令查看添加 inline 函数后的C语言代码预处理结果,如下:
0deb7c8d71eec87f17b30b5f9065fe46.png

可以看出,在预处理阶段,inline 函数并没有像 define 宏那样展开。现在使用 gcc -g 命令编译得到可执行文件,然后使用 objdump 查看汇编代码,如下:
4b32dd557c3f21d9fc4d5efef7150bf9.png

从汇编代码可以看出,inline 函数似乎并没有起到作用,i_add() 函数和 f_add() 函数的表现并没有什么不同,继续往上查看,发现编译器也将 i_add() 函数的汇编代码生成了,这无疑是将 i_add() 函数当作普通函数使用了:
static inline int i_add(int a, int b)
{
  400501:   55                      push   %rbp
  400502:   48 89 e5                mov    %rsp,%rbp
  400505:   89 7d fc                mov    %edi,-0x4(%rbp)
  400508:   89 75 f8                mov    %esi,-0x8(%rbp)
    return a+b;
  40050b:   8b 45 f8                mov    -0x8(%rbp),%eax
  40050e:   8b 55 fc                mov    -0x4(%rbp),%edx
  400511:   01 d0                   add    %edx,%eax
}
  400513:   5d                      pop    %rbp
  400514:   c3                      retq

怎么回事?不是说 inline 函数的表现和 define 宏相似,会将函数代码展开吗?其实,inline 只是建议编译器这么做,编译器究竟会不会这么做就不一定了。这与编译器的优化级别相关,请看下图:

059c65c015bd51bca26219760f7f5038.png

gcc 的 -O 选项可以指定优化级别,我们上面编译程序时没有使用 -O 选项,因此编译器执行的是默认的 -O0,也即无优化编译。那能否在 -O0 优化级别也使用 inline 函数的特性呢?当然是可以的,只需要在定义 inline 函数时,添加 __attribute__((always_inline)) 即可,例如:
static __attribute__((always_inline)) inline int i_add(int a, int b)
{
     return a+b;
}
aba255ad5f1e144c170700b7c063b5ad.png

现在再来编译C语言程序并查看汇编代码,得到如下结果:
eee28be99de1d60e85e042f009eeb88d.png

这种情况下,编译器并没有为 i_add() 函数生成响应的汇编代码。虽然 inline 函数在预处理阶段没有像 define 宏定义那样展开,但是在生成汇编代码阶段展开了,而且参与了调用它的代码部分的优化,这显然会让整个C语言程序的效率提高。

inline 函数虽然表现上很像 define 宏定义,但是却并不能完全取代 define 宏定义,这一点在我之后的文章里会讨论,敬请关注。

在 C语言程序开发中,建议把那些对时间要求比较高,且C语言代码长度比较短的函数定义为 inline 函数,这么做常常可以提升程序的效率。在默认的 -O0 编译优化项不能确保 inline 一定起作用,但是可以添加添加 __attribute__((always_inline))强制编译器对 inline 函数做相应的处理。因为 inline 函数会将自己展开,所以编译器通常不会再为 inline 生成汇编代码,不过,如果是通过函数指针的形式调用 inline 函数,编译器为了获得 inline 函数的地址,仍然会为其生成汇编代码的。

3dcdb3f8f5f0b900ac7be4991d65fe19.png

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK