内建优化器

启用内建的优化器就像是用一门大炮。它非常强大,但是有点不太好用,使用和解释起来比较复杂。

你可以了解更多关于profile模块的东西,但是它的基础是非常简单的:你能够启用和禁用优化器,而且它能打印所有的函数调用和执行时间。它能给你编译和打印出输出。一个简单的装饰器如下:

  1. import cProfile 
  2.   
  3. def do_cprofile(func): 
  4.     def profiled_func(*args, **kwargs): 
  5.         profile = cProfile.Profile() 
  6.         try
  7.             profile.enable() 
  8.             result = func(*args, **kwargs) 
  9.             profile.disable() 
  10.             return result 
  11.         finally
  12.             profile.print_stats() 
  13.     return profiled_func 
  14.   
  15. def get_number(): 
  16.     for x in xrange(5000000): 
  17.         yield x 
  18.   
  19. @do_cprofile 
  20. def expensive_function(): 
  21.     for x in get_number(): 
  22.         i = x ^ x ^ x 
  23.     return 'some result!' 
  24.   
  25. # perform profiling 
  26. result = expensive_function() 

在上面代码的情况下,你应该看到有些东西在终端打印出来,打印的内容如下:

  1. 5000003 function calls in 1.626 seconds 
  2.   
  3.    Ordered by: standard name 
  4.   
  5.    ncalls  tottime  percall  cumtime  percall filename:lineno(function) 
  6.   5000001    0.571    0.000    0.571    0.000 timers.py:92(get_number) 
  7.         1    1.055    1.055    1.626    1.626 timers.py:96(expensive_function) 
  8.         1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects} 

你可以看到,它给出了不同函数的调用次数,但它遗漏了一些关键的信息:是哪个函数让运行这么慢?

可是,这对于基础优化来说是个好的开始。有时候甚至能用更少的精力找到解决方案。我经常用它来在深入挖掘究竟是哪个函数慢或者调用次数过多之前来调试程序。

  • 内建优点:没有额外的依赖并且非常快。对于快速的高等级检查非常有用。
  • 内建缺点:信息相对有限,需要进一步的调试;报告有点不太直接,尤其是对于复杂的代码。

Line Profiler

如果内建的优化器是一门大炮,那么line profiler可以看作是一门离子加农炮。它非常的重量级和强大。

在这个例子里,我们会用非常棒的line_profiler库。为了容易使用,我们会再次用装饰器包装一下,这种简单的方法也可以防止把它放在生产代码里。

  1. try
  2.     from line_profiler import LineProfiler 
  3.   
  4.     def do_profile(follow=[]): 
  5.         def inner(func): 
  6.             def profiled_func(*args, **kwargs): 
  7.                 try
  8.                     profiler = LineProfiler() 
  9.                     profiler.add_function(func) 
  10.                     for f in follow: 
  11.                         profiler.add_function(f) 
  12.                     profiler.enable_by_count() 
  13.                     return func(*args, **kwargs) 
  14.                 finally
  15.                     profiler.print_stats() 
  16.             return profiled_func 
  17.         return inner 
  18.   
  19. except ImportError: 
  20.     def do_profile(follow=[]): 
  21.         "Helpful if you accidentally leave in production!" 
  22.         def inner(func): 
  23.             def nothing(*args, **kwargs): 
  24.                 return func(*args, **kwargs) 
  25.             return nothing 
  26.         return inner 
  27.   
  28. def get_number(): 
  29.     for x in xrange(5000000): 
  30.         yield x 
  31.   
  32. @do_profile(follow=[get_number]) 
  33. def expensive_function(): 
  34.     for x in get_number(): 
  35.         i = x ^ x ^ x 
  36.     return 'some result!' 
  37.   
  38. result = expensive_function() 
  39.  
  40. 如果你运行上面的代码,你就可以看到一下的报告: 
  41. 1 
  42. 2 
  43. 3 
  44. 4 
  45. 5 
  46. 6 
  47. 7 
  48. 8 
  49. 9 
  50. 10 
  51. 11 
  52. 12 
  53. 13 
  54. 14 
  55. 15 
  56. 16 
  57. 17 
  58. 18 
  59. 19 
  60. 20 
  61. 21 
  62. 22 
  63.      
  64. Timer unit: 1e-06 s 
  65.   
  66. File: test.py 
  67. Function: get_number at line 43 
  68. Total time: 4.44195 s 
  69.   
  70. Line #      Hits         Time  Per Hit   % Time  Line Contents 
  71. ============================================================== 
  72.     43                                           def get_number(): 
  73.     44   5000001      2223313      0.4     50.1      for x in xrange(5000000): 
  74.     45   5000000      2218638      0.4     49.9          yield x 
  75.   
  76. File: test.py 
  77. Function: expensive_function at line 47 
  78. Total time: 16.828 s 
  79.   
  80. Line #      Hits         Time  Per Hit   % Time  Line Contents 
  81. ============================================================== 
  82.     47                                           def expensive_function(): 
  83.     48   5000001     14090530      2.8     83.7      for x in get_number(): 
  84.     49   5000000      2737480      0.5     16.3          i = x ^ x ^ x 
  85.     50         1            0      0.0      0.0      return 'some result!' 

你可以看到,有一个非常详细的报告,能让你完全洞悉代码运行的情况。不想内建的cProfiler,它能计算话在语言核心特性的时间,比如循环和导入并且给出在不同的行花费的时间。

这些细节能让我们更容易理解函数内部。如果你在研究某个第三方库,你可以直接将其导入并加上装饰器来分析它。

一些小技巧:只装饰你的测试函数并将问题函数作为接下来的参数。

  •  Line Profiler 优点:有非常直接和详细的报告。能够追踪第三方库里的函数。
  •  Line Profiler 缺点:因为它会让代码比真正运行时慢很多,所以不要用它来做基准测试。这是额外的需求。

总结和最佳实践

你应该用更简单的工具来对测试用例进行根本的检查,并且用更慢但能显示更多细节的line_profiler来深入到函数内部。

九成情况下,你可能会发现在一个函数里循环调用或一个错误的数据结构消耗了90%的时间。一些调整工具是非常适合你的。

如果你仍然觉得这太慢,而是用一些你自己的秘密武器,如比较属性访问技术或调整平衡检查技术。你也可以用如下的方法:

1.忍受缓慢或者缓存它们

2.重新思考整个实现

3.更多使用优化的数据结构

4.写一个C扩展

注意了,优化代码是种罪恶的快感!用合适的方法来为你的Python代码加速很有意思,但是注意不要破坏了本身的逻辑。可读的代码比运行速度更重要。先把它缓存起来再进行优化其实更好。

译文链接:http://blog.jobbole.com/54057/


评论关闭