--感谢dreampuf
Python Profiling
Author: dreampuf(http://huangx.in)
Slide: http://www.slideshare.net/dreampuf/python-profiling
URL: http://huangx.in/2012/9/python_profiling.htmlIntroduction
性能分析(Performance analysis 或 profiling)是收集程序运行时信息为手段研究程序行为的分析方法。接下来的分析基于如下版本的快排。
def qks(ls): if not ls : return ls p, ls = ls[0], ls[1:] return qks([i for i in ls if i < p]) + [p] + qks([i for i in ls if i >= p]) if __name__ == "__main__": import random ls = [random.randint(0, 100) for i in xrange(10)]
time utility, time module, timeit, sys.getsizeof
本栏介绍一些系统Profile的工具。
time
time
是系统自带的测量工具,能够统计出程序在系统中的耗时。dreampuf@HX:~/hub/fortest$ time python test_timeit.py 0.143877983093 real 0m0.184s #实际总耗时 user 0m0.166s #应用耗时 sys 0m0.015s #应用期间执行系统指令耗时
需要注意的是,
real
一般而言是指程序总执行时间,但是真正的CPU占用可能只有一小部分,比如在执行sleep 5
指令之后,并没有占用CPU 5s,而是将控制权让给了其他并行的程序。dreampuf@HX:~/hub/fortest$ time sleep 5 real 0m5.036s user 0m0.001s sys 0m0.003s
time
的特点:
- 系统级别耗时(一般而言 real > user + sys)
- 应用耗时和系统调用耗时比率
time module
提起Profile,最直接的测量往往如下:
dreampuf@HX:~/hub/fortest$ python test_timeit.py 2.31266021729e-05 import time def time_module(): start = time.time() qks(ls) return time.time() - start COUNT = 100000 print sum([time_module() for i in xrange(COUNT)])/COUNT
这当然只是最原始的方式,不过在不变更原有的代码基础之上,能够很快而且灵活的测量出执行时间,所以不失为一种方法。
不过考虑到重用,最好还是封装到自己的常用函数中,Benchmarker算是一个不错的封装,而且提供了比较完善的报表。
time module
特点:
- 平台精度
- 灵活
timeit
除了自己手写
time.time()
测量,Python 还提供了内置的timeit
模块。这个模块相当于封装了time module
的一些测量操作。from timeit import Timer @Timer def caller(): qks(ls) print caller.timeit(10000) #0.14427113533
除此之外,还能够在解释器环境中,快速执行测量。
In [1]: from timeit import timeit In [2]: timeit('s.appendleft(3)', ...: 'import collections; s= collections.deque()', number=1000000) Out[2]: 0.1150519847869873 In [3]: timeit('s.insert(0, 3)', 's=[]', number=1000000) Out[3]: 392.4638919830322
timeit
模块的特点:
- 不同平台使用的time不同(Xinux 使用
time.time()
, Windows 使用time.clock()
)python -m timeit -s "import collections; s = collections.deque()" -n 1000000 "s.appendleft(3)"
sys.getsizeof
除了耗时占用,有的时候我们需要对内存的占用有一个估算,那么内置的
sys.getsizeof
就能很好的帮助我们。import sys def sys_getsizeof(): a = range(1000000) b = xrange(1000000) c = [i for i in xrange(1000000)] print sys.getsizeof(a) #8000072 print sys.getsizeof(b) #40 print sys.getsizeof(c) #8697472
上面三条语句分别测量的是:
- 一个列表(Python 2.x)
- 一个迭代器
- 一个通过枚举器输出的列表推导
第一种方式和第三种方式的不同在于,
range
是系统内置方法,他初始化就确定了列表大小,并不是动态增长的,所以没有动态增长带来的“盈余空间”。可以通过如下方式证明:In [1]: import sys In [2]: a = range(1000000) In [3]: b = [i for i in xrange(1000000)] In [4]: sys.getsizeof(a) Out[4]: 8000072 In [5]: sys.getsizeof(b) Out[5]: 8697472 In [6]: a.append(1) In [7]: b.append(1) In [8]: sys.getsizeof(a) # 由于不是通过动态增长,没有留下额外的空间,增加一个元素需要新开辟一段空间 Out[8]: 9000128 In [9]: sys.getsizeof(b) # 而b则直接使用之前的盈余空间 Out[9]: 8697472
当然,你也可以通过源码找到结果(Python2.7_SourceCode/Python/bltinmodule.c r1875)。
下面是对于sys.getsizeof
获取的容器大小的一个展示:class Entity(object): def __init__(self, name, age): self.name = name self.age = age es = [Entity("dreampuf", 100) for i in xrange(1000000)] print sys.getsizeof(es) #8697472 print sum([sys.getsizeof(i) for i in es]) #64000000
sys.getsizeof
的特点:
- 和实际有所出入(Python对象缓存池)
- 容器还是对象
Module: profile, cProfile, hotshot, pystats
系统自带的模块或多或少的能够帮助我们解决一些问题,但实际上在我们解决现实问题时有或这或那的不足。
我们就需要其他模块来帮助我们定位问题。
profile
,cProfile
以及hotshot
,他们拥有近似的接口,作用也有一样,就是采集运行时的调用信息,将信息打印或者保存为stats格式,供后期Profile。
比如使用cProfile
:from cProfile import Profile as profile def profile_module(): p = profile() p.enable() qks(ls*100) p.disable() print p.getstats() dreampuf@HX:~/hub/fortest$ python test_timeit.py [_lsprof.profiler_entry(code="
", callcount=1, reccallcount=0, totaltime=0.0, inlinetime=0.0, calls=None), _lsprof.profiler_entry(code= , callcount=2001, reccallcount=2000, totaltime=0.01724, inlinetime=0.01724, calls=[_lsprof.profiler_subentry(code=
, callcount=2000, reccallcount=1998, totaltime=0.017048, inlinetime=0.017048)])]
可以看到,直接输出的结果,几乎无法阅读。实际上,还需要借助其他的模块来格式化这些信息:
from cProfile import Profile as profile from pstats import Stats def profile_module(): p = profile() p.snapshot_stats() p.enable() dirs(".") p.disable() p.print_stats(2) # 按照调用累加总耗时累加排序,即将最耗时的函数最优先 p.dump_stats("call.log")
这样,我们就能够得到程序的调用结果:
dreampuf@HX:~/hub/fortest$ python test_timeit.py 135259 function calls in 0.071 seconds Ordered by: cumulative time ncalls tottime percall cumtime percall filename:lineno(function) 1 0.002 0.002 0.071 0.071 test_timeit.py:101(dirs) 22 0.005 0.000 0.068 0.003 test_timeit.py:74(main) 10519 0.008 0.000 0.047 0.000 ast.py:203(walk) 10498 0.004 0.000 0.035 0.000 {method 'extend' of 'collections.deque' objects} 20975 0.015 0.000 0.031 0.000 ast.py:173(iter_child_nodes) 21 0.000 0.000 0.014 0.001 ast.py:32(parse) 21 0.014 0.001 0.014 0.001 {compile} 26234 0.009 0.000 0.011 0.000 ast.py:161(iter_fields) 39368 0.008 0.000 0.008 0.000 {isinstance} 1 0.002 0.002 0.004 0.004 collections.py:1(
) 15736 0.002 0.000 0.002 0.000 {getattr} 1 0.001 0.001 0.001 0.001 heapq.py:31( ) 10498 0.001 0.000 0.001 0.000 {method 'popleft' of 'collections.deque' objects}
profile
vs.cProfile
vs.hotshot
profile
模块的特点:
- 行级别调用信息(时间,次数)
- 可以保存执行结果
- 友好的输出信息
Multithread Profiling
在Python中,多线程的Profiling,可以通过统一的
threading.setprofile
接口绑定测量函数。
具体内容可以参见下面的连接。
- threading.setprofile(func)
- sys.setprofile(profilefunc)
- Yet Another Python Profiler, but this time Thread-Aware
Thrid-party modules
除了Python自带的模块,其他第三方模块提供了更好的易用功能。
为了更好的展示这些功能,我们使用如下的基准代码:import re import os import ast is_chinese = re.compile("[^\x00-\x7f]") mnode = re.compile(">([^<]+?)<") breakline = re.compile("[\r\n]+") title = re.compile("
([^<]+) ") def main(filename): with open(filename) as f: data = f.read() if filename[-3:] == ".py": try: node = ast.parse(data, filename=filename) except: return for i in ast.walk(node): if not isinstance(i, ast.Str): continue if is_chinese.search(i.s): t = i.s if isinstance(i.s, str) else i.s.encode("u8") #print t.strip()#, i.lineno, filename elif filename[-3:] == "tml": t = title.search(data) if t: pass #print #print "=========", t.group(1), "==========" for i in mnode.finditer(data): m = i.group(1).strip() if m and is_chinese.search(m): pass #print "".join(map(lambda x: x.strip(), breakline.split(m)))#, i.start() def dirs(dirpath): for current, ds, fs in os.walk(dirpath): for f in fs: if f[-3:] not in ('tml', '.py'): continue main(os.path.join(current, f)) break上面的代码主要实现了对于一个目录下的
.py
文件以及.html
文件中字符常量的遍历(已经将打印字符注释了)。
这主要是解决一个项目中的文案重构工作。Line Profiler
LineProfiler
如其名,能够对每一行的耗时做profile,下面是他的示例代码:from line_profiler import LineProfiler def line_profiler(): p = LineProfiler() dirs_lineprofiler = p(dirs) dirs_lineprofiler(".") p.print_stats()
这里需要注意的是,调用的函数需要是经过
LineProfiler
包装后的函数。dreampuf@HX:~/hub/fortest$ python test_timeit.py Timer unit: 1e-06 s File: test_timeit.py Function: dirs at line 101 Total time: 0.108716 s Line # Hits Time Per Hit % Time Line Contents ============================================================== 101 def dirs(dirpath): 102 1 1129 1129.0 1.0 for current, ds, fs in os.walk(dirpath): 103 49 56 1.1 0.1 for f in fs: 104 48 50 1.0 0.0 if f[-3:] not in ('tml', '.py'): 105 26 16 0.6 0.0 continue 106 22 107461 4884.6 98.8 main(os.path.join(current, f)) 107 1 4 4.0 0.0 break
可以看到,上面的Profile结果没有包含子函数的调用,不过这点可以通过
line_profiler.LineProfiler()
函数解决,最简单的方法则是以装饰器的形式,标记在想要Profile的函数上。
LineProfiler
的特点:
- 直观
- 准确(Python C API 实现的trace)
Memory Profiler
Memory Profiler
是基于Line Profiler
的一个对于内存占用的代码行Profile类库。使用结果如下:dreampuf@HX:~/hub/fortest$ python test_timeit.py Filename: test_timeit.py Line # Mem usage Increment Line Contents ================================================ 101 8.68 MB 0.00 MB def dirs(dirpath): 102 8.68 MB 0.01 MB for current, ds, fs in os.walk(dirpath): 103 11.59 MB 2.91 MB for f in fs: 104 11.59 MB 0.00 MB if f[-3:] not in ('tml', '.py'): 105 11.59 MB 0.00 MB continue 106 11.59 MB 0.00 MB main(os.path.join(current, f)) 107 11.59 MB 0.00 MB break
Objgraph
在
sys.getsizeof
的例子中,我们看到了一些不是很直观结论。如果能够通过图示的方法展示内存中对象的引用,那么我们对程序的Profile理解,可能会更容易,Objgraph
就是这样的工具。
仍然是sys.getsizeof
中的例子:import objgraph def objgraph_profile(): a = range(1000000) b = xrange(1000000) c = (i for i in xrange(1000000)) class Entity(object): def __init__(self, name, age): self.name = name self.age = age es = [Entity("dreampuf", 100) for i in xrange(1000)] objgraph.show_refs([es], filename='simple-es.png')
我们来分别看看这些容器对象在内存中的形态:
a = range(1000000) b = xrange(1000000) # 直接返回的是一个xrange对象,并没有展开 c = (i for i in xrange(1000000)) # 注意,这里不是列表推倒了,而是一个[生成器](http://wiki.python.org/moin/Generators) es = [Entity("dreampuf", 100) for i in xrange(1000)] # 实体对象中,对于基本类型,Python并不会创建多份,而是不断的复用。
objgraph
的特点:
- 图形结果,直观
- 可以指定遍历层级(第三方模块,Python Module,CPython)
RunSnakeRun
RunSnakeRun
是一个基于wxPython
构建的图形界面stats
分析工具。他能够通过profile
模块得到的stats
结果(以文件的形式),以TreeMap的形式展现出来程序的运行状态。你可以直观的了解到程序执行过程,CPU耗时,调用函数相关的实现,函数调用关系(当然,这些都是stats
文件所包含的,只不过RunSnakeRun
通过界面更直观的展示了出来)。Conclusion (From Python Essential Reference,4th)
- 理解你的程序
- 不要过度抽象
- 使用合适的Entity
- 使用
__slots__
- 避免重复引用
- 不常见情况使用异常
- 避免对常见情况使用异常
- 推崇函数式编程和迭
- 使用装饰器和元类
Guido van Rossum's talk
Source
- Avoid overengineering datastructures. Tuples are better than objects (try namedtuple too though). Prefer simple fields over getter/setter functions.
- Built-in datatypes are your friends. Use more numbers, strings, tuples, lists, sets, dicts. Also check out the collections library, esp. deque.
- Be suspicious of function/method calls; creating a stack frame is expensive.
- Don't write Java (or C++, or Javascript, ...) in Python.
- Are you sure it's too slow? Profile before optimizing!
The universal speed-up is rewriting small bits of code in C. Do this only when all else fails.
(Michael Foord) Understand the performance characteristics of basic operations on the builtin types. For example checking for membership in a list is O(N) but for a set or dictionary it is O(1). Adding lists and tuples creates new objects, inserting to the left of a list is O(N) whilst append is O(1) (use a deque instead). And so on.
Reference
- Benchmarker
- Benchmarker - a good friend for performance
- Python Speed
- Python Speed - Performance Tips
- 究竟是什么使Linux滴答地响
- The Python Profilers
- Profiling threads
- High Performance Python tutorial
- RunSnakeRun
- 用profile协助程序性能优化
- profile,cProfile, and stats - Performance analysis of Python programs
- Line Profiler
- Memory Profiler
- Objgraph
- A guide to analyzing Python performance
- 性能分析
- Introduction to Python Profiling
邮件来自: `CPyUG`华蟒用户组(中文Python技术邮件列表)
规则: http://code.google.com/p/cpyug/wiki/PythonCn
发言: [email protected]
退订: [email protected] (向此发空信即退!)
详情: http://code.google.com/p/cpyug/wiki/CpyUg
严正: 理解列表! 智慧提问! http://wiki.woodpecker.org.cn/moin/AskForHelp