Python 性能分析大全

开发
为了更好了解python程序,我们需要一套工具,能够记录代码运行时间,生成一个性能分析报告,方便彻底了解代码,从而进行针对性的优化(本篇侧重于代码性能分析,不关注如何优化)。

 

 虽然运行速度慢是 Python 与生俱来的特点,大多数时候我们用 Python 就意味着放弃对性能的追求。但是,就算是用纯 Python 完成同一个任务,老手写出来的代码可能会比菜鸟写的代码块几倍,甚至是几十倍(这里不考虑算法的因素,只考虑语言方面的因素)。很多时候,我们将自己的代码运行缓慢地原因归结于python本来就很慢,从而心安理得地放弃深入探究。

但是,事实真的是这样吗?面对python代码,你有分析下面这些问题吗:

程序运行的速度如何?

程序运行时间的瓶颈在哪里?

能否稍加改进以提高运行速度呢?

为了更好了解python程序,我们需要一套工具,能够记录代码运行时间,生成一个性能分析报告,方便彻底了解代码,从而进行针对性的优化(本篇侧重于代码性能分析,不关注如何优化)。

谁快谁慢

假设有一个字符串,想将里面的空格替换为字符‘-’,用python实现起来很简单,下面是四种方案:

  1. def slowest_replace(): 
  2.  
  3. replace_list = [] 
  4.  
  5. for i, char in enumerate(orignal_str): 
  6.  
  7. c = char if char != " " else "-" 
  8.  
  9. replace_list.append(c) 
  10.  
  11. return "".join(replace_list) 
  12.  
  13. def slow_replace(): 
  14.  
  15. replace_str = "" 
  16.  
  17. for i, char in enumerate(orignal_str): 
  18.  
  19. c = char if char != " " else "-" 
  20.  
  21. replace_str += c 
  22.  
  23. return replace_str 
  24.  
  25. def fast_replace(): 
  26.  
  27. return "-".join(orignal_str.split()) 
  28.  
  29. def fastest_replace(): 
  30.  
  31. return orignal_str.replace(" ""-"

这四种方案的效率如何呢,哪种方案比较慢呢?这是一个问题!

时间断点

最直接的想法是在开始 replace 函数之前记录时间,程序结束后再记录时间,计算时间差即为程序运行时间。python提供了模块 time,其中 time.clock() 在Unix/Linux下返回的是CPU时间(浮点数表示的秒数),Win下返回的是以秒为单位的真实时间(Wall-clock time)。

由于替换函数耗时可能非常短,所以这里考虑分别执行 100000次,然后查看不同函数的效率。我们的性能分析辅助函数如下:

  1. def _time_analyze_(func): 
  2.  
  3. from time import clock 
  4.  
  5. start = clock() 
  6.  
  7. for i in range(exec_times): 
  8.  
  9. func() 
  10.  
  11. finish = clock() 
  12.  
  13. print "{:<20}{:10.6} s".format(func.__name__ + ":", finish - start) 

这样就可以了解上面程序的运行时间情况:

 

 

***种方案耗时是第四种的 45 倍多,大跌眼镜了吧!同样是 python代码,完成一样的功能,耗时可以差这么多。

为了避免每次在程序开始、结束时插入时间断点,然后计算耗时,可以考虑实现一个上下文管理器,具体代码如下:

  1. class Timer(object): 
  2.  
  3. def __init__(self, verbose=False): 
  4.  
  5. self.verbose = verbose 
  6.  
  7. def __enter__(self): 
  8.  
  9. self.start = clock() 
  10.  
  11. return self 
  12.  
  13. def __exit__(self, *args): 
  14.  
  15. self.end = clock() 
  16.  
  17. self.secs = self.end - self.start 
  18.  
  19. self.msecs = self.secs * 1000 # millisecs 
  20.  
  21. if self.verbose: 
  22.  
  23. print 'elapsed time: %f ms' % self.msecs 

使用时只需要将要测量时间的代码段放进 with 语句即可,具体的使用例子放在 gist上。

timeit

上面手工插断点的方法十分原始,用起来不是那么方便,即使用了上下文管理器实现起来还是略显笨重。还好 Python 提供了timeit模块,用来测试代码块的运行时间。它既提供了命令行接口,又能用于代码文件之中。

命令行接口

命令行接口可以像下面这样使用:

  1. $ python -m timeit -n 1000000 '"I like to reading.".replace(" ", "-")' 
  2.  
  3. 1000000 loops, best of 3: 0.253 usec per loop 
  4.  
  5. $ python -m timeit -s 'orignal_str = "I like to reading."' '"-".join(orignal_str.split())' 
  6.  
  7. 1000000 loops, best of 3: 0.53 usec per loop 

具体参数使用可以用命令 python -m timeit -h 查看帮助。使用较多的是下面的选项:

-s S, –setup=S: 用来初始化statement中的变量,只运行一次;

-n N, –number=N: 执行statement的次数,默认会选择一个合适的数字;

-r N, –repeat=N: 重复测试的次数,默认为3;

Python 接口

可以用下面的程序测试四种 replace函数的运行情况(完整的测试程序可以在 gist 上找到):

  1. def _timeit_analyze_(func): 
  2.  
  3. from timeit import Timer 
  4.  
  5. t1 = Timer("%s()" % func.__name__, "from __main__ import %s" % func.__name__) 
  6.  
  7. print "{:<20}{:10.6} s".format(func.__name__ + ":", t1.timeit(exec_times)) 

运行结果如下:

 

 

Python的timeit提供了 timeit.Timer() 类,类构造方法如下:

  1. Timer(stmt='pass', setup='pass', timer=<timer function>) 

其中:

stmt: 要计时的语句或者函数;

setup: 为stmt语句构建环境的导入语句;

timer: 基于平台的时间函数(timer function);

Timer()类有三个方法:

timeit(number=1000000): 返回stmt执行number次的秒数(float);

repeat(repeat=3, number=1000000): repeat为重复整个测试的次数,number为执行stmt的次数,返回以秒记录的每个测试循环的耗时列表;

print_exc(file=None): 打印stmt的跟踪信息。

此外,timeit 还提供了另外三个函数方便使用,参数和 Timer 差不多。

  1. timeit.timeit(stmt='pass', setup='pass', timer=<default timer>, number=1000000) 
  2.  
  3. timeit.repeat(stmt='pass', setup='pass', timer=<default timer>, repeat=3, number=1000000) 
  4.  
  5. timeit.default_timer() 

profile

以上方法适用于比较简单的场合,更复杂的情况下,可以用标准库里面的profile或者cProfile,它可以统计程序里每一个函数的运行时间,并且提供了可视化的报表。大多情况下,建议使用cProfile,它是profile的C实现,适用于运行时间长的程序。不过有的系统可能不支持cProfile,此时只好用profile。

可以用下面程序测试 timeit_profile() 函数运行时间分配情况。

  1. import cProfile 
  2.  
  3. from time_profile import * 
  4.  
  5. cProfile.run("timeit_profile()"

这样的输出可能会很长,很多时候我们感兴趣的可能只有耗时最多的几个函数,这个时候先将cProfile 的输出保存到诊断文件中,然后用 pstats 定制更加有好的输出(完整代码在 gist 上)。

  1. cProfile.run("timeit_profile()""timeit"
  2.  
  3. p = pstats.Stats('timeit'
  4.  
  5. p.sort_stats('time'
  6.  
  7. p.print_stats(6) 

输出结果如下:

 

 

如果觉得 pstas 使用不方便,还可以使用一些图形化工具,比如 gprof2dot 来可视化分析 cProfile 的诊断结果。

vprof

vprof 也是一个不错的可视化工具,可以用来分析 Python 程序运行时间情况。如下图:

 

 

line_profiler

上面的测试最多统计到函数的执行时间,很多时候我们想知道函数里面每一行代码的执行效率,这时候就可以用到 line_profiler 了。

line_profiler 的使用特别简单,在需要监控的函数前面加上 @profile 装饰器。然后用它提供的 kernprof -l -v [source_code.py] 行进行诊断。下面是一个简单的测试程序 line_profile.py:

  1. from time_profile import slow_replace, slowest_replace 
  2.  
  3. for i in xrange(10000): 
  4.  
  5. slow_replace() 
  6.  
  7. slowest_replace() 

运行后结果如下:

 

 

输出每列的含义如下:

Line #: 行号

Hits: 当前行执行的次数.

Time: 当前行执行耗费的时间,单位为 “Timer unit:”

Per Hit: 平均执行一次耗费的时间.

% Time: 当前行执行时间占总时间的比例.

Line Contents: 当前行的代码

line_profiler 执行时间的估计不是特别精确,不过可以用来分析当前函数中哪些行是瓶颈。

责任编辑:赵立京 来源: Just For Fun
相关推荐

2014-07-28 09:52:14

PythonPython性能

2017-07-13 11:08:52

PythonC模块性能分析

2010-05-20 09:20:06

MyEclipse8.

2015-09-14 10:41:51

PHP性能分析微观分析

2018-06-14 14:07:57

Pythonweb框架

2015-08-18 11:44:02

PHP性能分析宏观分析

2023-12-13 09:08:26

CPU性能分析Linux

2023-06-09 12:59:52

Python性能分析

2020-10-27 11:35:31

PythonRedis数据库

2011-07-20 14:29:33

HBase

2011-03-22 13:00:47

Nagios

2022-04-12 12:35:02

Linux启动性能systemd

2019-10-31 11:50:19

MySQL数据库Windows

2022-09-02 14:56:24

Python界面开发库框架

2015-12-11 10:09:38

2023-09-18 16:14:35

性能测试开发

2022-09-28 14:13:03

Linux工具

2019-02-21 15:01:45

PythonWeb部署

2013-03-20 17:18:07

Linux系统性能调优

2017-06-15 12:42:07

Linux常用性能分析命令
点赞
收藏

51CTO技术栈公众号