10.1 属性声明:noinline & always_inline

这一节,接着讲 __atttribute__ 属性声明,__atttribute__ 可以说是 GNU 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 属性声明,就是告诉编译器,要内联展开。

什么是内联展开呢?我们不得不说一下内联函数的基础知识。

10.2 什么是内联函数

函数调用开销

说起内联函数,又不得不说函数调用开销。一个函数在执行过程中,如果需要调用其它函数,一般会执行下面这个过程。

  • 保存当前函数现场
  • 跳到调用函数执行
  • 恢复当前函数现场
  • 继续执行当前函数

比如一个 ARM 程序,在一个函数 f1() 中,我们对一些数据进行处理,运算结果暂时保存在 R0 寄存器中。接着要调用另外一个函数 f2(),调用结束后,接着返回到 f1() 函数中继续处理数据。如果我们在 f2() 函数中使用到 R0 这个寄存器(用于保存函数的返回值),此时就会改变 R0 寄存器中的值,那么就篡改了 f1() 函数中的暂存运算结果。当我们返回到 f1() 函数中继续进行运算时,结果肯定不正确。

那怎么办呢?很简单,在跳到 f2() 执行之前,先把 R0 寄存器的值保存到堆栈中,f() 函数执行结束后,再将堆栈中的值恢复到 R0 寄存器中,这样 f1() 函数就可以接着继续执行了,就跟什么事情都没发生过一样。

这种方法证明是 OK 的,现代计算机系统,无论是什么架构和指令集,都是采用这种方法。虽然麻烦了点,但至少能解决问题,无非就是多花点代价,需要不断地保存现场、恢复现场,这就是函数调用带来的开销。

内联函数的好处

对于一般的函数调用,这种方法是没有问题的。但对于一些极端情况,比如说一个函数很小,函数体内只有一行代码,而且被大量频繁的调用。如果每次调用,都不断地保存现场,执行时却发现函数只有一行代码,又要恢复现场,往往造成函数开销比较大,性价比不高。这就跟你去五星级饭店订个餐位吃饭一样,VIP 包间、刀叉餐具、空调、服务人员都准备好了,你到了之后只要了一碗面条,吃完之后抹嘴走人,而且一天三顿你都这么干,你说服务员烦不烦?

函数调用也是如此。有些函数很小,而且调用频繁,调用开销大,算下来性价比不高。我们就可以将这个函数声明为内联函数。编译器在编译过程中遇到内联函数时,像宏一样,将内联函数直接在调用处展开。这样做的好处就是减少了函数调用开销,直接执行内联函数展开的代码,不用再保存现场、恢复现场。

10.3 内联函数与宏

看到这里,可能就有人纳闷了,内联函数既然跟宏的功能差不多,那为什么不直接定义一个宏,而去定义一个内联函数呢?

存在即合理,内联函数既然在 C 语言中广泛应用,自然有它存在的道理。相对于宏,内联函数有以下几个优势。

  • 参数类型检查。内联函数虽然具有宏的展开特性,但其本质仍是函数,编译过程中,编译器仍可以对其进行参数检查,而宏就不具备这个功能。
  • 便于调试。函数支持的调试功能有断点、单步……,内联函数也同样可以。
  • 返回值。内联函数有返回值,返回一个结果给调用者。这个优势是相对于 ANSI C 说的。不过现在宏也可以有返回值和类型了,比如前面我们使用语句表达式定义的宏。
  • 接口封装。有些内联函数可以用来封装一个接口,而宏不具备这个特性。

10.4 编译器对内联函数的处理

前面也讲过,我们虽然可以通过 inline 关键字,将一个函数声明为内联函数,但编译器不一定会对这个内联函数展开处理。编译器也要进行评估,权衡展开和不展开的利弊。

内联函数并不是完美无瑕,也有一些缺点。比如说,会增大程序的体积。如果在一个文件中多次调用内联函数,多次展开,那整个程序的体积就会变大,在一定程度上,会造成 CPU 的取址效率降低,程序执行效率降低。函数的作用之一就是提高代码的复用性,我们将常用的一些代码或代码块封装成函数,进行模块化编程,而内联函数往往是降低了函数的复用性。所以编译器在对内联函数作展开处理时,除了检测用户定义的内联函数内部是否有指针、循环、递归外,还会在函数执行效率和函数调用开销之间进行权衡。一般来讲,判断对一个内联函数到底展不展开,从程序员的角度,主要考虑以下几个因素。

  • 函数体积小且调用频繁
  • 函数体内无递归、循环等语句
  • 函数本身当作一个函数指针在别处被引用
  • 函数和调用该函数的caller是否在同一文件内

当我们认为一个函数体积小,而且被大量频繁调用,应该做内联展开时,就可以使用 static inline 关键字修饰它。但编译器会不会作内联展开,编译器也会有自己的权衡。如果你想告诉编译器一定要展开,或者不作展开,就可以使用 noinline 或 always_inline 对函数作一个属性声明。

