用cProfile对fib(22)这一个朴素递归调用做性能分析,看57313次调用和0.008秒耗时背后,ncalls那个57313/1的写法在说什么。
fib(22)不用任何缓存,是最直接的指数级递归。
代码def fib(n): if n < 2: return n return fib(n-1) + fib(n-2) import cProfile, pstats pr = cProfile.Profile() pr.enable() fib(22) pr.disable() pstats.Stats(pr).sort_stats('ncalls').print_stats(3)
n=22看起来很小,但fib的调用次数是按斐波那契数指数增长的。
cProfile输出 57314 function calls (2 primitive calls) in 0.008 seconds ncalls tottime percall cumtime percall filename:lineno(function) 57313/1 0.008 0.000 0.008 0.008 real_py21_30.py:48(fib) 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
关键在57313/1这个写法:分子57313是fib被调用的总次数(这个数字等于2×fib(23)-1,是递归树的节点总数),分母1是"原生调用次数"(primitive calls)——指fib作为不是被自己递归调用、而是从外部直接调用的次数,本例只有最外层fib(22)这一次调用满足这个条件,剩下57312次都是fib在内部递归调用自己产生的。cProfile用这个斜杠写法,是在告诉你一个函数"被别人调用"和"被自己递归调用"这两种不同的调用来源各有多少次。
对fib这种情况,两列数字都显示0.008。
tottime 是这个函数自身代码执行的耗时,不含它调用的子函数;cumtime 是这个函数自身加上它调用的全部子函数的累计耗时。因为fib唯一调用的子函数就是fib自己,cProfile会把递归调用自身产生的耗时全部计入同一行统计(而不是拆成57313行各自独立的记录),所以对这类"只调用自己"的递归函数,tottime本身就已经把整棵递归树的耗时都算进去了,cumtime跟它自然相等。要看清递归内部各层级的耗时分布,cProfile这种按函数名合并统计的方式反而是不够的,需要换更细粒度的行级profiler。
每一步都是本文上面用cProfile跑出来的原始结果。
cProfile / pstats:统计朴素递归fib(22)的调用次数与耗时