C 语言中函数调用与返回时会有部分的额外开销,如果在函数需要调用的次数非常多时,这些额外开销就会产生积累效应。
C89 中避免函数额外开销的唯一方式是使用带参数的宏
C99 中提供了更好的一种方式,即内联函数 inline。内联表明编译器将函数的每一次调用都用函数的机器指令来代替,但其只是建议编译器这样做,并不强制,编译器可以选择忽略。
inline 函数是 C99 标准中添加的一个新的特性,非常适合于经常被调用的小函数。
不同 | inline 函数 | 带参数的宏 |
---|---|---|
对应的 C 语言版本 | C89 | C99 |
展开的时机 | 在编译的时候展开,因此 inline 关键字是一个编译关键字 | 在预处理时展开,因此 #define 关键字是一个预处理关键字 |
参数类型检查 | inline() 函数是一中函数,会进行严格的参数类型检查 | 不会检查参数类型,只是做简单的字符串替换,因此在使用带参数的宏时会有一些副作用,编写程序是要人为预防 |
是否允许有复杂语句 | 不允许出现复杂语句,如果出现复杂语句,该函数将不会展开,例如递归,大型循环等 | 对此不做要求。宏只是做字符串替换操作,而不了解语句的含义 |
是否一定被展开 | 不一定,是否展开由编译器决定 | 一定,只要使用了宏就可以保证被展开 |
在调用子函数时,通常要经过 保存当前的指令位置,程序流跳转到子函数,执行子函数,返回之前的指令位置
这个过程,但对于那些经常被调用的小函数来说,这样的调用过程会影响程序效率。
inline 函数的调用过程与此不同,它会告诉编译器把那些小函数编译后的机器码放到调用函数的地方,这样就节省了函数调用的时间。
- 关键字 inline 必须与函数的定义体放在一起,才能使函数成为内联函数,仅仅将 inline 放在函数声明前面不起作用,如果想把一个函数定义成 inline 函数,只要在函数定义前面添加 inline 关键字。
例如,下面风格的函数 add_func 将不能成为内联函数:
inline int add_func(int a, int b);
int add_func(int a, int b)
{
...
}
如下风格的函数 add_func
则成为内联函数
int add_func(int a, int b);
inline int add_func(int a, int b)
{
...
}
如果有其他函数调用 inline 函数 add_func
,对比普通的函数调用其过程如下
···
保存当前指令位置
跳转到 add_func
执行 add_func
返回到之前的指令位置
...
···
add_func 编译后的机器码
...
- 现代编译器比较智能,有时候会自动将比较短小的函数编译成
inline
了,如果不需要此特性可以使用__attribute__((noinline))
static int add_func(int a, int b)
{
/* 添加循环 nop 是为了防止编译器把这个函数默认 inline 了 当然也可以使用 __attribute__((noinline)) 属性 */
for (int i = 0; i < 10; i++)
{
asm volatile("nop");
asm volatile("nop");
asm volatile("nop");
asm volatile("nop");
asm volatile("nop");
}
for (int i = 0; i < 10; i++)
{
asm volatile("nop");
asm volatile("nop");
asm volatile("nop");
asm volatile("nop");
asm volatile("nop");
}
for (int i = 0; i < 10; i++)
{
asm volatile("nop");
asm volatile("nop");
asm volatile("nop");
asm volatile("nop");
asm volatile("nop");
}
return a + b;
}
void Software7_IRQn_Handler(void)
{
sgi_7_cnt++;
add_func(2, 3);
printf("this is a sgi irq %d\r\n", sgi_7_cnt);
}
未加 inline 修饰 add_func 时
Software7_IRQn_Handler 的反汇编是这样的
8000024c <Software7_IRQn_Handler>:
8000024c: f242 42c4 movw r2, #9412 ; 0x24c4
80000250: f2c8 0200 movt r2, #32768 ; 0x8000
80000254: b508 push {r3, lr}
80000256: 2103 movs r1, #3
80000258: 2002 movs r0, #2
8000025a: 6813 ldr r3, [r2, #0]
8000025c: 3301 adds r3, #1
8000025e: 6013 str r3, [r2, #0]
80000260: f7ff ffdc bl 8000021c <add_func>
80000264: f242 3038 movw r0, #9016 ; 0x2338
80000268: f2c8 0000 movt r0, #32768 ; 0x8000
8000026c: 6811 ldr r1, [r2, #0]
8000026e: e8bd 4008 ldmia.w sp!, {r3, lr}
80000272: f000 bac7 b.w 80000804 <printf>
80000276: bf00 nop
可以看到代码中有一条 bl 8000021c
跳转指令,跳转到 add_func
函数
加 inline 修饰 add_func 时
Software7_IRQn_Handler 的反汇编是这样的
8000021c <Software7_IRQn_Handler>:
8000021c: f242 42ec movw r2, #9452 ; 0x24ec
80000220: f2c8 0200 movt r2, #32768 ; 0x8000
80000224: 230a movs r3, #10
80000226: 6811 ldr r1, [r2, #0]
80000228: 3101 adds r1, #1
8000022a: 6011 str r1, [r2, #0]
8000022c: bf00 nop
8000022e: bf00 nop
80000230: bf00 nop
80000232: 3b01 subs r3, #1
80000234: d1fa bne.n 8000022c <Software7_IRQn_Handler+0x10>
80000236: 230a movs r3, #10
80000238: bf00 nop
8000023a: bf00 nop
8000023c: bf00 nop
8000023e: bf00 nop
80000240: 3b01 subs r3, #1
80000242: d1f9 bne.n 80000238 <Software7_IRQn_Handler+0x1c>
80000244: 230a movs r3, #10
80000246: bf00 nop
80000248: bf00 nop
8000024a: bf00 nop
8000024c: bf00 nop
8000024e: bf00 nop
80000250: 3b01 subs r3, #1
80000252: d1f8 bne.n 80000246 <Software7_IRQn_Handler+0x2a>
80000254: f242 3060 movw r0, #9056 ; 0x2360
80000258: f2c8 0000 movt r0, #32768 ; 0x8000
8000025c: 6811 ldr r1, [r2, #0]
8000025e: f000 bad5 b.w 8000080c <printf>
80000262: bf00 nop
可以看到代码中没有跳转指令,而是将 add_func
函数展开放在了对应的位置
inline
关键字只是对编译器的建议,如果编译器认为需要 inline 的函数太复杂,使用 inline
关键字修饰的函数可能在编译的时候可能并不会真正 inline
。
这时可以使用 __attribute__((always_inline))
属性来强制 inline。
static __attribute__((always_inline)) inline int add_func(int a, int b)
{
......
}
- 需要注意的是,使用
__attribute__((always_inline))
强制 inline 时,函数最好还要加上inline
,否则也可能没有 inline 成功。
nline 函数除了调用方式不同外,它和一般的函数没有区别,它也有自己的地址。如果 inline 函数中用到了宏,宏会被预处理器展开。
inline 定义特别针对翻译单元,不构成外部定义,别的翻译单元可以包含这个 inline 函数的外部定义。但是如果一个已经被定义成 inline 函数的函数被存储类修饰符 extern 修饰,那么它就会具有外部链接属性。
翻译单元只要用到某个 inline 函数,必须重复定义此 inline 函数,因此,inline 函数的定义常常被放在头文件中。
因为内联函数要在调用点展开,所以编译器必须随处可见内联函数的定义,要不然就成了非内联函数的调用了。所以,这要求每个调用了内联函数的文件都出现了该内联函数的定义。
因此,将内联函数的定义放在头文件里实现是合适的,省却你为每个文件实现一次的麻烦。
声明跟定义要一致:如果在每个文件里都实现一次该内联函数的话,那么,最好保证每个定义都是一样的,否则,将会引起未定义的行为。如果不是每个文件里的定义都一样,那么,编译器展开的是哪一个,那要看具体的编译器而定。所以,最好将内联函数定义放在头文件中
。
内联能提高函数的执行效率,为什么不把所有的函数都定义成内联函数?如果所有的函数都是内联函数,还用得着 内联
这个关键字吗?
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。 如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
所以,inline 函数一般只会用在函数内容非常简单的时候,这是因为内联函数的代码会在任何调用它的地方展开,如果函数太复杂,代码膨胀带来的恶果很可能会大于效率的提高带来的益处
static 是静态修饰符,由其关键字修饰的变量会保存到全局数据区,对于普通的局部变量或者全局变量,都是由系统自动分配内存的,并且当变量离开作用域的时候释放掉,而使用 static 关键字来修饰,只有当程序结束时候才会释放掉,
使用 static inline 修饰时,函数仅在文件内部可见,不会污染命名空间。
另外,函数在运行过程中也会分配内存空间,但是由于 static 的存在,就和修饰变量类似,它只会开辟一块内存空间。