//inline.c
static inline
__attribute__((always_inline)) int func(int a)
{
return a+;
}

static inline void print_num(int a)
{
printf("%d\n",a);
}
int main(void)
{
int i;
i=func();
print_num();
return ;
}

在这个程序中,我们分别定义两个内联函数 func() 和 print_num(),然后使用 always_inline 对 func() 函数进行属性声明。接下来,我们对生成的可执行文件 a.out 作反汇编处理,其汇编代码如下。

$ arm-linux-gnueabi-gcc -o a.out inline.c
$ arm-linux-gnueabi-objdump -D a.out
<print_num>:
: e92d4800 push {fp, lr}
1043c: e28db004 add fp, sp, #
: e24dd008 sub sp, sp, #
: e50b0008 str r0, [fp, #-]
: e51b1008 ldr r1, [fp, #-]
1044c: e59f000c ldr r0, [pc, #]
: ebffffa2 bl 102e0 <printf@plt>
: e1a00000 nop ; (mov r0, r0)
: e24bd004 sub sp, fp, #
1045c: e8bd8800 pop {fp, pc}
: 0001050c andeq r0, r1, ip, lsl #

<main>:
: e92d4800 push {fp, lr}
: e28db004 add fp, sp, #
1046c: e24dd008 sub sp, sp, #
: e3a03003 mov r3, #
: e50b3008 str r3, [fp, #-]
: e51b3008 ldr r3, [fp, #-]
1047c: e2833001 add r3, r3, #
: e50b300c str r3, [fp, #-]
: e3a0000a mov r0, #
: ebffffea bl <print_num>
1048c: e3a03000 mov r3, #
: e1a00003 mov r0, r3
: e24bd004 sub sp, fp, #
: e8bd8800 pop {fp, pc}

通过反汇编代码可以看到,因为我们对 func() 函数作了 always_inline 属性声明,所以编译器在编译过程中,对于 main()函数调用 func(),会直接在调用处展开。

   :    e3a03003    mov r3, #
: e50b3008 str r3, [fp, #-]
: e51b3008 ldr r3, [fp, #-]
1047c: e2833001 add r3, r3, #
: e50b300c str r3, [fp, #-]

而对于 print_num() 函数,虽然我们对其作了内联声明,但编译器并没有对其作内联展开,而是当作一个普通函数对待。还有一个注意的细节是,当编译器对内联函数作展开处理时,会直接在调用处展开内联函数的代码,不再给 func() 函数本身生成单独的汇编代码。这是因为其它调用该函数的位置都作了内联展开,没必要再去生成。在这个例子中,我们发现就没有给 func() 函数本身生成单独的汇编代码,编译器只给 print_num() 函数生成了独立的汇编代码。

10.5 思考:内联函数为什么常使用 static 修饰?

在 Linux 内核中,你会看到大量的内联函数定义在头文件中,而且常常使用 static 修饰。

为什么 inline 函数经常使用 static 修饰呢?这个问题在网上也讨论了很久,听起来各有道理,从 C 语言到 C++,甚至有人还拿出了 Linux 内核作者 Linus 作者关于对 static inline 的解释:

"static inline" means "we have to have this function, if you use it, but don't inline it, then make a static version of it in this compilation unit". "extern inline" means "I actually have an extern for this function, but if you want to inline it, here's the inline-version".

我的理解是这样的:内联函数为什么要定义在头文件中呢?因为它是一个内联函数,可以像宏一样使用,任何想使用这个内联函数的源文件,不必亲自再去定义一遍,直接包含这个头文件,即可像宏一样使用。那为什么还要用 static 修饰呢?因为我们使用 inline 定义的内联函数,编译器不一定会内联展开,那么当多个文件都包含这个内联函数的定义时,编译时就有可能报重定义错误。而使用 static 修饰,可以将这个函数的作用域局限在各自本地文件内,避免了重定义错误。理解了这两点,就能够看懂 Linux 内核头文件中定义的大部分内联函数了。至于其它的一些内联函数定义,基本上没怎么遇到过,就不再赘述了。

嵌入式C语言自我修养 10:内联函数探究的更多相关文章

  1. 嵌入式C语言自我修养 12:有一种宏,叫可变参数宏

    12.1 什么是可变参数宏 在上面的教程中,我们学会了变参函数的定义和使用,基本套路就是使用 va_list.va_start.va_end 等宏,去解析那些可变参数列表我们找到这些参数的存储地址后, ...

  2. C++语言基础(7)-inline内联函数

    函数调用是有时间和空间开销的.程序在执行一个函数之前需要做一些准备工作,要将实参.局部变量.返回地址以及若干寄存器都压入栈中,然后才能执行函数体中的代码:函数体中的代码执行完毕后还要清理现场,将之前压 ...

  3. 特殊用途语言特性(默认实参/内联函数/constexpr函数/assert预处理宏/NDEBUG预处理变量)

    默认实参: 某些函数有这样一种形参,在函数的很多次调用中它们都被赋予一个相同的值,此时,我们把这个反复出现的值称为函数的默认实参.调用含有默认实参的函数时,可以包含该实参,也可以省略该实参. 需要特别 ...

  4. 嵌入式C语言自我修养 01:Linux 内核中的GNU C语言语法扩展

    1.1 Linux 内核驱动中的奇怪语法 大家在看一些 GNU 开源软件,或者阅读 Linux 内核.驱动源码时会发现,在 Linux 内核源码中,有大量的 C 程序看起来“怪怪的”.说它是C语言吧, ...

  5. 嵌入式C语言自我修养 03:宏构造利器:语句表达式

    3.1 基础复习:表达式.语句和代码块 表达式 表达式和语句是 C 语言中的基础概念.什么是表达式呢?表达式就是由一系列操作符和操作数构成的式子.操作符可以是 C 语言标准规定的各种算术运算符.逻辑运 ...

  6. 嵌入式C语言自我修养 13:C语言习题测试

    13.1 总结 前面12节的课程,主要针对 Linux 内核中 GNU C 扩展的一些常用 C 语言语法进行了分析.GNU C 的这些扩展语法,主要用来完善 C 语言标准和编译优化.而通过 C 标准的 ...

  7. 嵌入式C语言自我修养 11:有一种函数,叫内建函数

    11.1 什么是内建函数 内建函数,顾名思义,就是编译器内部实现的函数.这些函数跟关键字一样,可以直接使用,无须像标准库函数那样,要 #include 对应的头文件才能使用. 内建函数的函数命名,通常 ...

  8. 嵌入式C语言自我修养 06:U-boot镜像自拷贝分析:section属性

    6.1 GNU C 的扩展关键字:attribute GNU C 增加一个 __atttribute__ 关键字用来声明一个函数.变量或类型的特殊属性.声明这个特殊属性有什么用呢?主要用途就是指导编译 ...

  9. 嵌入式C语言自我修养 04:Linux 内核第一宏:container_of

    4.1 typeof 关键字 ANSI C 定义了 sizeof 关键字,用来获取一个变量或数据类型在内存中所占的存储字节数.GNU C 扩展了一个关键字 typeof,用来获取一个变量或表达式的类型 ...

随机推荐

  1. d3js data joins深入理解

    Data joins 给定一个数据数组和一个 D3 selection  我们就可以attach或者说是'join'数组中的每个数据到selection中的每个元素上. 这将使得我们的数据和可视化元素 ...

  2. java笔记--正则表达式的运用(包括电话,邮箱验证等)

    正则表达式 --如果朋友您想转载本文章请注明转载地址"http://www.cnblogs.com/XHJT/p/3877402.html "谢谢-- 正则表达式符号:" ...

  3. leetcode Ch7-Graph Search

    1. Clone Graph BFS: class Solution { public: typedef UndirectedGraphNode UGNode; UndirectedGraphNode ...

  4. Linux案例01:eth0网卡异常

    一.现象描述 今天在调试两台物理机,做完配置重启主机后,发现一台服务器网络无法ssh连接,通过ILO进去ifconfig发现eth0配置的IP地址等信息丢失,手动重启后,可以ssh连接,但过一段时间, ...

  5. “互联网+”背景下使用微信公众号增强班主任工作与整合教学资源(泰微课)

    前记:此文是我爱人一项作业.因为我本人对于微信这一块比较熟悉,就参与这项作业中.此文已经参加移动和教育相关活动.作者是我爱人,如有转载请署名作者. 一.什么是"互联网+"? 早在1 ...

  6. HBuilder:一个不错的web前端IDE(代码编辑器)

    Web前端开发,2000之后基本就是三剑客的天下.到现在DW也是不错的HTMLcoder,如今的前端开发早已是JS的天下.但是DW对于JS方面就弱爆了.DW虽然支持JS语法高亮也支持JQuery Jq ...

  7. Python实例---利用正则实现计算器[参考版]

    利用正则进行运算规则的计算 版本一: # import re # # ss = '1 - 2 * ((60 - 30 + (-40/5) * (9 - 2 * 5 / 3 + 7 / 3 * 99 / ...

  8. SpringMvc学习---基础知识考核

    SpringMVC 1.SpringMVC的工作流程 流程 : 1.用户发送请求至前端控制器DispatcherServlet2.DispatcherServlet收到请求调用HandlerMappi ...

  9. php给$_POST赋值会导致值为空

    在调试一个程序的时候发现很奇怪的现象,post传过来的值再某些地方为空,先看下面的代码 <?php if($_POST['submit'] == 'Add'){ if($_POST['type' ...

  10. Chapter 5 Order Inversion Pattern

    5.1 Introdution The main focus of this chapter is to discuss the order inversion (OI) pattern, which ...