Python性能分析与优化

一个优秀的程序员,在保证业务正常的条件下都会追求自己的程序更快、更省。更快:运行时间短;更省:相对节省计算机资源(比如:CPU、Memory)。一般都是以这两种衡量方式来度量自己的程序及进一步优化自己程序的空间。更专业的性能分析软件一般有两类方法论:event-based profiling和statistical profiling

Event-based Profiling

并不是所有编程语言都支持这类性能分析,支持这类分析的语言主要有:

  • Java:JVMTI(JVM Tools Interface,JVM工具接口)为性能提供了钩子,可以跟踪函数调用和事件执行等等。
  • .NET :也提供了事件监听器,主要得益于.net 运行时。
  • Python:可以利用sys.setprofile函数来跟踪函数python_(call,return,exception)或者c_(call,return,exception).

基于事件的性能分析(event-based profiler or tracing profiler)是通过手机程序执行过程中的具体事件进行工作的,这些性能分析会产生大量的数据,基本而言,你监听的事件越多产生的数据量句越多。这导致它们不太实用,在开始对程序进行性能往往不是首选,当其他性能分析不够用或者不精准它们可以作为最后的选择。

我们看一下一个python的例子:

Statistical Profiling

以固定的事件间隔对程序计数器进行抽样统计,可以查看每个函数的消耗时间。由于它对程序计数器进行抽样,所以数据结果是对真实值的统计近似。以便于查看分析程序的性能细节,查出性能瓶颈。

  • 分析的数据更少:只对程序执行过程进行抽样,而不用保留每一条数据。
  • 对性能造成的影响小:由于使用抽样(操作系统中断),目标程序的性能遭受干扰更小;虽然不能做到100%无干扰,但是要比基于事件的分析造成的干扰更小。

Python性能分析

现在我们来谈谈Python的性能分析,Python性能分析有很多工具和模块。比如:time粗粒度分析、cProfile,line_Profile等等。

time分析器

time.time()简单的衡量运行时间,我们看一下Demo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env python
# -*- coding:utf-8 -*-

import time

class Timer(object):
    def __init__(self):
        pass

    def __enter__(self):
        self.start = time.time()
        return self

    def __exit__(self, *args):
        self.end = time.time()
        self.secs = self.end - self.start
        print 'costed time: %f ms' % (self.secs * 1000)

def test():
    k = []
    for i in range(1000000):
        k.append(i**2+1000)
    return k
        
if __name__ == "__main__":
    with Timer() as t:
        test()
#运行结果:
costed time: 776.966095 ms
        

将要测量时间的代码用Python关键字with和Timer上下文管理器包起来。它会在你的代码运行的时候开始计时,并且在执行结束的完成计时。

性能分析器cProfile

cProfile是Python默认的性能分析器,它是一种确定性的性能分析器,提供了一组API来帮助开发者手机Python程序运行的信息。统计每个函数消耗的CPU时间,它只测量CPU时间,并不关心内存消耗和其他内存相关信息统计。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import cProfile
import pstats

def get_result():
    sum = 0
    for i in range(10000):
        sum += add_result(i,i+1)
    return sum

def add_result(a,b):
    return a+b
  
if __name__ == "__main__":
    cProfile.run("get_result()")
#结果如下:
         10004 function calls in 0.018 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.018    0.018 <string>:1(<module>)
    10000    0.006    0.000    0.006    0.000 untitled-1.py:10(add_result)
        1    0.011    0.011    0.018    0.018 untitled-1.py:4(get_result)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        1    0.000    0.000    0.000    0.000 {range}

1004代表一共函数调用,花费了0.018秒,第一列ncalls代表了函数总共调用次数,第二列tottime总共运行时间,它不包括内部其它函数运行的时间,第三列cumtime函数总计运行时间,含调用的函数运行时间,tottime和cumtime是不一样的。可以这样理解:get_result花费时间等于tottime(get_result)+cumtime(add_result)最后一列比显示调用函数名。这是最常用的用法,cprofile也提供很多API,比如:查看函数调用了那些函数等等。

性能分析器line_Profile

这个是耕细粒度的性能分析,这个分析一行一行函数分析,不过得需要装饰器注册@profile然后还得需要kernprof脚本将会在执行的时候将它自动地注入到你的脚步的运行时。下面我们来看一下例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import cProfile
import pstats

@profile
def get_result():
    sum = 0
    for i in range(10000):
        sum += add_result(i,i+1)
    return sum

def add_result(a,b):
    return a+b
  
if __name__ == "__main__":
    get_result()