本文章为知乎用户

@徐yang哟

原创,禁止抄袭!

灵感来源

在查stm32的LL库部分函数的API时,有时会查到这种函数:

__STATIC_INLINE void LL_GPIO_SetPinOutputType  ( GPIO_TypeDef *  GPIOx, uint32_t  PinMask, uint32_t  OutputType);

我不禁对__STATIC_INLINE产生了好奇。在查看源文件后,发现这个关键字的定义如下

#ifndef   __INLINE#define __INLINE                               __inline
#endif
#ifndef   __STATIC_INLINE#define __STATIC_INLINE                        static __inline
#endif
#ifndef   __STATIC_FORCEINLINE                 #define __STATIC_FORCEINLINE                   static __forceinline
#endif

很明显这个关键字是由static 和 __inline 一起构成的。所以我们要分别知道这两个关键字的作用是什么。

关键字定义

static

在C语言中,函数默认情况下是global的。函数名前的static关键字使它们变成静态。不同于C语言其他的global的函数,访问static函数被限制到声明它们的文件。因此,当我们要限制对函数的访问时,我们让它们static。此外,在不同的文件中可以允许存在拥有相同函数名的static函数。

inline

inline是c99的特性。在c99中,inline是向编译器建议,将被inline修饰的函数以内联的方式嵌入到调用这个函数的地方。而编译器会判断这样做是否合适,以此最终决定是否这么做。

static inline的优劣

根据上面的定义可以知道,如果static inline关键字生效(因为只有编译器有最终决定权,我们只有建议权,这点在后面会细讲),static inline会以一种类似于宏定义的方式,将调用被static inline修饰的函数的语句替换为那个函数体对应的指令,但实际上只是inline的作用,static作用其实是维护代码的健壮性,实验中会加以证明。 所以:

好处:
减少调用函数时的开销,如:
减少传参时可能引起的压栈出栈的开销。
减少PC跳转时对流水线的破坏。
坏处: * 代码所占体积会更大。

实验

目的:直观证明上述优点和缺点、探究static的作用

实验一 :探究static inline对代码起到的影响

实验背景:
硬件平台:stm32f401re
IDE:stm32cubeMX、Keil(关掉指令优化)
库:LL库(仅仅初始化芯片)
编程语言:C语言、Arm指令

实验设计

尝试构造四个函数,这四个函数都是实现对四个参数相加,然后将结果返回,不同之处在于它们被不同的关键字修饰。

分别为:
int Normal_Add(int n1,int n2,int n3,int n4,int n5);
static int Static_Add(int n1,int n2,int n3,int n4,int n5);
__inline int Inline_Add(int n1,int n2,int n3,int n4,int n5);
static __inline int StaticInline_Add(int n1,int n2,int n3,int n4,int n5);
注:在Armcc编译器的实现中,inline被实现为__inline

在这些函数中: Normal_Add是最简单的函数;
Static_Add是用static关键字修饰的函数;
Inline_Add是用inline关键字修饰的函数;
StaticInline_Add是用static inline关键字修饰的函数;

main.c

只罗列了关键部分

#include "funcTest.h"
int main(void)
{int i;i = Normal_Add(1,1,1,1,1);i = Static_Add(2,2,2,2,2);i = Inline_Add(3,3,3,3,3);i = StaticInline_Add(4,4,4,4,4);/*使用i,为了去掉编译警告*/while (i > 0);
}

funcTest.h

#ifndef __FUNCTEST_H
#define __FUNCTEST_H/*Normal_Add的声明*/
int Normal_Add(int n1,int n2,int n3,int n4,int n5);/*其他三个函数的定义*/
static int Static_Add(int n1,int n2,int n3,int n4,int n5){return (n1+n2+n3+n4+n5);;
}__inline int Inline_Add(int n1,int n2,int n3,int n4,int n5)
{return (n1+n2+n3+n4+n5);;
}static __inline int StaticInline_Add(int n1,int n2,int n3,int n4,int n5)
{return (n1+n2+n3+n4+n5);
};
#endif

