返回介绍

10.2 性能分析

发布于 2024-01-23 21:41:46 字数 5087 浏览 0 评论 0 收藏 0

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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文