文章内容全部来自–>《嵌入式C语言自我修养——从芯片、编译器到操作系统》 王利涛前辈的,超级推荐
内联函数
这个万一就是真的有点意思了,来来接着看看
这一节,我们接着介绍与内联函数相关的两个属性:noinline和always_inline。这两个属性的用途是告诉编译器,在编译时,对我们指定的函数内联展开或不展开。
static inline __attribute__((noinline)) int func();
static inline __attribute__((always_inline)) int func();
一个使用inline声明的函数被称为内联函数,内联函数一般前面会有static和extern修饰。
使用inline声明一个内联函数,和使用关键字register声明一个寄存器变量一样,只是建议编译器在编译时内联展开。
**使用关键字register修饰一个变量,只是建议编译器在为变量分配存储空间时,将这个变量放到寄存器里,**这会使程序的运行效率更高。
当然以上都是建议,至于会不会这样做,视具体情况而定,编译器要根据寄存器资源是否紧张、这个变量的类型及是否频繁使用来做权衡。
使用inline时也是一样,编译器也会根据实际情况,如函数体大小、函数体内是否有循环结构、是否有指针、是否有递归、函数调用是否频繁来做决定。
如GCC编译器,一般是不会对函数做内联展开的,只有当编译优化等级开到-O2以上时,才会考虑是否内联展开。
但是在我们使用noinline和always_inline对一个内联函数作显式属性声明后,编译器的编译行为就变得确定了:使用noinline声明,就是告诉编译器不要展开;使用always_inline属性声明,就是告诉编译器要内联展开。
在说这个事情之前,你得先明白个事情,函数在执行得过程中去调用其他的函数时是有很大得开销的。
看看调用过程:
(1)保存当前函数现场。
(2)跳到调用函数执行。
(3)恢复当前函数现场。
(4)继续执行当前函数。
需要不断地保存现场、恢复现场,这就是函数调用带来的开销。
对于一般的函数来说这个事情也就那么回事,但是如果你整个只有一句的短小精悍的代码,这样反复调用很不划算啊。
于是我们将这样的函数设置为内联函数,像宏一样在调用出直接展开执行,这样就可以减少函数调用的开销。
那为啥不直接整个宏算了?
来嘛来看看内联函数独特的优势:
● 参数类型检查:内联函数虽然具有宏的展开特性,但其本质仍是函数,在编译过程中,编译器仍可以对其进行参数检查,而宏不具备这个功能。
● 便于调试:函数支持的调试功能有断点、单步等,内联函数同样支持。
● 返回值:内联函数有返回值,返回一个结果给调用者。这个优势是相对于ANSI C说的,因为现在宏也可以有返回值和类型了,如前面使用语句表达式定义的宏。
● 接口封装:有些内联函数可以用来封装一个接口,而宏不具备这个特性。
看来还是很具有优势的,那我们看看编译器对内联函数的处理
虽然可以通过inline关键字将一个函数声明为内联函数,但编译器不一定会对这个函数做内联展开。编译器也要根据实际情况进行评估,**权衡展开和不展开的利弊,**并最终决定要不要展开。
内联函数并不是完美无瑕的,也有一些缺点。
内联函数会增大程序的体积,如果在一个文件中多次调用内联函数,多次展开,那么整个程序的体积就会变大,在一定程度上会降低程序的执行效率。同时内联函数往往降低了函数的复用性。
编译器在对内联函数做展开时,除了检测用户定义的内联函数内部是否有指针、循环、递归,还会在函数执行效率和函数调用开销之间进行权衡。
考虑因素:
● 函数体积小。
● 函数体内无指针赋值、递归、循环等语句。
● 调用频繁。
当我们认为一个函数体积小,而且被大量频繁调用,应该做内联展开时,就可以使用static inline关键字修饰它。但编译器不一定会做内联展开,如果你想明确告诉编译器一定要展开,或者不展开,就可以使用noinline或always_inline对函数做一个属性声明。
如果你曾经看过这个玩意,你会发现其很多的内联函数都在头文件中,为什么?还非得用个static修饰一下。
因为它是一个内联函数,可以像宏一样使用,任何想使用这个内联函数的源文件,都不必亲自再去定义一遍,直接包含这个头文件,即可像宏一样使用。
使用inline定义的内联函数,编译器不一定会内联展开,那么当一个工程中多个文件都包含这个内联函数的定义时,编译时就有可能报重定义错误。而使用static关键字修饰,则可以将这个函数的作用域限制在各自的文件内,避免重定义错误的发生。
(static 修饰了那这个外面的函数不久用不成了?包含头文件不就可以了,static限制了使用的范文,定义在头文件,可以让其他文件包含。)
【学习资料】《嵌入式C语言自我修养——从芯片、编译器到操作系统》