funcTest.h文件其实涉及到一个问题,为什么Static_Add函数和Inline_Add函数还有StaticInline_Add函数要放在.h头文件里呢?

理由: 考虑下这三个函数的作用,我们希望它们被其他文件访问到吗?显然希望。那么,如果我们如果把static修饰的函数仅仅放在.c源文件中,其实其他的源文件就不能访问到那几个函数了。至于Inline_Add函数,其实对于armcc编译器,放不放在头文件都可以。但是Inline关键字其实不建议单独用,原因是不安全,最好配合static使用。这个在下一个实验可以看到

funcTest.h

#include <funcTest.h>
/*Normal_Add函数的声明部分*/
int Normal_Add(int n1,int n2,int n3,int n4,int n5)
{return (n1+n2+n3+n4+n5);
}

实验结果

通过armcc生成的反汇编结果,我们来看一看这几个函数对应的汇编指令有什么不同。

main.s

;注释是自己加的;函数调用时:;R0-R3为参数;剩下的一个变量在栈中;R0为返回值;R4为main函数里的变量i;Normal_Add函数,加上子函数总共15条指令MOVS     r0,#1MOV      r3,r0MOV      r2,r0MOV      r1,r0;最后一个参数压栈了STR      r0,[sp,#0]BL       Normal_AddMOV      r4,r0;Static_Add函数MOVS     r0,#2MOV      r3,r0MOV      r2,r0MOV      r1,r0;最后一个参数压栈了STR      r0,[sp,#0]BL       Static_AddMOV      r4,r0;Inline_Add内联函数,10条指令MOVS     r0,#3MOV      r1,r0MOV      r2,r0MOV      r3,r0MOV      r5,r0;开始相加ADDS     r6,r0,r1ADD      r6,r6,r2ADD      r6,r6,r3ADD      r6,r6,r5;传回变量iMOV      r4,r6;StaticInline_Add内联函数,10条指令MOVS     r1,#4MOV      r3,r1MOV      r0,r1MOV      r5,r1MOV      r2,r1;开始相加ADDS     r6,r1,r3ADD      r6,r6,r0ADD      r6,r6,r5ADD      r6,r6,r2;传回变量iMOV      r4,r6NOP

functest.s

