黑马程序员技术交流社区

标题: 神贴,必须转__python程序如何分析时空效率 [打印本页]

作者: 易大帅    时间: 2017-3-19 08:48
标题: 神贴,必须转__python程序如何分析时空效率
有人会问为什么易哥会也学Python,其实我想告诉小童鞋们,你的视野开阔了,这些技能其实也或多或少也应该了解,甚至运用!!!!
必须转__python程序如何分析时空效率(gausszh)                                                        [size=1em]感谢dreampuf
Python Profiling
Author: dreampuf(http://huangx.in)
Slide: http://www.slideshare.net/dreampuf/python-profiling
URL: [size=1em]http://huangx.in/2012/9/python_profiling.html
Introduction
性能分析(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.143877983093real     0m0.184s  #实际总耗时user     0m0.166s  #应用耗时sys     0m0.015s  #应用期间执行系统指令耗时
需要注意的是,real一般而言是指程序总执行时间,但是真正的CPU占用可能只有一小部分,比如在执行sleep 5指令之后,并没有占用CPU 5s,而是将控制权让给了其他并行的程序。
dreampuf@HX:~/hub/fortest$ time sleep 5real     0m5.036suser     0m0.001ssys     0m0.003s
time的特点:
time module
提起Profile,最直接的测量往往如下:
dreampuf@HX:~/hub/fortest$ python test_timeit.py    2.31266021729e-05import timedef time_module():    start = time.time()    qks(ls)    return time.time() - startCOUNT = 100000print sum([time_module() for i in xrange(COUNT)])/COUNT
这当然只是最原始的方式,不过在不变更原有的代码基础之上,能够很快而且灵活的测量出执行时间,所以不失为一种方法。
不过考虑到重用,最好还是封装到自己的常用函数中,Benchmarker算是一个不错的封装,而且提供了比较完善的报表。
time module 特点:
timeit
除了自己手写time.time()测量,Python 还提供了内置的timeit模块。这个模块相当于封装了time module的一些测量操作。
from timeit import Timer@Timerdef caller():    qks(ls)print caller.timeit(10000)   #0.14427113533
除此之外,还能够在解释器环境中,快速执行测量。
In [1]: from timeit import timeitIn [2]: timeit('s.appendleft(3)',   ...:        'import collections; s= collections.deque()', number=1000000)Out[2]: 0.1150519847869873In [3]: timeit('s.insert(0, 3)', 's=[]', number=1000000)Out[3]: 392.4638919830322
timeit模块的特点:
sys.getsizeof
除了耗时占用,有的时候我们需要对内存的占用有一个估算,那么内置的sys.getsizeof就能很好的帮助我们。
import sysdef 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
上面三条语句分别测量的是:
第一种方式和第三种方式的不同在于,range是系统内置方法,他初始化就确定了列表大小,并不是动态增长的,所以没有动态增长带来的“盈余空间”。可以通过如下方式证明:
In [1]: import sysIn [2]: a = range(1000000)In [3]: b = [i for i in xrange(1000000)]In [4]: sys.getsizeof(a)Out[4]: 8000072In [5]: sys.getsizeof(b)Out[5]: 8697472In [6]: a.append(1)In [7]: b.append(1)In [8]: sys.getsizeof(a)  # 由于不是通过动态增长,没有留下额外的空间,增加一个元素需要新开辟一段空间Out[8]: 9000128In [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 = agees = [Entity("dreampuf", 100) for i in xrange(1000000)]print sys.getsizeof(es)     #8697472print sum([sys.getsizeof(i) for i in es])     #64000000
sys.getsizeof的特点:
Module: profile, cProfile, hotshot, pystats
系统自带的模块或多或少的能够帮助我们解决一些问题,但实际上在我们解决现实问题时有或这或那的不足。
我们就需要其他模块来帮助我们定位问题。
profile,cProfile 以及 hotshot,他们拥有近似的接口,作用也有一样,就是采集运行时的调用信息,将信息打印或者保存为stats格式,供后期Profile。
比如使用cProfile:
from cProfile import Profile as profiledef 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="<method 'disable' of '_lsprof.Profiler' objects>", callcount=1, reccallcount=0, totaltime=0.0, inlinetime=0.0, calls=None), _lsprof.profiler_entry(code=<code object qks at 0x10a1694b0, file "test_timeit.py", line 6>, callcount=2001, reccallcount=2000, totaltime=0.01724, inlinetime=0.01724, calls=[_lsprof.profiler_subentry(code=<code object qks at 0x10a1694b0, file "test_timeit.py", line 6>, callcount=2000, reccallcount=1998, totaltime=0.017048, inlinetime=0.017048)])]
可以看到,直接输出的结果,几乎无法阅读。实际上,还需要借助其他的模块来格式化这些信息:
from cProfile import Profile as profilefrom pstats import Statsdef 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(<module>)    15736    0.002    0.000    0.002    0.000 {getattr}        1    0.001    0.001    0.001    0.001 heapq.py:31(<module>)    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接口绑定测量函数。
具体内容可以参见下面的连接。
Thrid-party modules
除了Python自带的模块,其他第三方模块提供了更好的易用功能。
为了更好的展示这些功能,我们使用如下的基准代码:
import reimport osimport astis_chinese = re.compile("[^\x00-\x7f]")mnode = re.compile(">([^<]+?)<")breakline = re.compile("[\r\n]+")title = re.compile("<title>([^<]+)</title>")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 LineProfilerdef 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 sFile: test_timeit.pyFunction: dirs at line 101Total time: 0.108716 sLine #      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的特点:
Memory Profiler
Memory Profiler是基于Line Profiler的一个对于内存占用的代码行Profile类库。使用结果如下:
dreampuf@HX:~/hub/fortest$ python test_timeit.py    Filename: test_timeit.pyLine #    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           breakObjgraph
在sys.getsizeof的例子中,我们看到了一些不是很直观结论。如果能够通过图示的方法展示内存中对象的引用,那么我们对程序的Profile理解,可能会更容易,Objgraph就是这样的工具。
仍然是sys.getsizeof中的例子:
import objgraphdef 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的特点:
RunSnakeRun
RunSnakeRun是一个基于wxPython构建的图形界面stats分析工具。他能够通过profile模块得到的stats结果(以文件的形式),以TreeMap的形式展现出来程序的运行状态。你可以直观的了解到程序执行过程,CPU耗时,调用函数相关的实现,函数调用关系(当然,这些都是stats文件所包含的,只不过RunSnakeRun通过界面更直观的展示了出来)。
Conclusion (From Python Essential Reference,4th)Guido van Rossum's talk
Source
Reference

--





欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2