关键词: C/C++ inline
0. 前言
最近在看VPPip4-frag的代码,尤其是同事关于ip4-frag多架构支持的patch,其中的一个改动是将核心调用函数的static改为always_inline,查看always_inline在VPP里的定义,发现always_inline是一个宏,其在VPP的定义如下。那么这个always_inline到底是什么含义?inline关键词到底有什么作用?参考油管上的这个教学视频,用例子对C关键词inline进行一次总结。
1 |
1. 正文
视频包含一个例子,这个例子一个有四个文件,分别是Makefile、test.c、nums.c、nums.h。目前nums.c和nums.h是空文件,test.c的内容如下。可以看到,我们定义了两个非常简单的函数max和two,函数max比较两个整型数的大小,函数two直接返回2。main函数比较命令行传入的参数与2进行比较,返回较大的值。
1 |
|
对应的Makefile文件:
1 | CC=clang |
Makefile文件中通过设置不同的优化选项来生成不同的汇编代码和二进制文件。编译器使用的clang,对应的版本是clang version 10.0.0-4ubuntu1。当然可以用gcc或者其它编译器,不过不同编译器生成的汇编代码可能会有差异。
命令行敲入make指令,可以看到生成以下文件:
1 | make |
执行test0/1/2文件,查看程序执行是否符合预期。
1 | ./test0 1 |
1.1 编译器会自动将简单函数优化成内联汇编代码
以上述例子生成的三个汇编代码中调用two和max函数对应生成的汇编代码为例。当编译器选项为-O0时,可以看到main函数中调用了two和max函数。当编译器选项为-O1时,对应的two函数直接通过movl $2, %edi语句实现,max函数依旧被调用。当编译器选项为-O2时,编译器直接将two和max函数通过内联汇编代码实现。
1 | //test.opt0.s |
1.2 inline关键词只是对编译器的提示
上述的代码还没有涉及inline关键词。我们将代码作如下小修改,将two函数改为inline函数。
1 |
|
敲入make编译发现如下报错。怎么回事?不是说inline关键词会让函数变成内联汇编代码?
1 | /usr/bin/ld: /tmp/test-4ea072.o: in function `main': |
再进一步观察,发现当编译器优化选项是-O0或-O1时编译出错,而-O2选项则编译正常。查阅C99标准文档发现以下一段说明。
An inline definition provides an alternative to an external definition, which a translator may use to implement any call to the function in the same translation unit. It is unspecified whether a call to the function uses the inline definition or the external definition.
啥意思呢?我的理解就是说,对应一个编译单元(如例子中的test.c文件),其中的函数如果被定义成inline,则必须外部有一个同名的函数定义,并且程序在运行时并不确定最后究竟调用的是哪个函数定义。
继续使用上述例子,我们在nums.h和nums.c里新建一个two函数。
1 | // nums.h |
再次编译,发现不论编译器的优化选项是多少,原先的编译错误都消失了。运行程序,得到以下结果。
1 | $ ./test0 1 |
可以看到,不同的优化选项的程序在调用two函数时选用的定义是不同的。对于-O0和-O1,调用的是nums.c里定义的two函数;对于-O2,调用的则是test.c中的inline函数。这种不确定性是我们不想看到的,并且函数的多重定义不仅影响代码阅读,也极易出错,是我们在写代码时应极力避免的。那有什么替代方案吗?
有的,给inline函数添加static关键词,显式地告诉编译器函数的定义就在此编译单元。
1 | // test.c |
重新编译,再次查看对应生成的汇编代码,发现同之前的汇编代码一样。
1 | //test.opt0.s |
上述现象也就印证了标题,同时也如同wikipedia对inline function第一个目的介绍的一样:
It serves as a compiler directive that suggests (but does not require) that the compiler substitude the body of the function inline by performing inline expansion, i.e. by inserting the function code at the address of each function call, thereby saving the overhead of a function call.
1.3 使用__attribute__ ((always_inline))强制内联
既然inline只是对编译器是否内联的提示,那么有没有一种方式,显式地告诉编译器函数就是要内联呢?
答案是有的,使用编译器的属性__attribute__((always_inline)),即使在没有编译器优化的情况下(-O0),函数也会生成内联汇编代码。
继续以原来例子为例,将two函数的定义添加上以上函数属性。
1 | // test.c |
查看生成的汇编代码,可以看到所有的two函数都被生成了内联汇编代码,不再发生函数调用。
1 | //test.opt0.s |
2. 总结
inline函数只是建议编译器生成内联汇编代码,并不是强制。- 使用
static inline函数可避免定义inline函数的同时还需要外部同名函数的定义。 - 如需函数强制生成内联汇编代码,可添加编译器函数属性
__attribute__((always_inline))。
inline函数本质上还是适合执行较为简单但调用频率高的函数,省去了函数调用入栈出栈的开销。但凡事都有利有弊,在程序调试过程中inline函数的信息就会丢失,这就可以看到为什么开头VPP的源码里针对debug和release版本的inline宏定义做了不同的处理。