函数内联(进阶)
复习
- 循环优化:减少循环内的重复工作
- 函数调用与运行栈:调用本身有开销
- 优化为什么必须保留行为:等价是前提
本章为进阶内容,零基础读者可以跳过,不影响后续阅读。
TL;DR
- 内联把被调函数的代码直接嵌入调用处
- 它省去调用开销,并为更多优化创造条件
- 代价是代码体积膨胀
- 是否内联,需要权衡
正文
函数调用不是免费的:要传参、要保存返回地址、要跳转、要建立栈帧、要返回。对于很小又被频繁调用的函数,这些开销甚至可能超过函数体本身的工作量。
一个直接的办法是:别调了,把函数体直接“塞”进调用处。 这就是函数内联(function inlining)。
好处不止是省调用
内联最直接的好处,是省掉调用的一整套开销。但更妙的是,它常常能打开新的优化空间:
函数 加一(x): 返回 x + 1
调用处: a = 加一(5)
内联之后,变成了 a = 5 + 1,再一折叠就是 a = 6。内联把“跨越函数边界”的计算,拉到了同一个上下文里,于是常量传播、公共子表达式消除、死代码删除等优化,都更容易施展。
这也是为什么内联常被视为“优化的放大器”:它自己不总是最亮眼的那个,却让别的优化更有用武之地。
代价:代码变大
但内联不是白来的,代价是代码体积膨胀。一个函数被调十次,内联后就复制了十份。代码变大,可能带来两个问题:
- 占更多内存,指令缓存更容易装不下,反而变慢
- 编译产物变大,编译时间也可能增加
所以,内联不是“越多越好”,而是一次精打细算的取舍。
怎么决定
编译器通常会挑小函数和热点函数(被频繁调用的)优先内联,对体积大、调用少的函数则保守一些。此外,递归函数不能无限内联——否则会无限展开下去,必须设一个界限。
“用体积换速度,还是保体积”,又一次是那个熟悉的权衡。关键看那点速度值不值得那些体积。
思考题 1
函数内联能带来哪些好处?
思考题 2
函数内联的代价是什么?
小结
知识点
- 内联把函数体嵌入调用处,省去调用开销
- 它常为其他优化创造条件
- 代价是代码体积膨胀,可能影响指令缓存
- 通常优先内联小函数与热点函数
参考资料
- Wikipedia(zh):内联函数:把函数体嵌入调用处的优化
- Wikipedia(zh):编译器优化:内联所属的优化范畴
思考题答案(仅供参考)
思考题 1
一是省去函数调用的开销(传参、跳转、建栈、返回等);二是把调用与被调函数的代码拉到同一上下文,使常量传播、公共子表达式消除等优化更容易施展,从而间接带来更多优化收益。
思考题 2
代价是代码体积膨胀:函数被调用多少次就复制多少份,占用更多内存,可能使指令缓存命中率下降,编译产物与编译时间也可能增加。因此内联需要权衡,并非越多越好。
协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
封面图
设计师 | 南国微雪