GUN C编译器拓展语法学习笔记(三)内联函数、内建函数与可变参数宏

内联函数、内建函数与可变参数宏

    • 一、内联函数
      • 1.1 属性声明:noinline
      • 1.2 内联函数与宏
      • 1.3 编译器对内联函数的处理
      • 1.4 内联函数为什么定义在头文件中
    • 二、内建函数
      • 2.1 内建函数定义
      • 2.2 常用的内建函数
      • 2.3 C标准库的内建函数
      • 2.4 内建函数:`__builtin_constant_p(n)`
      • 2.5 内建函数:`__builtin_expect(exp,c)`
      • 2.6 Linux内核中的likely和unlikely
    • 三、可变参数宏
      • 3.1 可变参数宏定义
      • 3.2 改进版本
      • 3.3 另外一种写法
      • 3.4 内核中的可变参数宏

一、内联函数

1.1 属性声明:noinline

  内联函数相关的两个属性:noinlinealways_inline。这两个属性的用途是告诉编译器,在编译时,对我们指定的函数内联展开或不展开。
在这里插入图片描述
  一个使用inline声明的函数被称为内联函数,内联函数一般前面会有static和extern修饰。使用inline声明一个内联函数,和使用关键字register声明一个寄存器变量一样,只是建议编译器在编译时内联展开。
  对于函数调用中,有些函数短小精悍,而且调用频繁,调用开销大,算下来性价比不高,这时候我们就可以将这个函数声明为内联函数。编译器在编译过程中遇到内联函数,像宏一样,将内联函数直接在调用处展开,这样做就减少了函数调用的开销:直接执行内联函数展开的代码,不用再保存现场和恢复现场。

1.2 内联函数与宏

  看到这里,可能就有人疑问了:内联函数和宏的功能差不多,那么为什么不直接定义一个宏,而去定义一个内联函数呢?
  与宏相比,内联函数有以下优势。

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

1.3 编译器对内联函数的处理

  内联函数会增大程序的体积,如果在一个文件中多次调用内联函数,多次展开,那么整个程序的体积就会变大,在一定程度上会降低程序的执行效率。编译器会根据实际情况进行评估,权衡展开和不展开的利弊,并最终决定要不要展开。编译器在对内联函数做展开时,除了检测用户定义的内联函数内部是否有指针、循环、递归,还会在函数执行效率和函数调用开销之间进行权衡。
  一般来讲,判断对一个内联函数是否做展开,从程序员的角度出发,主要考虑如下因素。

  • 函数体积小。
  • 函数体内无指针赋值、递归、循环等语句。
  • 调用频繁。
      当我们认为一个函数体积小,而且被大量频繁调用,应该做内联展开时,就可以使用static inline关键字修饰它。但编译器不一定会做内联展开,如果你想明确告诉编译器一定要展开,或者不展开,就可以使用noinlinealways_inline对函数做一个属性声明。

1.4 内联函数为什么定义在头文件中

   问:内联函数为什么要定义在头文件中呢?
   答:因为它是一个内联函数,可以像宏一样使用,任何想使用这个内联函数的源文件,都不必亲自再去定义一遍,直接包含这个头文件,即可像宏一样使用。
   问:为什么还要用static修饰呢?
   答:因为我们使用inline定义的内联函数,编译器不一定会内联展开,那么当一个工程中多个文件都包含这个内联函数的定义时,编译时就有可能报重定义错误。而使用static关键字修饰,则可以将这个函数的作用域限制在各自的文件内,避免重定义错误的发生。

二、内建函数

2.1 内建函数定义

  内建函数,就是编译器内部实现的函数。这些函数和关键字一样可以直接调用,无须像标准库函数那样,要先声明后使用。
  内建函数的函数命名,通常以__builtin开头。这些函数主要在编译器内部使用,主要是为编译器服务的。内建函数的主要用途如下。

  • 用来处理变长参数列表。
  • 用来处理程序运行异常、编译优化、性能优化。
  • 查看函数运行时的底层信息、堆栈信息等。
  • 实现C标准库的常用函数。
      因为内建函数是在编译器内部定义的,主要供与编译器相关的工具和程序调用,所以这些函数并没有文档说明,而且变动又频繁,对于应用程序开发者来说,不建议使用这些函数。但有些函数,对于我们了解程序运行的底层机制、编译优化很有帮助,在Linux内核中也经常使用这些函数,所以我们很有必要了解Linux内核中常用的一些内建函数。

