本节书摘来自异步社区《Python高性能编程》一书中的第2章,第2.4节,作者[美] 戈雷利克 (Micha Gorelick),胡世杰,徐旭彬 译,更多章节内容可以访问云栖社区“异步社区”公众号查看。
2.4 计时的简单方法——打印和修饰
运行例2-4,我们看到的输出是由代码中几句print语句生成的。在Ian的笔记本上用CPython 2.7跑这段代码要花大约12秒。运行时间一般都会有一些波动。你必须在计时的同时观察这些正常的变化,否则你可能会误把某个随机的运行时间的变化当作是由于某次代码的改进造成的。
你的计算机在运行你的代码时还会进行其他任务,比如访问网络、磁盘或RAM,这些因素都会导致程序运行时间的变化。
Ian的笔记本是一台Dell E6420,拥有一个Intel Core I7-2720QM的CPU(2.20GHz,6MB缓存,4核)以及8GB的RAM,操作系统是Ubuntu 13.10。
在calc_pure_python中(例2-2),我们能看到一些print语句。这是最简单的在函数内部测量一段代码执行时间的方法。这个基本方法既快且脏,是在你刚开始着手调查代码时非常有用的手段。
在代码除错和性能分析上使用print语句是常用的手段。虽然它很快就会变得无法管理,但适用于简短的调查。用完它们以后要记得收拾干净,否则它们会搞乱你的stdout。
一个稍微干净一点的方法是使用修饰器——在需要调查的函数上面增加一行代码。我们的修饰器十分简单,仅仅复制了print语句的功能。后面我们会让它变得更加高级。
在例2-5中,我们定义了一个新函数timefn,它以一个函数fn为参数。它的内嵌函数measure_time接受args(数量可变的位置参数)以及*kwargs(数量可变的键值对参数)等参数并将其传入fn执行。在执行fn前后,我们抓取time.time()并将结果和fn.func_name一起打印出来。使用这个修饰器的开销很小,但如果你调用上千万次fn,开销就会变得引人注意。我们用@wraps(fn)将函数名和docstring暴露给fn的调用者(否则调用者看到的将是修饰器自身的函数名和docstring,而不是被修饰的函数的)。
例2-5 定义一个修饰器来自动测量时间
from functools import wrapsdef timefn(fn):@wraps(fn)def measure_time(*args, **kwargs):t1 = time.time()result = fn(*args, **kwargs)t2 = time.time()print ("@timefn:" + fn.func_name + " took " + str(t2 - t1) + " seconds")return resultreturn measure_time@timefn
def calculate_z_serial_purepython(maxiter, zs, cs):...
当我们运行这个版本的代码时(之前的print语句依然保留),我们会看到修饰器打印的执行时间要略快于calc_pure_python打印的时间。这是由于函数的调用带来了额外开销(差异非常小):
Length of x: 1000
Total elements: 1000000
@timefn:calculate_z_serial_purepython took 12.2218790054 seconds
calculate_z_serial_purepython took 12.2219250043 seconds
备忘 额外的分析信息不可避免地降低了代码的执行速度——某些性能分析选项非常详细以至于带来了巨大的性能代价。分析信息的细节程度和运行速度是你必须要进行权衡的。
我们可以用timeit模块作为另一种测量执行速度的方法。通常来说,你会在解决问题的过程中用它来为各种简单的语句计时。
警告 注意,timeit模块暂时禁用了垃圾收集器。如果你的操作会调用到垃圾收集器,那么它有可能影响到你实际操作的速度。更多信息请参见Python文档:http://bit.ly/timeit_doc。
你可以从命令行运行timeit如下:
$ python -m timeit -n 5 -r 5 -s "import julia1""julia1.calc_pure_python(desired_width=1000,max_iterations=300)"
注意你必须以-s命令在设置阶段导入julia1模块,因为calc_pure_python来自那个模块。timeit有一些合理的默认值适用于一段简短的代码,但对于要长期运行的代码来说,最好指定循环次数(-n 5)以及重复次数(-r 5)。timeit会对语句循环执行n次并计算平均值作为一个结果,重复r次并选出最好的那个结果。
如果我们不指定-n和-r运行timeit,默认是循环10次重复5次,需要6分钟。改变默认值可以让你更快获得结果。
我们只关注最好的那个结果,平均值以及最差结果可能是由于其他进程的影响。选择循环5次重复5次应该能给我们一个较为公正的结果:
5 loops, best of 5: 13.1 sec per loop
试着多运行几次,看看我们会不会得到不同的结果——你可能需要更多的重复次数来获得一个稳定的最佳结果时间。在这一点上不存在“正确”的配置,所以如果你发现你的计时结果变动范围很广,你就要选择更高的重复次数,直到获得稳定的最终结果。
我们的结果显示调用calc_pure_python的总体开销是13.1秒(最佳情况),而@timefn修饰器测算的单次调用calculate_z_serial_purepython耗时12.2秒。中间的差别主要是用于创建zs和cs列表的时间。
在IPython内部,我们可以用同样的方式使用%timeit魔法函数。如果你在IPython中用互动的方式开发你的代码且函数在本地名字空间(可能是因为你正在使用%run),那么你可以用:
%timeit calc_pure_python(desired_width=1000, max_iterations=300)
还有一点值得考虑的是一台计算机上的负载变化。很多后台运行的任务(如Dropbox、备份等)都会随机影响CPU和磁盘资源。网页上的脚本也会导致不可预测的资源使用。图2-4显示了我们刚刚进行的计时过程中某个CPU的使用率达到了100%,机器中的其他核心都在轻松处理其他的任务。
系统监视器会时不时地显示这台机器上的活动峰值。有必要检查会不会有其他事件发生影响了你的关键资源(CPU、磁盘、网络)。