嵌入式C语言自我修养 10:内联函数探究

10.1 属性声明:noinline & always_inline

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

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

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 的取址效率降低,程序执行效率降低。函数的作用之一就是提高代码的复用性,我们将常用的一些代码或代码块封装成函数,进行模块化编程,而内联函数往往是降低了函数的复用性。所以编译器在对内联函数作展开处理时,除了检测用户定义的内联函数内部是否有指针、循环、递归外,还会在函数执行效率和函数调用开销之间进行权衡。一般来讲,判断对一个内联函数到底展不展开,从程序员的角度,主要考虑以下几个因素。

  • 函数体积小
  • 函数体内无指针赋值、递归、循环等语句
  • 调用频繁

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

//inline.c
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 作反汇编处理,其汇编代码如下。

$ arm-linux-gnueabi-gcc -o a.out inline.c
$ arm-linux-gnueabi-objdump -D a.out
00010438 <print_num>:
   10438:    e92d4800    push    {fp, lr}
   1043c:    e28db004    add fp, sp, #4
   10440:    e24dd008    sub sp, sp, #8
   10444:    e50b0008    str r0, [fp, #-8]
   10448:    e51b1008    ldr r1, [fp, #-8]
   1044c:    e59f000c    ldr r0, [pc, #12]
   10450:    ebffffa2    bl  102e0 <[email protected]>
   10454:    e1a00000    nop ; (mov r0, r0)
   10458:    e24bd004    sub sp, fp, #4
   1045c:    e8bd8800    pop {fp, pc}
   10460:    0001050c    andeq   r0, r1, ip, lsl #10

00010464 <main>:
   10464:    e92d4800    push    {fp, lr}
   10468:    e28db004    add fp, sp, #4
   1046c:    e24dd008    sub sp, sp, #8
   10470:    e3a03003    mov r3, #3
   10474:    e50b3008    str r3, [fp, #-8]
   10478:    e51b3008    ldr r3, [fp, #-8]
   1047c:    e2833001    add r3, r3, #1
   10480:    e50b300c    str r3, [fp, #-12]
   10484:    e3a0000a    mov r0, #10
   10488:    ebffffea    bl  10438 <print_num>
   1048c:    e3a03000    mov r3, #0
   10490:    e1a00003    mov r0, r3
   10494:    e24bd004    sub sp, fp, #4
   10498:    e8bd8800    pop {fp, pc}

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

10470:    e3a03003    mov r3, #3
   10474:    e50b3008    str r3, [fp, #-8]
   10478:    e51b3008    ldr r3, [fp, #-8]
   1047c:    e2833001    add r3, r3, #1
   10480:    e50b300c    str r3, [fp, #-12]

而对于 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语言嵌入式Linux高级编程视频教程 第05期 改编,电子版书籍可加入QQ群:475504428 下载,更多嵌入式视频教程,可关注:
微信公众号:宅学部落(armlinuxfun)
51CTO学院-王利涛老师:http://edu.51cto.com/sd/d344f

原文地址:http://blog.51cto.com/zhaixue/2348620

时间: 02-02

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

特殊用途语言特性:默认实参,内联函数和constexpr函数,调试帮助

重点: 1.三种函数相关的语言特性:默认实参,内联函数和constexpr函数. 2.默认实参:某些函数有一种形参,在函数的很多次调用中它们都被赋予一个相同的值. 3.一旦某个形参被赋予了默认值之后,它后面的所以形参都必须要默认值. 4.若想使用默认形参,只要在调用函数时省略该实参即可. Tip:Window = screen ( , , ‘?’ );//错误:只能省略尾部的实参! 5.对于函数的声明来说,习惯将其放在头文件当中,在给定的作用域中一个形参只能赋予一次默认实参. NOTE:通常,应

【C语言天天练(二一)】内联函数

        引言:调用函数时,通常会因为建立调用.传递参数.跳转到函数代码并返回等花费掉一些时间,C语言的解决办法是使用类函数宏.在C99中,还提出了另外一种方法:内联函数.         内联函数:把函数变为内联函数将建议编译器尽可能快速地调用该函数,至于建议的效果则由实现来定义.因此,使函数变为内联函数可能会简化函数的调用机制,但也可能不起作用.内联函数是通过编译器来实现的,而宏则是在预编译的时候替换. 创建内联函数方法:在函数声明中使用函数说明符inline. 内联函数的特点: 1.

拷贝构造,深度拷贝,关于delete和default相关的操作,explicit,类赋初值,构造函数和析构函数,成员函数和内联函数,关于内存存储,默认参数,静态函数和普通函数,const函数,友元

 1.拷贝构造 //拷贝构造的规则,有两种方式实现初始化. //1.一个是通过在后面:a(x),b(y)的方式实现初始化. //2.第二种初始化的方式是直接在构造方法里面实现初始化. 案例如下: #include<iostream> //如果声明已经定义,边不会生成 class classA { private: int a; int b; public: //拷贝构造的规则,有两种方式实现初始化 //1.一个是通过在后面:a(x),b(y)的方式实现初始化 //2.第二种初始化的方式是直

内联函数详解

什么是内联性和外联函数 类的成员函数可以分为内联函数和外联函数.内联函数是指那些定义在类体内的成员函数,即该函数的函数体放在类体内.而说明在类体内,定义在类体外的成员函数叫外联函数.外联函数的函数体在类的实现部分. 内联函数在调用时不是像一般的函数那样要转去执行被调用函数的函数体,执行完成后再转回调用函数中,执行其后语句,而是在调用函数处用内联函数体的代码来替换,这样将会节省调用开销,提高运行速度. 内联函数与前面讲过的带参数的宏定义进行一下比较,它们的代码效率是一样的,但是内联函数要优于宏定义

C++引用,内联函数,函数重载二义性总结_C++

1.引用 1.1 引用的概念 C++语言中,可以定义"引用".引用定义如下: 类型名 & 引用名 = 同类型的某变量名: 例如:int n: int &r=n://r就是一个引用,可以说r的类型是int &,r引用了变量n,或者说r成为n的引用. 某个变量的引用和这个变量是一回事,相当于该变量的一个别名.请注意,定义引用时一定要将其初始化,否则编译不会通过,通常会用某个变量去初始化引用,初始化后,它就一直引用该变量,不会再引用别的变量了.也可以用一个引用去初始化

C++ 内联函数

1.定义: 被调用函数的函数体代码直接插入到该函数被调用处, 而不是通过call语句进行. 2.方式: (1).类的定义体外: 当在类的定义体外时,需要在该成员函数的定义前面加“inline”关键字,显式地告诉编译器该函数在调用时需要“内联”处理,如: class Person { public: string GetName(); private:   string  name; }; inline string GetName() {         return name; } (2).类

C99语法之可变参宏和内联函数

可变参宏: 1 #include<stdio.h> 2 #include<stdlib.h> 3 4 #define MYPRINT(...) printf(__VA_ARGS__) 5 6 int main(int argc, char **argv) 7 { 8 MYPRINT("%d,%s", 10, "hello china"); 9 getchar(); 10 return 0; 11 } 使用 ... 来指明多参,使用宏 __A_

C++文件头,命名空间,new和delete,内联函数,引用,函数重载,构造函数和析构函数,深拷贝和浅拷贝,explict,this指针

 目  录 1       开始学习C++.............................................................................................................. 4 1.1       C++的头文件.................................................................................................

内联函数和宏

内联函数是为了提高程序运行速度的一种改进. 当程序运行时,有时候需要重复调用一个函数,但是因为重复调用这个函数,会不断的造成函数调用, 会不断进栈出栈造成cpu的消耗. 而内联函数是在编译时就将这个函数边入进去,不用再进行地址的跳转.但是不可避免的产生了一些 内存的消耗,所有有时候用户在申请内联时,内联函数过于大,编译器不会进行允许 例如: #include<iostream> #include<ctime> #include<windows.h> using name

内联函数与虚函数

如果函数已经被声明为inline, 内联函数已经在编译期间它的调用点上就被展开; 而虚拟函数调用的决定则要等到运行时刻在执行程序内部的每个调用点上系统根据被调用对象的实际基类或派生类的类型来决定选择哪一个虚拟函数实例. 内联不是强制性的,你只是向编译器提出这个建议,允许它在可以内联的时候采取内联形式.而虚函数本身就是一个函数,只是在多态的情况下,它要到执行时才能确定调用的函数,所以这样的特性阻止了虚函数的内联.但,要注意,只是在多态的情况下(多态不用我解释吧).如果是静态的调用,编译器还是会采用