Table of Contents
导言:重新评价函数调用间接费用
在 C 编程中, 每个函数调用都会引入间接调用: 编译器必须将参数推到堆栈上( 或在注册簿中传递) , 跳转到函数正文, 执行代码, 然后返回。 对于小的、 经常引用的函数, 这种间接调用可以支配执行时间, 特别是在性能关键循环或深嵌操作中。 现代编译器会积极优化, 但有时程序员必须提供明确的提示来实现最大速度 。 其中一条提示是 [ [FLT: 0] 关键词, 它邀请编译器用函数正文本身替换调用调用调用调用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点用点
内在职能背后的机制
内置函数用 [[FLT: 1] 关键词宣布。 这不是命令编译器使用内置; 而是建议 。 编译器可能会忽略它, 其功能太大、 递归性过强, 或优化级别较低。 在 C99 和后来的标准中, [[FLT: 2] ] 的语义被澄清: 以 定义的函数可以包含在多个翻译单元中, 只要存在非内置外部定义, 也可以不引起重复或链接错误。 这常常是用 [ 或 声明的组合来实现 。
- Static inline: 该函数有内部链接;每个翻译单元都获得自己的副本。这是在信头中定义的小助手函数最安全、最便携的方法。
- 外置内置(C99):[ 内置定义提供了内置的正体,但外部定义必须分别存在(通常存在于一个.c文件中). C11 和后来,这种行为是统一的.
- 内无静态或外形: C99中,这与外形内含类似;在C11中,只有在函数不内含的情况下,才需要外形定义. 实际, 多数使用案例都比较可取.
关键洞察力:[ 内置不是免费午餐. 编译器分析成本效益权衡:在每个呼叫站点插入函数的机体会增加代码大小(code bloat),这可以降低指令缓存效率,因此,内置最好保留在小的,常称为函数上.
当内置函数 Excel: 使用案例和最佳做法
小数学操作
进行基本算术的函数——例如计算一个方块、夹住一个值或测试一个符号——是主要候选函数。函数调用时的间接费用往往大于操作本身。例如:
static inline int clamp(int value, int low, int high) {
return (value < low) ? low : (value > high) ? high : value;
}
数据结构中的存取器和变异函数
C 中面向对象的图案经常使用获取器和设置器来封装数据。这些无关紧要的函数不内置,会增加不必要的间接费用:
typedef struct {
int x, y;
} Point;
static inline int point_get_x(const Point *p) {
return p->x;
}
static inline void point_set_x(Point *p, int x) {
p->x = x;
}
嵌入式系统和实时代码
在堆栈空间有限,且定时要求的环境下,内置功能消除了推/流行堆栈帧的需要,减少了耐久性和内存使用,然而,代码大小必须在内存受约束的微控制器上进行仔细监测.
当 不 内置时
- Large函数: 内置一个在多个呼叫站点的100+线函数将膨胀二进制,并可能由于指令缓存压力而降解性能.
- 递归函数: 递归不能完全内含(虽然编译器可能解卷几个关卡).
- 带有环的功能:[] 内嵌一个包含一个大环的函数可能不会提供显著的好处.
- 很少称为函数: 如果函数不经常被称作,则其间接费用可以忽略不计;只内置废物空间。
内置函数 Versus 宏: 详细比较
在关键词为标准之前,C程序员使用宏(])实现"内置"——但宏是文本替换,而不是函数。它们有严重的缺点:
- 类型安全性: 宏忽略类型。臭名昭著的宏会多次评价参数,在使用诸如这样的表达式时会导致危险的副作用。
- 调试:Macros在预处理中消失;调试器不能踩进它们.
- 语句:[ 多语句宏需要丑陋的工作变通(例如]]).
- 名称碰撞:[] 宏扩展可以干扰局部变量.
内置函数克服了所有这些问题:它们是真正的函数,具有类型检查、范围以及侧效果安全参数评价。它们参与常规类型系统,可以调试。宏的唯一理论优势是它们可用于类型-遗传 操作——但C11和C23建议甚至缩小了这一差距。
拇指规则:[] 偏好对任何符合函数签名的逻辑的宏具有函数比宏更优先。只保留宏用于简单的常数或符号粘贴。
实际例子:内在函数在行动中
例1:方块(已经提供)
static inline int square(int x) {
return x * x;
}
编译器很可能不会发出任何调用指令;代码在每一个调用站点都变成简单的].
示例2:检查字符是否为数字
static inline int is_digit(char c) {
return c >= '0' && c <= '9';
}
例3:快敏/最大(弃用宏)
static inline int imax(int a, int b) {
return (a > b) ? a : b;
}
与宏版本不同,此评价]和]精确一次,避免双重评价风险.
例4:比特操作(联盟或字节拼写)
static inline uint16_t swap_bytes(uint16_t x) {
return (x << 8) | (x >> 8);
}
此编码为 ARM 上的单个 [[FLT: 24] ] 指令, 或内嵌时在 x86 上旋转 。
编译器优化和内置关键词
的关键词只是编译器内置决定的一个因素。大多数编译者都有命令行旗来控制攻击性 :
- GCC/Clang:]]允许中度内衬;]允许更积极的内衬。可以明确启用旗。无论编译器的高度如何,都使用[ 或更高程度的“FLT:30]]]]来强制内衬特定函数。
- MSVC:]]关键词可供使用,但并不保证内衬(编译器仍然可以拒绝某些功能).
带有海湾合作委员会属性的示例:
static inline __attribute__((always_inline)) int triple(int x) {
return x * 3;
}
对于性能关键代码,最好检查生成的组装(例如,GCC的或]),以确认内衬发生. 现代编译器可以在高优化水平上不标记的内置函数,反之,对于会导致代码过度增长的函数,则可以忽略.
潜在陷阱:代码 Bloat 和 二进制大小
内插许多地方使用的函数的每个调用,可以大大增加文本段的大小。这对下列情况尤其有问题:
- 字迹:[] 头部的内置函数扩展为包括它们的每一翻译单元,可能乘以代码大小.
- 嵌入式系统:[闪存和RAM是有限的,1000位中使用的10字节函数增加了近10KB的代码.
- 指令缓存:[] 更大的代码可以引起更多的缓存缺失,减缓整个程序.
要减轻代码 bloat, 请只针对真小的函数使用 [[FLT: 37]] (典型的 1–5 语句) 。 使用剖面器在盲目内嵌之前识别热函数。 既测量执行时间, 也测量二进制大小 。
贯穿C标准的内含函数
关键词在C99中被提出,并在C11和C17中进一步澄清. C23保留了相同的语义,并作了一些额外的改进. "内在定义"和"外部定义"之间的历史区分造成了混淆. 在现代实践中,大多数项目只使用,它绕过微妙之处. 这种模式与C99以后的所有C标准都有效,避免了链接器错误.
如果您必须支持 C99 预编译器( 这一点越来越少) , 您必须返回宏或外部标题只执行。 否则, 请接受 [[FLT: 40] , 作为可移植和类型安全的替代方案 。
结论:业绩工程师工具包中的战略工具
内置函数是C语言成熟,定义明确的特征,在明智地应用时,可以通过消除函数调用间接费用和允许交叉函数优化来产生可测量的速度改进,它们几乎在现代的每一个上下文都优于宏,关键在于将其使用限制在小型的热功能,并以剖面和组装检查来验证结果. 结合适当的编译器旗,内置函数使C代码既快速又可维护——低级编程中罕见的组合.
欲进一步阅读,请参看GCC关于内置函数的文件[和的引用条目。对于现实世界的性能分析,本ACM列车条款[详细探讨内置权衡。