本节书摘来自异步社区《python高性能编程》一书中的第2章,第2.8节,作者[美] 戈雷利克 (micha gorelick),胡世杰,徐旭彬 译,更多章节内容可以访问云栖社区“异步社区”公众号查看。
根据ian的观点,robert kern的line_profiler是调查python的cpu密集型性能问题最强大的工具。它可以对函数进行逐行分析,你应该先用cprofile找到需要分析的函数,然后用line_profiler对函数进行分析。
当你修改你的代码时,值得打印出这个工具的输出以及代码的版本,这样你就拥有一个代码变化(无论有没有用)的记录,让你可以随时查阅。当你在进行逐行改变时,不要依赖你的记忆。
输入命令<code>pip install line_profiler</code>来安装line_profiler。
用修饰器(@profile)标记选中的函数。用kernprof.py脚本运行你的代码,被选函数每一行花费的cpu时间以及其他信息就会被记录下来。
运行时参数-l代表逐行分析而不是逐函数分析,-v用于显示输出。没有-v,你会得到一个.lprof的输出文件,回头你可以用line_profiler模块对其进行分析。例2-6中,我们会完整运行一遍我们的cpu密集型函数。
例2-6 运行kernprof逐行分析被修饰函数的cpu开销
引入kernprof.py导致了额外的运行时间。本例的calculate_z_serial_purepython花费了100秒,远高于使用print语句的13秒和cprofile的19秒。获得的好处则是我们现在得到了一个函数内部每一行花费时间的分析结果。
%time列最有用——我们可以看到36%的时间花在了while测试上。不过我们不知道是第一条语句(abs(z) < 2)还是第二条语句(n < maxiter)更花时间。循环内,我们可以看到更新z也颇花时间。甚至n += 1都很贵!每次循环时,python的动态查询机制都在工作,即使每次循环中我们使用的变量都是同样的类型——在这一点上,编译和类型指定(第7章)可以给我们带来巨大的好处。创建output列表以及第20行上的更新相对整个while循环来说相当便宜。
对while语句更进一步的分析明显就是将两个判断拆开。python社区中有一些讨论关于是否需要重写.pyc文件中对于一行语句中多个部分的具体信息,但目前还没有一个工具提供比line_profiler更细粒度的分析。
在例2-7中,我们将while语句分拆成多个语句。这一额外的复杂度会增加函数的运行时间,因为我们有了更多行代码需要执行,但它可能可以帮助我们了解这部分代码的开销。
例2-7 将组合式while语句拆成单个语句来记录每一部分的开销
这个版本花了184秒执行,而之前的仅100秒。其他因素确实让分析变得更复杂。本例中每一条额外语句都执行了34219980次,拖慢了代码。如果不是通过kernprof.py调查了每行的影响,我们可能会在缺乏证据的情况下得出是其他原因导致了变慢的结论。
此时有必要回到之前的timeit技术来测试每个单独表达式的开销:
从这一简单分析上来看,对n的逻辑测试的速度几乎是abs函数调用的两倍。既然python语句的评估次序是从左到右且支持短路,那么我们应该将最便宜的测试放在左边。每301次测试就有1次n < maxiter的值为false,这样python就不必评估and操作符右边的语句了。
在评估前我们永远无法知道abs(z) < 2的值何时为false,而我们之前对复数平面的观察告诉我们300次迭代中大约10%的可能是true。如果我们想要更进一步了解这段代码的时间复杂度,有必要继续进行数值分析。不过在目前的情况下,我们只是想要看看有没有快速提高的机会。
我们可以做一个新的假设声明,“通过交换while语句的次序,我们会获得一个可靠的速度提升。”我们可以用kernprof.py测试这个假设,但是其额外的开销 可能会给我们的结果带来太多噪声。所以我们用一个之前版本的代码,测试比较while abs(z) < 2 and n < maxiter:和while n < maxiter and abs(z) < 2:之间的区别。
结果显示出大约0.4秒的稳定提升。这一结果显然很无足轻重且局限性太强,使用另一个更合适的方法(如换用第7章描述的cython或pypy)来解决问题会带来更高的收益。
我们对自己的结果有信心,是因为:
我们声明的假设易于测试。
我们对代码的改动仅局限于假设的测试(永远不要一次测试两件 事!)。
我们收集了足够的证据支持我们的结论。
为了保持完整性,我们可以在包含了我们优化的两个主要函数上最后运行一次kernprof.py来确认我们代码整体的复杂度。例2-8交换了第17行while测试的语句,我们可以看到原来占用的36.1%的执行时间现在仅占用35.9%(这一结果在多次运行中稳定存在)。
例2-8 交换while语句的次序提升测试的速度
和预期的一样,我们可以看例2-9的输出中,calculate_z_serial_purepython占用了其父函数97%的时间。创建列表的步骤相对来说无足轻重。
例2-9 逐行测试设置阶段的开销