随便聊聊:静态资源分析
简单地说,程序的资源分析(Resource Analysis)指的是把该程序的资源消耗表示成一个关于程序输入的函数。
这里的资源可以是运行时间、内存使用、能源消耗,或别的什么数值指标。
高德纳(Donald Knuth)在《计算机程序设计的艺术》(The Art of Computer Programming)中就基于 MIX 汇编语言的语义讨论了一些资源分析的问题。
在计算机科学的课程中,资源分析通常以分析算法、数据结构的时间、空间复杂度的形式出现,而这里面最常见的又通常是渐近分析,即忽略常数、假定输入规模充分大时的复杂度,例如对长度为
静态资源分析即在不实际运行程序的情况下对其进行资源分析。 Facebook 出品的 Infer 工具提供了函数级的静态运行时间分析,其宗旨是在软件开发的过程中更早地指出性能问题(比方说,在 Code Review 阶段由工具自动反馈,而不是等到后面的性能回归测试)。 下面这个例子来源于 Infer 的官方文档1:
void loop(ArrayList<Integer> list){
for (int i = 0; i <= list.size(); i++){
}
}
Infer 通过静态分析对上面这个函数得出一个描述其运行时间的多项式(比如
void loop(ArrayList<Integer> list){
for (int i = 0; i <= list.size(); i++){
foo(i); // newly added function call
}
}
而此处的 foo 函数的时间复杂度也是与输入呈线性关系,那么 Infer 可以检测到 loop 函数的复杂度从
笼统地说,Infer 中的资源分析可以大致理解为对循环次数进行计数,从而将资源分析问题转化为程序变量数值关系的分析;这也是很多静态资源分析(比如 SPEED2)背后的原理。
在这篇文章中,我们来聊另一种颇为有趣的静态资源分析:自动均摊资源分析(Automatic Amortized Resource Analysis,AARA)。
这种方法衍生自 Robert Tarjan 在 1985 年的一篇论文3,文中给出了一种推导序列操作的最坏情况资源消耗的方法。
该方法的思路可以概括为:对于很多数据结构来说,一个操作消耗的资源很大程度上是由数据结构的状态决定的,而且有可能会根据状态有很大的不同;但是比较高的资源消耗(例如重新组织整个数据结构)往往会以一种可以预测的频率出现,也就是说,这些消耗在时间轴上均摊了。
均摊资源分析即利用了这种思想:我们可以把程序的执行看做是一个操作序列
即一个状态的势能要足以支付当前程序操作的资源消耗以及下一个程序状态的势能(思考题:
我们知道,类型系统的一大特点也是 Compositionality;所以,设计一个用于均摊资源分析的类型系统是非常自然的想法。 考虑下面这段 OCaml 代码,它是连接两个列表的简单实现,而我们想要分析它递归调用的次数(这也是一种资源消耗):
let rec append (l1, l2) =
match l1 with
| [] -> l2
| x :: xs ->
let rest = append (xs, l2) in
x :: rest
我们能够很容易地看出 append 函数递归调用的次数是 append 函数的类型为 append 的参数 append 的带资源标注的类型可以写为 append 的类型签名(当然!)。
最后,类型签名告诉我们
我在这份胶片里更详细地展示了这个带资源标注的类型系统。 需要特别提出的是,自动推导这些数值标注并不困难:我们用变量来表示这些类型标注,那么类型检查就成为了生成关于这些变量的约束(通常是线性的)的过程,最后再解这些约束就好了。 在这个页面上你可以和一个实现了自动均摊资源分析的工具(Resource-aware ML,RaML)玩耍,其主要贡献者也在今年发表了一篇还不错的综述4。 祝大家玩得愉快!
Footnotes
-
https://www.microsoft.com/en-us/research/publication/speed-precise-and-efficient-static-estimation-of-program-computational-complexity-2/ ↩
-
Robert Tarjan. Amortized Computational Complexity. https://doi.org/10.1137/0606031 ↩
-
Jan Hoffmann and Steffen Jost. Two decades of automatic amortized resource analysis. https://doi.org/10.1017/S0960129521000487 ↩