关键词: 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
宏定义做了不同的处理。