Static_Add PROC;还有保存现场的操作,相当浪费时间PUSH     {r4,r5,lr}MOV      r4,r0LDR      r5,[sp,#0xc]ADDS     r0,r4,r1ADD      r0,r0,r2ADD      r0,r0,r3ADD      r0,r0,r5POP      {r4,r5,pc}ENDPNormal_Add PROC;还有保存现场的操作,相当浪费时间PUSH     {r4,r5,lr}MOV      r4,r0LDR      r5,[sp,#0xc]ADDS     r0,r4,r1ADD      r0,r0,r2ADD      r0,r0,r3ADD      r0,r0,r5POP      {r4,r5,pc}ENDP

综合以上两个文件的代码,我们不难发现,非内联函数所用开销显然比内联函数的开销大。因为非内联函数不但用的指令多,还访问内存了,还用跳转指令破坏了流水线。所以肯定会慢很多。使用keil的debug功能,定量算它们所用时间差异。结果如下:

  • Normal_Add函数与static_Add函数:
    0.000037s = 37us
  • Inline_Add函数与StaticInline_Add函数:
    0.000012s = 12us

虽然在这里,没有体现内联函数内存开销大的特点。但请想一想,如果一个内联函数被调用多次,那么这个函数就将被内联多次,代码就会被复制多次;但是一个非内联函数被调用多次,却并不会被复制多次。所以内联函数往往内存开销会大一点。

实验二 :探究static inline和inline的区别

在讲这个之前,其实我们需要知道一点。就是static inline关键字和inline关键字无法决定被关键字所修饰的函数是否最后真正会被内联。我们其实只有建议权,只有armcc编译器才可以决定函数最后是否真正会被内联。

参见Armcc User Guide原文:

inline functions in C99
The C99 keyword inline hints to the compiler that invocations of a function qualified with inline are to be expanded inline. For example: c inline int max(int a, int b) { return (a > b) ? a : b; } The compiler inlines a function qualified with inline only if it is reasonable to do so. It is free to ignore the hint if inlining the function adversely affects performance. Note :The __inline keyword is available in C90.
Note: The semantics of inline in C99 are different to the semantics of inline in Standard C++.
Comiler decisions on function inlining
When function inlining is enabled, the compiler uses a complex decision tree to decide if a function is to be inlined.
The following simplified algorithm is used: 1. If the function is qualified with forceinline, the function is inlined if it is possible to do so.
2. If the function is qualified with inline and the option --forceinline is selected, the function is inlined if it is possible to do so. If the function is qualified with inline and the option --forceinline is not selected, the function is inlined if it is practical to do so. 3. If the optimization level is -O2 or higher, or --autoinline is specified, the compiler automatically inlines functions if it is practical to do so, even if you do not explicitly give a hint that function inlining is wanted.
When deciding if it is practical to inline a function, the compiler takes into account several other criteria, such as:
• The size of the function, and how many times it is called.
• The current optimization level.
• Whether it is optimizing for speed (-Otime) or size (-Ospace).
• Whether the function has external or static linkage.
• How many parameters the function has.
• Whether the return value of the function is used.
Ultimately, the compiler can decide not to inline a function, even if the function is qualified with forceinline. As a general rule:
• Smaller functions stand a better chance of being inlined.
• Compiling with -Otime increases the likelihood that a function is inlined.
• Large functions are not normally inlined because this can adversely affect code density and performance.
A recursive function is never inlined into itself, even if __forceinline is used.

浓缩成一句话:

开发者决定不了一个函数是否被内联,开发者只有建议权,只有编译器具有决定权。

这就造成了一个很disturbing的事情:除非你看到一个函数的反汇编代码,否则你很难确定他是不是内联函数。

下面,我们来看看一个被static inline修饰的非内联函数:

static __inline int Fake_StaticInline_Add(int n1,int n2,int n3,int n4,int n5)
{/*只是为了多凑几条指令*/n1++;n1++;n1++;n1++;n1++;n1++;n1++;n1++;n1++;n1++;n1++;return (n1+n2+n3+n4+n5);;
}

这个函数我们把他放在main.c中,并在main函数这样调用:i = Fake_StaticInline_Add(6,6,6,6,6);。现在我们来看看他的反汇编代码:

MOVS     r0,#5MOV      r3,r0MOV      r2,r0MOV      r1,r0STR      r0,[sp,#0]BL       Fake_StaticInline_AddMOV      r4,r0NOP

惊不惊喜,意不意外? 这个函数居然是被调用了的,而并没有被内联到调用它的地方。那么,为什么这个函数没变成内联函数呢?
我推断是因为我在这个函数中写入了太多指令,编译器判断如果它变成内联函数,会极大占用空间,所以不将它编译成内联函数。
但你还记得我们之前把Inline_Add函数放在哪里吗?放在了一个头文件。试想,如果这个Inline_Add在没有被编译成内联函数的情况下,被include到了多个源文件中,势必会产生函数重复定义的问题。 因此,我们要再加一个关键字static,才能避免这个问题。

结论

至此,我们直观地理解了static inline的特点,并知道了为什么static和inline要联合使用。
总结一下static inline什么时候用比较好:
当所修饰的函数语句较少时,尤其是只有一两条语句的函数。 当你所修饰的函数不是递归函数,而是正常的函数时。因为递归式不支持内联的。

感想

终于粗略地写完了。这个我原以为一个小时就能完成地实验,实际上做了一下午......
一句话与君共勉:

纸上得来终觉浅,绝知此事要躬行

详解static inline关键字相关推荐

  1. 、简述global关键字的作用_详解static inline关键字

    详解static inline关键字 本文章为知乎用户 @徐yang哟 原创,禁止抄袭! 灵感来源 在查stm32的LL库部分函数的API时,有时会查到这种函数: __STATIC_INLINE vo ...

  2. Java基础:详解static关键字与类加载顺序

    1. 前言 前文中说到了static关键字,在Java中这是一个很重要的关键字,它有很多的用法,并且在某些特定的情况下使用可以优化程序的性能.本文学习static关键字的应用场景.在这之前了解变量的类 ...

  3. [CSS]详解display:inline | block |inline-block的区别

    2019独角兽企业重金招聘Python工程师标准>>> [CSS]详解display:inline | block |inline-block的区别[点评网站][发布新闻][申请专栏 ...

  4. day28 static关键字详解 static在代码中的顺序

    static关键字 static的优先级: 随着类的加载一起存在. static的优势: 可以直接用 不通过new,也可以直接通过类调用 static的缺点: 不可以被方法重写. static在代码中 ...

  5. 一篇带你详解static关键字(超详细)

    文章目录 前言 一.static可以修饰哪些东西? 二.static修饰属性 1.为什么要引入static? 2.final和static的区别 3.修饰属性总结 二.static修饰方法 2.1为什 ...

  6. C++/C语言申请动态空间的详解【new关键字、malloc关键字、delete和free关键字】

    文章目录 [1] C++ ->new关键字 [2] C语言->malloc关键字 [3]C++/C->delete和free关键字 [1] C++ ->new关键字 new: ...

  7. 详解static、volatile、const

    1.背景 在查阅相关资料的时候,无意间看到一个大佬对于static关键字的讲解,如雷贯耳,写得非常容易理解,这是大佬的链接 本人在学习相关知识的时候,喜欢也习惯把从各种书籍或者是各位大佬的博客中学到的 ...

  8. Java线程详解(10)-volatile关键字

    Java 语言中的 volatile 变量可以被看作是一种 "程度较轻的 synchronized":与 synchronized 块相比,volatile 变量所需的编码较少,并 ...

  9. C++ 中explicit关键字详解

    展开 explicit关键字的作用 使用情况 类型转换函数 单操作数构造函数 同时出现拷贝构造函数和类型转换函数 拷贝构造函数 总结 参考文献 explicit关键字的作用 explicit关键字在写 ...

最新文章

  1. 解读 | 2019年10篇计算机视觉精选论文(中)
  2. 神经风格迁移模型综述
  3. 步步为营 .NET 设计模式学习笔记 十四、Decorator(装饰模式)
  4. rabbitMQ、activeMQ、zeroMQ、Kafka、Redis 比较
  5. 【数据结构与算法】之深入解析“我的日程安排表I”的求解思路与算法示例
  6. matlab计算斜方差_计算一幅图像的信噪比
  7. nodjs npm 报错:Segmentation fault: 11
  8. 每日源码分析 - Lodash(remove.js)
  9. 现在小餐厅的推广视频
  10. OpenStack踩坑记录
  11. Python基础之变量和常量
  12. android华为状态栏字体颜色,Flutter修改状态栏颜色以及字体颜色
  13. 坚果云企业版服务器端,坚果云企业版常见问题解答
  14. 2台电脑共享一套键盘鼠标
  15. 基于MicroStation CE的点云软件二次开发
  16. python启动浏览器崩溃
  17. jquery的设置多个 CSS 属性
  18. 微信小程序使用mock.js
  19. origin论文画图记录
  20. 爬虫----request简介(以及urllib模块和request模块保存图片区别)

热门文章

  1. 报表软件等同于BI软件吗?
  2. ROS学习笔记-ROS订阅和发布节点
  3. 错过了竟是一生的遺憾
  4. VS2019:scanf返回值被忽略
  5. 《模拟飞行入坑(一)P3D目录文件介绍》
  6. elasticsearch 与 传统数据库的区别与选用
  7. ucgui在windows上的移植,及为go语言打造简易跨平台GUI的想法
  8. real-Token
  9. oracle 数据库体系结构详解
  10. 糖尿病新世界杂志糖尿病新世界杂志社糖尿病新世界编辑部2023年第1期目录