00. 目录

文章目录

  • 00. 目录
  • 01. 属性声明
  • 02. 内联函数概述
  • 03. 内联函数与宏
  • 04. 编译器对内联函数的处理
  • 05. static修饰内联函数
  • 06. 附录

01. 属性声明

attribute 属性声明,attribute可以说是 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 属性声明,就是告诉编译器,要内联展开。

02. 内联函数概述

函数调用开销

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

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

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

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

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

内联函数的好处

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

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

03. 内联函数与宏

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

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

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

04. 编译器对内联函数的处理

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

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

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

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

程序示例

#include <stdio.h>static inline
__attribute__((always_inline)) int func(int a)
{return a + 1;
}static inline void print_num(int a)
{printf("%d\n",a);
}int main(void)
{int i;i = func(3);print_num(10);return 0;
}

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

deng@itcast:~/tmp$ arm-linux-gcc test.c
deng@itcast:~/tmp$ arm-linux-objdump -D a.out > a.dis
deng@itcast:~/tmp$ 
000083b4 <print_num>:83b4:   e92d4800    push    {fp, lr} 83b8:   e28db004    add fp, sp, #483bc:   e24dd008    sub sp, sp, #883c0:   e50b0008    str r0, [fp, #-8]83c4:   e59f3010    ldr r3, [pc, #16]   ; 83dc <print_num+0x28>83c8:   e1a00003    mov r0, r383cc:   e51b1008    ldr r1, [fp, #-8]83d0:   ebffffc9    bl  82fc <_init+0x44>83d4:   e24bd004    sub sp, fp, #483d8:   e8bd8800    pop {fp, pc} 83dc:   00008490    muleq   r0, r0, r4000083e0 <main>:83e0:   e92d4800    push    {fp, lr} 83e4:   e28db004    add fp, sp, #483e8:   e24dd008    sub sp, sp, #883ec:   e3a03003    mov r3, #383f0:   e50b300c    str r3, [fp, #-12]83f4:   e51b300c    ldr r3, [fp, #-12]83f8:   e2833001    add r3, r3, #183fc:   e50b3008    str r3, [fp, #-8]8400:   e3a0000a    mov r0, #10 8404:   ebffffea    bl  83b4 <print_num>8408:   e3a03000    mov r3, #0840c:   e1a00003    mov r0, r38410:   e24bd004    sub sp, fp, #48414:   e8bd8800    pop {fp, pc}

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

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

05. 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 内核头文件中定义的大部分内联函数了。至于其它的一些内联函数定义,基本上没怎么遇到过,就不再赘述了。

06. 附录

参考:C语言嵌入式Linux高级编程

