内联函数相关的两个属性:noinline
和always_inline
。这两个属性的用途是告诉编译器,在编译时,对我们指定的函数内联展开或不展开。
一个使用inline声明的函数被称为内联函数,内联函数一般前面会有static和extern修饰。使用inline
声明一个内联函数,和使用关键字register
声明一个寄存器变量一样,只是建议编译器在编译时内联展开。
对于函数调用中,有些函数短小精悍,而且调用频繁,调用开销大,算下来性价比不高,这时候我们就可以将这个函数声明为内联函数。编译器在编译过程中遇到内联函数,像宏一样,将内联函数直接在调用处展开,这样做就减少了函数调用的开销:直接执行内联函数展开的代码,不用再保存现场和恢复现场。
看到这里,可能就有人疑问了:内联函数和宏的功能差不多,那么为什么不直接定义一个宏,而去定义一个内联函数呢?
与宏相比,内联函数有以下优势。
ANSI C
说的,因为现在宏也可以有返回值和类型了,如前面使用语句表达式定义的宏。 内联函数会增大程序的体积,如果在一个文件中多次调用内联函数,多次展开,那么整个程序的体积就会变大,在一定程度上会降低程序的执行效率。编译器会根据实际情况进行评估,权衡展开和不展开的利弊,并最终决定要不要展开。编译器在对内联函数做展开时,除了检测用户定义的内联函数内部是否有指针、循环、递归,还会在函数执行效率和函数调用开销之间进行权衡。
一般来讲,判断对一个内联函数是否做展开,从程序员的角度出发,主要考虑如下因素。
static inline
关键字修饰它。但编译器不一定会做内联展开,如果你想明确告诉编译器一定要展开,或者不展开,就可以使用noinline
或always_inline
对函数做一个属性声明。 问:内联函数为什么要定义在头文件中呢?
答:因为它是一个内联函数,可以像宏一样使用,任何想使用这个内联函数的源文件,都不必亲自再去定义一遍,直接包含这个头文件,即可像宏一样使用。
问:为什么还要用static修饰呢?
答:因为我们使用inline定义的内联函数,编译器不一定会内联展开,那么当一个工程中多个文件都包含这个内联函数的定义时,编译时就有可能报重定义错误。而使用static关键字修饰,则可以将这个函数的作用域限制在各自的文件内,避免重定义错误的发生。
内建函数,就是编译器内部实现的函数。这些函数和关键字一样,可以直接调用,无须像标准库函数那样,要先声明后使用。
内建函数的函数命名,通常以__builtin
开头。这些函数主要在编译器内部使用,主要是为编译器服务的。内建函数的主要用途如下。
常用的内建函数主要有两个:__builtin_return_address()和__builtin_frame_address()。
__builtin_return_address(),其函数原型如下。
这个函数用来返回当前函数或调用者的返回地址。函数的参数LEVEL表示函数调用链中不同层级的函数。
● 0:获取当前函数的返回地址。
● 1:获取上一级函数的返回地址。
● 2:获取上二级函数的返回地址。
● ……
另一个常用的内建函数__builtin_frame_address()
,其函数原型如下。
在函数调用过程中,还有一个栈帧的概念。函数每调用一次,都会将当前函数的现场(返回地址、寄存器、临时变量等)保存在栈中,每一层函数调用都会将各自的现场信息保存在各自的栈中。这个栈就是当前函数的栈帧,每一个栈帧都有起始地址和结束地址,多层函数调用就会有多个栈帧,每个栈帧都会保存上一层栈帧的起始地址,这样各个栈帧就形成了一个调用链。
我们通过内建函数__builtin_frame_address(LEVEL)
查看函数的栈帧地址。
● 0:查看当前函数的栈帧地址。
● 1:查看上一级函数的栈帧地址。
● ……
在GNU C编译器内部,C标准库的内建函数实现了一些与C标准库函数类似的内建函数。这些函数与C标准库函数功能相似,函数名也相同,只是在前面加了一个前缀__builtin
。
常见的C标准库函数如下。
● 与内存相关的函数:memcpy()、memset()、memcmp()
。
● 数学函数:log()、cos()、abs()、exp()
。
● 字符串处理函数:strcat()、strcmp()、strcpy()、strlen()
。
● 打印函数:printf()、scanf()、putchar()、puts()
。
使用与C标准库对应的内建函数,同样能实现字符串的复制和打印,实现C标准库函数的功能。
__builtin_constant_p(n)
该函数主要用来判断参数n在编译时是否为常量。如果是常量,则函数返回1,否则函数返回0。该函数常用于宏定义中,用来编译优化。比如一个宏定义,根据宏的参数是常量还是变量,可能实现的方法不一样。比如如下内核源码。
__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,用来提醒程序员优化程序。
在Linux内核中,我们使用__builtin_expect()内建函数,定义了两个宏。
这两个宏的主要作用就是告诉编译器:某一个分支发生的概率很高,或者很低,基本不可能发生。编译器根据这个提示信息,在编译程序时就会做一些分支预测上的优化。
在这两个宏的定义中有一个细节,就是对宏的参数x做两次取非操作,这是为了将参数x转换为布尔类型,然后与1和0直接做比较,告诉编译器x为真或假的可能性很高。
变参函数的定义和使用,基本套路就是使用va_list、va_start、va_end等宏,去解析那些可变参数列表。GNU C觉得这样不过瘾,再来一个“神助攻”:干脆宏定义也支持可变参数吧!
可变参数宏的实现形式其实和变参函数差不多:用…表示变参列表,变参列表由不确定的参数组成,各个参数之间用逗号隔开。如下程序所示。可变参数宏使用C99标准新增加的一个__VA_ARGS__
预定义标识符来表示前面的变参列表,而不是像变参函数一样,使用va_list、va_start、va_end
这些宏去解析变参列表。预处理器在将宏展开时,会用变参列表替换掉宏定义中的所有__VA_ARGS__
标识符。
上面这个程序在编译时就会报错,产生一个语法错误。这是因为,我们只给LOG宏传递了一个参数,而变参为空。当宏展开后,就变成了下面的样子。
宏展开后,在第一个字符串参数的后面还有一个逗号,不符合语法规则,所以就产生了一个语法错误。我们需要继续对这个宏进行改进,使用宏连接符##,可以避免这个语法错误。
我们在标识符__VA_ARGS__前面加上了宏连接符##,这样做的好处是:当变参列表非空时,##的作用是连接fmt和变参列表,各个参数之间用逗号隔开,宏可以正常使用;当变参列表为空时,##还有一个特殊的用处,它会将固定参数fmt后面的逗号删除掉,这样宏就可以正常使用了。
当我们定义一个变参宏时,除了使用预定义标识符__VA_ARGS__表示变参列表,还可以使用下面这种写法。
上面这种格式是GNU C扩展的一个新写法:可以不使用__VA_ARGS__
,而是直接使用args...
来表示一个变参列表,然后在后面的宏定义中,直接使用args代表变参列表就可以了。为了避免变参列表为空时的语法错误,我们也需要在参数之间添加一个连接符##
。
可变参数宏在内核中主要用于日志打印。一些驱动模块或子系统有时候会定义自己的打印宏,支持打印开关、打印格式、优先级控制等功能。
这个宏定义了三个版本:如果我们在编译内核时有动态调试选项,那么这个宏就定义为dynamic_pr_debug。如果没有配置动态调试选项,则我们可以通过DEBUG这个宏,来控制这个宏的打开和关闭。
no_printk()作为一个内联函数,定义在printk.h头文件中,而且通过format属性声明,指示编译器按照printf标准去做参数格式检查。
最有意思的是dynamic_pr_debug这个宏,宏定义采用do{…}while(0)结构。这看起来貌似有点多余:有它没它,我们的宏都可以工作。反正都是执行一次,为什么要用这种看似“画蛇添足”的循环结构呢?道理其实很简单,这样定义是为了防止宏在条件、选择等分支结构的语句中展开后,产生宏歧义。