【C语言】- inline

文章目录

  • C 语言 inline
    • 带参数的宏和 inline 区别
    • 应用场景
    • 使用方法
      • 普通函数调用
      • inline 函数调用
      • 代码示例
        • ` 未加 inline 修饰 add_func 时 `
        • ` 加 inline 修饰 add_func 时 `
    • 强制 inline
    • 特点
      • 与普通函数区别
      • 不构成外部定义
      • 头文件定义
      • 不能包含复杂结构
      • 对编译器的建议
      • 代码膨胀
      • static 和 inline 联合使用
      • inline 和 #define 的区别
    • 参考链接

C 语言 inline

C 语言中函数调用与返回时会有部分的额外开销,如果在函数需要调用的次数非常多时,这些额外开销就会产生积累效应。

  • C89 中避免函数额外开销的唯一方式是使用带参数的宏

    • 使用宏造成的缺陷: 宏定义在形式上类似于一个函数,但在使用时,仅仅只是做简单替换,因此它不能进行参数有效性的检测,无法执行编译器严格类型检查,另外它的返回值也不能被强制转换为可转换的合适的类型。
  • C99 中提供了更好的一种方式,即内联函数 inline。内联表明编译器将函数的每一次调用都用函数的机器指令来代替,但其只是建议编译器这样做,并不强制,编译器可以选择忽略。

inline 函数是 C99 标准中添加的一个新的特性,非常适合于经常被调用的小函数。

带参数的宏和 inline 区别

不同 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
返回到之前的指令位置
...

inline 函数调用

···
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 关键字修饰的函数可能在编译的时候可能并不会真正 inline

这时可以使用 __attribute__((always_inline)) 属性来强制 inline。

static __attribute__((always_inline)) inline int add_func(int a, int b)
{
    ......
}
  • 需要注意的是,使用 __attribute__((always_inline)) 强制 inline 时,函数最好还要加上 inline,否则也可能没有 inline 成功。

特点

  • inline 函数中不能定义可改变的 static 变量
  • inline 函数中不能引用具有内部链接的变量

与普通函数区别

nline 函数除了调用方式不同外,它和一般的函数没有区别,它也有自己的地址。如果 inline 函数中用到了宏,宏会被预处理器展开。

不构成外部定义

inline 定义特别针对翻译单元,不构成外部定义,别的翻译单元可以包含这个 inline 函数的外部定义。但是如果一个已经被定义成 inline 函数的函数被存储类修饰符 extern 修饰,那么它就会具有外部链接属性。

头文件定义

翻译单元只要用到某个 inline 函数,必须重复定义此 inline 函数,因此,inline 函数的定义常常被放在头文件中。

因为内联函数要在调用点展开,所以编译器必须随处可见内联函数的定义,要不然就成了非内联函数的调用了。所以,这要求每个调用了内联函数的文件都出现了该内联函数的定义。

因此,将内联函数的定义放在头文件里实现是合适的,省却你为每个文件实现一次的麻烦。

声明跟定义要一致:如果在每个文件里都实现一次该内联函数的话,那么,最好保证每个定义都是一样的,否则,将会引起未定义的行为。如果不是每个文件里的定义都一样,那么,编译器展开的是哪一个,那要看具体的编译器而定。所以,最好将内联函数定义放在头文件中

不能包含复杂结构

  • 一般来说,不应该将包含循环的函数定义成 inline 函数
  • inline 只适合函数体内代码比较简单的函数使用,不能包含复杂的结构控制语句,例如 while、switch,并且内联函数本身不能是直接递归函数(函数内部调用自己的函数)

对编译器的建议

  • inline 函数仅仅是一个对编译器的建议,所以最后能否真正内联,看编译器的意思,它如果认为函数不复杂,能在调用点展开,就会真正内联,并不是说声明了内联就会内联,声明内联只是一个建议而已

代码膨胀

内联能提高函数的执行效率,为什么不把所有的函数都定义成内联函数?如果所有的函数都是内联函数,还用得着 内联 这个关键字吗?
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。 如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。

所以,inline 函数一般只会用在函数内容非常简单的时候,这是因为内联函数的代码会在任何调用它的地方展开,如果函数太复杂,代码膨胀带来的恶果很可能会大于效率的提高带来的益处

static 和 inline 联合使用

static 是静态修饰符,由其关键字修饰的变量会保存到全局数据区,对于普通的局部变量或者全局变量,都是由系统自动分配内存的,并且当变量离开作用域的时候释放掉,而使用 static 关键字来修饰,只有当程序结束时候才会释放掉,

使用 static inline 修饰时,函数仅在文件内部可见,不会污染命名空间。

另外,函数在运行过程中也会分配内存空间,但是由于 static 的存在,就和修饰变量类似,它只会开辟一块内存空间。

inline 和 #define 的区别

  • inline 由编译器一起处理,直接将函数体插入调用的地方。
  • #define 由预处理器处理,进行简单的文本替换,没有任何编译过程。

参考链接

  • https://blog.csdn.net/weaiken/article/details/88085360
  • https://zhuanlan.zhihu.com/p/448262183

你可能感兴趣的:(C语言,inline,always,inline,noinline)