Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

函数内联(进阶)

复习

  • 循环优化:减少循环内的重复工作
  • 函数调用与运行栈:调用本身有开销
  • 优化为什么必须保留行为:等价是前提

本章为进阶内容,零基础读者可以跳过,不影响后续阅读。

TL;DR

  • 内联把被调函数的代码直接嵌入调用处
  • 它省去调用开销,并为更多优化创造条件
  • 代价是代码体积膨胀
  • 是否内联,需要权衡

正文

  函数调用不是免费的:要传参、要保存返回地址、要跳转、要建立栈帧、要返回。对于很小又被频繁调用的函数,这些开销甚至可能超过函数体本身的工作量。

  一个直接的办法是:别调了,把函数体直接“塞”进调用处。 这就是函数内联(function inlining)。

好处不止是省调用

  内联最直接的好处,是省掉调用的一整套开销。但更妙的是,它常常能打开新的优化空间

函数 加一(x): 返回 x + 1
调用处: a = 加一(5)

  内联之后,变成了 a = 5 + 1,再一折叠就是 a = 6内联把“跨越函数边界”的计算,拉到了同一个上下文里,于是常量传播、公共子表达式消除、死代码删除等优化,都更容易施展。

  这也是为什么内联常被视为“优化的放大器”:它自己不总是最亮眼的那个,却让别的优化更有用武之地。

代价:代码变大

  但内联不是白来的,代价是代码体积膨胀。一个函数被调十次,内联后就复制了十份。代码变大,可能带来两个问题:

  • 占更多内存,指令缓存更容易装不下,反而变慢
  • 编译产物变大,编译时间也可能增加

  所以,内联不是“越多越好”,而是一次精打细算的取舍。

怎么决定

  编译器通常会挑小函数热点函数(被频繁调用的)优先内联,对体积大、调用少的函数则保守一些。此外,递归函数不能无限内联——否则会无限展开下去,必须设一个界限。

  “用体积换速度,还是保体积”,又一次是那个熟悉的权衡。关键看那点速度值不值得那些体积。

思考题 1

  函数内联能带来哪些好处?

思考题 2

  函数内联的代价是什么?

小结

知识点

  • 内联把函数体嵌入调用处,省去调用开销
  • 它常为其他优化创造条件
  • 代价是代码体积膨胀,可能影响指令缓存
  • 通常优先内联小函数与热点函数

参考资料

  1. Wikipedia(zh):内联函数:把函数体嵌入调用处的优化
  2. Wikipedia(zh):编译器优化:内联所属的优化范畴

思考题答案(仅供参考)

思考题 1

  一是省去函数调用的开销(传参、跳转、建栈、返回等);二是把调用与被调函数的代码拉到同一上下文,使常量传播、公共子表达式消除等优化更容易施展,从而间接带来更多优化收益。

思考题 2

  代价是代码体积膨胀:函数被调用多少次就复制多少份,占用更多内存,可能使指令缓存命中率下降,编译产物与编译时间也可能增加。因此内联需要权衡,并非越多越好。

协议

  本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

封面图

设计师 | 南国微雪