【嵌入式】C语言高级编程-内联函数(10)相关推荐

  1. c语言中void和define,C语言里面的内联函数(inline)与宏定义(#define)探讨

    C语言里面的内联函数(inline)与宏定义(#define)探讨 先简明扼要,说下关键: 1.内联函数在可读性方面与函数是相同的,而在编译时是将函数直接嵌入调用程序的主体,省去了调用/返回指令,这样 ...

  2. c语言什么是内联函数,C语言中内联函数inline的使用方法

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 来源一:比特网 来源二:东方锐智 在C++中,为了解决一些频繁调用的小涵数大量消耗栈空间或者是叫栈内存的问题,特别的引入了inline修饰符,表示为内联涵 ...

  3. 内联函数和编译器对Go代码的优化

    什么是内联函数 图片版权:Renee French. 在很多讲 Go 语言底层的技术资料和博客里都会提到内联函数这个名词,也有人把内联函数说成代码内联.函数展开.展开函数等等,其实想表达的都是 Go ...

  4. 浅析MATLAB中的内联函数、匿名函数和函数函数

    内联函数 内联(inline)函数是MATLAB 7以前经常使用的一种构造函数对象的方法.在命令窗口.程序或函数中创建局部函数时,通过使用inline构造函数,而不用将其储存为一个M文件,同时又可以像 ...

  5. 内联函数(inline 函数)详解

    inline 函数详解 定义 内联函数和普通函数一样,区别仅仅是在被调用处直接使用机器码替换的形式. 直接采用机器码替换的目的是:减少因调用而造成的开销,加速执行效率. 很多编程语言包括:c,c++, ...

  6. 创建内联函数matlab,浅析MATLAB中的内联函数、匿名函数和函数函数

    原创,转载请注明出处--(不注明也拿你没办法) 内联函数 内联(inline)函数是MATLAB 7以前经常使用的一种构造函数对象的方法.在命令窗口.程序或函数中创建局部函数时,通过使用inline构 ...

  7. 【嵌入式】C语言高级编程-可变参数宏(12)

    00. 目录 文章目录 00. 目录 01. 可变参数宏概述 02. ##符号 03. 可变参宏另外一种写法 04. 内核中的可变参数宏 05. 附录 01. 可变参数宏概述 #include < ...

  8. C++ 语言学习 day01 (linux ),基本输入输出错误输出函数,名字空间的含义,内联函数,缺省参数,引用 ,、new操作符

    1.介绍c++ C++头文件风格 c++ 老风格:以.h结尾 是c语言风格  stdio.h  (尽量不) c++的转换风格: 去掉.h 在文件名前加c 例如#include <cstdio&g ...

  9. stm32 c语言内联函数,【实战经验】STM32F3xx/STM32F4xx使用浮点开方指令

    STM32F3xx/STM32F4xx使用浮点开方指令前言 STM32F3xx/STM32F4xx(ARM Cortex-M4内核)中集成了FPU,也就是浮点指令单元,可以将浮点运算变得简单快速,但如 ...

最新文章

  1. java 通过反射获取调用类方法及属性
  2. PHP中间件ICE,ICE的安装配置,ICE常见编译和运行(异常)错误(自测Php版本安装部分,因为php版本跟ice版本不一样失败)
  3. java 局部性原理_程序局部性原理
  4. 搜索目录里所有文件(包括子目录)
  5. v-if 表单验证_避免许多if块进行验证检查
  6. openssh升级sftp_OpenSSH 8.2 发布 包括 sftp 客户端和服务器支持
  7. 浅尝EffectiveCSharp_1
  8. 《Python数据分析常用手册》NumPy和Pandas
  9. Win8.1 JAVA环境配置全过程
  10. Dlib 19.14发布——增加了一个训练RBF-SVM的auto-ML工具
  11. 《UG NX10中文版完全自学手册》——2.4 布局
  12. 第二篇: Silverlight -- 下载与安装
  13. 生活情景英语:英语国家生存英语精选
  14. android加载图片+背景,Android开发中ImageLoder加载网络图片时将图片设置为ImageView背景的方法...
  15. latex编辑公式好用的在线网址
  16. 联想笔记本G400使用VS2013时,笔记本快捷键与软件快捷键冲突解决方案
  17. CTDC 2017 首席技术官领袖峰会 | 技术、探索、创新
  18. Java并发(一)并发基础
  19. 视频人体动作捕捉技术
  20. 第一天-网络设备安全操作知识

热门文章

  1. JS移动客户端--触屏滑动事件 banner图效果
  2. 在线的IDE(compilr)支持图形界面,支持C,C++,JAVA
  3. 服务器读取本地文件,java远程服务器访问本地文件
  4. 机载计算机网络拓扑结构设计,计算机网络课程设计-网络拓扑结构设计.doc
  5. python opengl加速_OpenGL with python 渲染加速
  6. L2-1 包装机 (25 分)(STL43行代码)
  7. python验证数学原理_一起学opencv-python九(性能的测量和优化与图像处理的数学原理)...
  8. Css网格布局-Grid布局
  9. day01 js三种导入html的方法、js书写规范、变量的基本使用、变量提升
  10. mac下日期、时间戳互转