2.2 常用的内建函数

  常用的内建函数主要有两个:__builtin_return_address()和__builtin_frame_address()。
  __builtin_return_address(),其函数原型如下。
在这里插入图片描述
  这个函数用来返回当前函数或调用者的返回地址。函数的参数LEVEL表示函数调用链中不同层级的函数。

● 0:获取当前函数的返回地址。
● 1:获取上一级函数的返回地址。
● 2:获取上二级函数的返回地址。
● ……

  另一个常用的内建函数__builtin_frame_address(),其函数原型如下。
在这里插入图片描述
  在函数调用过程中,还有一个栈帧的概念。函数每调用一次,都会将当前函数的现场(返回地址、寄存器、临时变量等)保存在栈中,每一层函数调用都会将各自的现场信息保存在各自的栈中。这个栈就是当前函数的栈帧,每一个栈帧都有起始地址和结束地址,多层函数调用就会有多个栈帧,每个栈帧都会保存上一层栈帧的起始地址,这样各个栈帧就形成了一个调用链。
  我们通过内建函数__builtin_frame_address(LEVEL)查看函数的栈帧地址。
● 0:查看当前函数的栈帧地址。
● 1:查看上一级函数的栈帧地址。
● ……

2.3 C标准库的内建函数

  在GNU C编译器内部,C标准库的内建函数实现了一些与C标准库函数类似的内建函数。这些函数与C标准库函数功能相似,函数名也相同,只是在前面加了一个前缀__builtin
  常见的C标准库函数如下。
● 与内存相关的函数:memcpy()、memset()、memcmp()
● 数学函数:log()、cos()、abs()、exp()
● 字符串处理函数:strcat()、strcmp()、strcpy()、strlen()
● 打印函数:printf()、scanf()、putchar()、puts()
  使用与C标准库对应的内建函数,同样能实现字符串的复制和打印,实现C标准库函数的功能。

2.4 内建函数:__builtin_constant_p(n)

  该函数主要用来判断参数n在编译时是否为常量。如果是常量,则函数返回1,否则函数返回0。该函数常用于宏定义中,用来编译优化。比如一个宏定义,根据宏的参数是常量还是变量,可能实现的方法不一样。比如如下内核源码。
在这里插入图片描述GUN C编译器拓展语法学习笔记(三)内联函数、内建函数与可变参数宏_第1张图片

2.5 内建函数:__builtin_expect(exp,c)

  内建函数__builtin_expect()也常常用来编译优化,这个函数有2个参数,返回值就是其中一个参数,仍是exp。该内建函数作用是告诉编译器:参数exp的值为c的可能性很大,然后编译器可以根据这个提示信息,做一些分支预测上的代码优化。参数c与这个函数的返回值无关,无论c为何值,函数的返回值都是exp。主要用途是编译器的分支预测优化
  现在CPU内部都有Cache缓存器件。CPU的运行速度很高,而外部RAM的速度相对来说就低了不少,所以当CPU从内存RAM读写数据时就会有一定的性能瓶颈。为了提高程序执行效率,CPU一般都会通过Cache这个CPU内部缓冲区来缓存一定的指令或数据,当CPU读写内存数据时,会先到Cache看看能否找到:如果找到就直接进行读写;如果找不到,则Cache会重新缓存一部分数据进来。CPU读写Cache的速度远远大于内存RAM,所以通过这种缓存方式可以提高系统的性能。
  那么Cache如何缓存内存数据呢?简单来说,就是依据空间相近原则。如CPU正在执行一条指令,那么在下一个时钟周期里,CPU一般会大概率执行当前指令的下一条指令。如果此时Cache将下面的几条指令都缓存到Cache里,则下一个时钟周期里,CPU就可以直接到Cache里取指、译指和执行,从而使运算效率大大提高。
  但有时候也会出现意外。如程序在执行过程中遇到函数调用、if分支、goto跳转等程序结构,会跳到其他地方执行,原先缓存到Cache里的指令不是CPU要执行的指令。此时,我们就说Cache没有命中,Cache会重新缓存正确的指令代码供CPU读取,这就是Cache工作的基本流程。
  我们在编写程序时,**遇到if/switch这种选择分支的程序结构,一般建议将大概率发生的分支写在前面。**当程序运行时,因为大概率发生,所以大部分时间就不需要跳转,程序就相当于一个顺序结构,Cache的缓存命中率也会大大提升。内核中已经实现一些相关的宏,如likely和unlikely,用来提醒程序员优化程序。

