10.2 性能分析
Python提供了一些工具对程序进行性能分析。标准的工具之一就是cProfile,而且它很容易使用,如示例10.1所示。
示例 10.1 使用cProfile模块
$ python -m cProfile myscript.py 343 function calls (342 primitive calls) in 0.000 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 0.000 0.000 :0(_getframe) 1 0.000 0.000 0.000 0.000 :0(len) 104 0.000 0.000 0.000 0.000 :0(setattr) 1 0.000 0.000 0.000 0.000 :0(setprofile) 1 0.000 0.000 0.000 0.000 :0(startswith) 2/1 0.000 0.000 0.000 0.000 <string>:1(<module>) 1 0.000 0.000 0.000 0.000 StringIO.py:30(<module>) 1 0.000 0.000 0.000 0.000 StringIO.py:42(StringIO)
运行结果的列表显示了每个函数的调用次数,以及执行所花费的时间。可以使用-s选项按其他字段进行排序,例如,-s time可以按内部时间进行排序。
如果你像我一样使用C语言很多年,那你很可能已经知道Valgrind(http://valgrind.org/)这个优秀的工具,除了其他功能之外,它能够提供对C程序的性能分析数据。生成的数据能够被另一个不错的工具KCacheGrind(http://kcachegrind.sourceforge.net/html/Home.html)可视化地展示。
cProfile生成的性能分析数据很容易转换成一个可以被KCacheGrind读取的调用树。cProfile模块有一个-o选项允许保存性能分析数据,并且pyprof2calltree([https:// pypi.python.org/pypi/pyprof2calltree](https:// pypi.python.org/pypi/pyprof2calltree))可以进行格式转换,如示例10.2所示。
示例 10.2 用KCacheGrind可视化 Python 性能分析数据
$ python -m cProfile -o myscript.cprof myscript.py $ pyprof2calltree -k -i myscript.cprof
这可以提供很多有用的信息,让你可以判断程序的哪个部分耗费了太多的资源,如图10-1所示。
图10-1 KCacheGrind示例
虽然从宏观角度看这么用没问题,它有时也可以对代码的某些部分提供一些微观角度的分析。但这样的上下文中,我发现用dis模块可以看到一些隐藏的东西。dis模块是Python字节码的反编译器,用起来也很简单。
>>> def x(): ... return 42 ... >>> import dis >>> dis.dis(x) 2 0 LOAD_CONST 1 (42) 3 RETURN_VALUE
dis.dis函数会反编译作为参数传入的函数,并打印出这个函数运行的字节码指令的清单。为了能适当地优化代码,这对于理解程序的每行代码非常有用。
下面的代码定义了两个函数,功能相同,都是拼接三个字母。
abc = ('a', 'b', 'c') def concat_a_1(): for letter in abc: abc[0] + letter def concat_a_2(): a = abc[0] for letter in abc: a + letter
两者看上去作用一样,但如果反汇编它们的话,可以看到生成的字节码有点儿不同。
>>> dis.dis(concat_a_1) 2 0 SETUP_LOOP 26 (to 29) 3 LOAD_GLOBAL 0 (abc) 6 GET_ITER >> 7 FOR_ITER 18 (to 28) 10 STORE_FAST 0 (letter) 3 13 LOAD_GLOBAL 0 (abc) 16 LOAD_CONST 1 (0) 19 BINARY_SUBSCR 20 LOAD_FAST 0 (letter) 23 BINARY_ADD 24 POP_TOP 25 JUMP_ABSOLUTE 7 >> 28 POP_BLOCK >> 29 LOAD_CONST 0 (None) 32 RETURN_VALUE >>> dis.dis(concat_a_2) 2 0 LOAD_GLOBAL 0 (abc) 3 LOAD_CONST 1 (0) 6 BINARY_SUBSCR 7 STORE_FAST 0 (a) 3 10 SETUP_LOOP 22 (to 35) 13 LOAD_GLOBAL 0 (abc) 16 GET_ITER >> 17 FOR_ITER 14 (to 34) 20 STORE_FAST 1 (letter) 4 23 LOAD_FAST 0 (a) 26 LOAD_FAST 1 (letter) 29 BINARY_ADD 30 POP_TOP 31 JUMP_ABSOLUTE 17 >> 34 POP_BLOCK >> 35 LOAD_CONST 0 (None) 38 RETURN_VALUE
如你所见,在函数的第二个版本中运行循环之前我们将abc[0]保存在了一个临时变量中。这使得循环内部执行的字节码稍微短一点,因为不需要每次迭代都去查找abc[0]。通过timeit测量,第二个版本的函数比第一个要快10%,少花了不到一微秒。显然,除非调用这个函数100万次,否则不值得优化,但这就是dis模块所能提供的洞察力。
是否应该依赖将值存储在循环外这样的“技巧”是有争议的,这类优化工作应该最终由编译器完成。但是,由于Python语言是高度动态的,因此编译器很难确保优化不会产生什么副作用。所以,编写代码一定要小心。
另一个我在评审代码时遇到的错误习惯是无理由地定义嵌套函数(分解嵌套函数见示例10.3)。这实际是有开销的,因为函数会无理由地被重复定义。
示例 10.3 分解嵌套函数
>> import dis >>> def x(): ... return 42 ... >>> dis.dis(x) 2 0 LOAD_CONST 1 (42) 3 RETURN_VALUE >>> def x(): ... def y(): ... return 42 ... return y() ... >>> dis.dis(x) 2 0 LOAD_CONST 1 (<code object y at 0x100ce7e30, file "<stdin>", line 2>) 3 MAKE_FUNCTION 0 6 STORE_FAST 0 (y) 4 9 LOAD_FAST 0 (y) 12 CALL_FUNCTION 0 15 RETURN_VALUE
可以看到函数被不必要地复杂化了,调用MAKE_FUNCTION、STORE_FAST、LOAD_FAST和CALL_FUNCTION,而不是直接调用LOAD_CONST,这无端造成了更多的操作码,而函数调用在Python中本身就是低效的。
唯一需要在函数内定义函数的场景是在构建函数闭包的时候,它可以完美地匹配Python的操作码中的一个用例。反汇编一个闭包如示例10.4所示。
示例 10.4 反汇编一个闭包
>>> def x(): ... a = 42 ... def y(): ... return a ... return y() ... >>> dis.dis(x) 2 0 LOAD_CONST 1 (42) 3 STORE_DEREF 0 (a) 3 6 LOAD_CLOSURE 0 (a) 9 BUILD_TUPLE 1 12 LOAD_CONST 2 (<code object y at 0x100d139b0, file "<stdin>", line 3>) 15 MAKE_CLOSURE 0 18 STORE_FAST 0 (y) 5 21 LOAD_FAST 0 (y) 24 CALL_FUNCTION 0 27 RETURN_VALUE
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论