2.6 Linux内核中的likely和unlikely

  在Linux内核中,我们使用__builtin_expect()内建函数,定义了两个宏。在这里插入图片描述
  这两个宏的主要作用就是告诉编译器:某一个分支发生的概率很高,或者很低,基本不可能发生。编译器根据这个提示信息,在编译程序时就会做一些分支预测上的优化。
  在这两个宏的定义中有一个细节,就是对宏的参数x做两次取非操作,这是为了将参数x转换为布尔类型,然后与1和0直接做比较,告诉编译器x为真或假的可能性很高。

三、可变参数宏

  变参函数的定义和使用,基本套路就是使用va_list、va_start、va_end等宏,去解析那些可变参数列表。GNU C觉得这样不过瘾,再来一个“神助攻”:干脆宏定义也支持可变参数吧!

3.1 可变参数宏定义

  可变参数宏的实现形式其实和变参函数差不多:用…表示变参列表,变参列表由不确定的参数组成,各个参数之间用逗号隔开。如下程序所示。可变参数宏使用C99标准新增加的一个__VA_ARGS__预定义标识符来表示前面的变参列表,而不是像变参函数一样,使用va_list、va_start、va_end这些宏去解析变参列表。预处理器在将宏展开时,会用变参列表替换掉宏定义中的所有__VA_ARGS__标识符。
GUN C编译器拓展语法学习笔记(三)内联函数、内建函数与可变参数宏_第2张图片
  上面这个程序在编译时就会报错,产生一个语法错误。这是因为,我们只给LOG宏传递了一个参数,而变参为空。当宏展开后,就变成了下面的样子。
在这里插入图片描述
  宏展开后,在第一个字符串参数的后面还有一个逗号,不符合语法规则,所以就产生了一个语法错误。我们需要继续对这个宏进行改进,使用宏连接符##,可以避免这个语法错误。

3.2 改进版本

  我们在标识符__VA_ARGS__前面加上了宏连接符##,这样做的好处是:当变参列表非空时,##的作用是连接fmt和变参列表,各个参数之间用逗号隔开,宏可以正常使用;当变参列表为空时,##还有一个特殊的用处,它会将固定参数fmt后面的逗号删除掉,这样宏就可以正常使用了。在这里插入图片描述在这里插入图片描述

3.3 另外一种写法

  当我们定义一个变参宏时,除了使用预定义标识符__VA_ARGS__表示变参列表,还可以使用下面这种写法。
在这里插入图片描述
  上面这种格式是GNU C扩展的一个新写法:可以不使用__VA_ARGS__,而是直接使用args...来表示一个变参列表,然后在后面的宏定义中,直接使用args代表变参列表就可以了。为了避免变参列表为空时的语法错误,我们也需要在参数之间添加一个连接符##
GUN C编译器拓展语法学习笔记(三)内联函数、内建函数与可变参数宏_第3张图片

3.4 内核中的可变参数宏

  可变参数宏在内核中主要用于日志打印。一些驱动模块或子系统有时候会定义自己的打印宏,支持打印开关、打印格式、优先级控制等功能。

GUN C编译器拓展语法学习笔记(三)内联函数、内建函数与可变参数宏_第4张图片GUN C编译器拓展语法学习笔记(三)内联函数、内建函数与可变参数宏_第5张图片
  这个宏定义了三个版本:如果我们在编译内核时有动态调试选项,那么这个宏就定义为dynamic_pr_debug。如果没有配置动态调试选项,则我们可以通过DEBUG这个宏,来控制这个宏的打开和关闭。
  no_printk()作为一个内联函数,定义在printk.h头文件中,而且通过format属性声明,指示编译器按照printf标准去做参数格式检查。
  最有意思的是dynamic_pr_debug这个宏,宏定义采用do{…}while(0)结构。这看起来貌似有点多余:有它没它,我们的宏都可以工作。反正都是执行一次,为什么要用这种看似“画蛇添足”的循环结构呢?道理其实很简单,这样定义是为了防止宏在条件、选择等分支结构的语句中展开后,产生宏歧义。

你可能感兴趣的:(嵌入式系开发\32\树莓派,c语言,学习,笔记)