Python性能分析优化及测试

  • 写在前面
  • CPU分析工具
  • 简单的time方法以及包装成的修饰器
  • timeit
  • UNIX的time命令进行简单的计时
  • cProfile:分析每个函数运行花费的时间
  • line_profiler:逐行分析
  • 内存分析工具
  • memory_profiler: 内存用量诊断工具
  • heapy:内存堆调查工具
  • 用dis模块检查CPython字节码


写在前面

分析工作的一个好的方式是在分析之前对代码的各部分的运行速度和内存消耗情况做一个假设或者预判,然后结合分析工具的分析结果验证或者纠正假设,不断提高对代码的分析能力。

CPU分析工具

简单的time方法以及包装成的修饰器

Python性能分析最简单的方法是通过内置time模块的测量,然后打印出来,但是这种方法只适用于简短的调查,用的过多会比较杂乱。如下:

t1= time.time()
result = fn(*args, **kwargs)
t2 = time.time()
print('@timefn:' + fn.func_time + 'took' + str(t2-t1) + 'second')

比较干净的方法是使用修饰器,在被需要调查的函数定义上面增加一行修饰语句。使用这个修饰器的开销很小,但是成千上万次会变得引人注意。

from functools import wraps

def timefn(fn):
    @wraps(fn)
    def measure_time(*args, **kwargs): # *arg数量可变的位置参数,**kwargs数量可变的键值对参数
        t1= time.time()
        result = fn(*args, **kwargs)
        t2 = time.time()
        print('@timefn:' + fn.func_time + 'took' + str(t2-t1) + 'second')
        return result
    return measure_time

这里定义了一个新函数timefn,它一个函数fn为参数,使用@wraps(fn)将函数名和docstring暴露给fn的调用者。

timeit

timeit一般用来测量简单语句的执行速度,作为一种辅助分析手段。timeit模块暂时禁用了垃圾收集器,如果测试代码中调用了垃圾收集器会影响到执行速度。方法:

python -m timeit -n 5 -r 5 -s "import modulename" "function(arg1 = **, arg2 = **)"

‘ -s’用来导入被测函数的模块;‘-n’指定循环次数,取平均数;‘-r’为重复次数,选平均数中的最好结果。这种方法与IPython中‘%time’方法类似。

UNIX的time命令进行简单的计时

使用UNIX操作系统的标准系统功能记录程序执行所耗费的各方面时间,且不在意代码的内部结构。使用方法:

/usr/bin/time -p python name.py

输出结果分三个部分:real记录了整体的耗时, user记录了CPU花在任务上的时间,但不包括内核函数耗费的时间, sys记录了内核函数耗费的时间。user和sys相加的和就得到CPU花费的总时间,而这个时间和real的差可能就是花费在等待IO上,也可能是系统正忙着运行其他任务因此影响了测量。另外比较有用的方法是用‘–verbose’来获得更多输出信息。

/usr/bin/time -verbose python name.py

打印的信息中很有用的一项是‘Major(requiring I/O) page faults’,因为它指示了内存缺页引发的性能惩罚。

cProfile:分析每个函数运行花费的时间

cProfile是一个标准库内建工具,通过钩入CPython虚拟机来测量每一个函数所花费的时间,这一技术会引入额外的巨大开销。执行如下命令,cProfile会将输出直接打印到屏幕。

python -m cProfile -s cumulative name.py

‘-s’的意义是设置cProfile的分析结果对每个函数累计花费的时间进行排序。然后还可以生成统计文件,然后通过Python进行分析。

python - m cProfile - o profile.stats name.py
import pstats
p = pstats.Stats("profile.stats")
p.sort_stats("cumulative")
p.print_stats()

另外,我们还可以通过‘print_callers()’和‘print_callees()’方法打印出调用者和被调用者的信息。

p.print_callers()
p.print_callees()

line_profiler:逐行分析

line_profile可以对函数进行组行分析,应该先用cProfile找到最需要分析的函数,然后用line_profile对函数进行分析,通过pip安装。

pip install line_profiler

使用方法:先通过修饰器‘@profile’标记需要分析的函数,然后运行如下命令。

kernprof -l -v name.py

'-l’代表组行分析而不是逐个函数分析, ‘-v’代表显示输出,分析过程会产生一个后缀为’.lprof’的文件。得到结果观察哪条语句执行占用的时间最多,然后进行进一步分析,尝试更换执行顺序、调整算法、编译和类型指定等方法来优化,并用后续的测试进行验证。可以运用‘%timeit’来对比测量一些语句的效率;Python语句的评估次序是从左至右且支持短路,可以将执行开销大的语句放在右边。

内存分析工具

memory_profiler: 内存用量诊断工具

memory_profile包最好有psutil包的支持,二者皆可以通过pip安装。

pip install psutil memory_profiler

memory_profiler的对性能的影响很大(10-100倍的降速,比line_profiler的效率还低),所以最好将测试局限在一个较小的问题上,或者在代码成长的早期进行分析。使用的方法分两步,第一步是在源代码中用‘@profile’对需要分析的函数进行标记,第二步是调用memory_profiler模块进行分析。

python -m memory_profiler code_need_analyse.py

IPython中可以使用‘%memit’魔法函数来测量某些语句的RAM使用情况,工作方式类似于‘%timeit’
特别说明:
在处理内存分配时,情况并不像CPU占有率那么直截了当,通常一个进程将内存超额分配给本地内存池并在空闲时使用会更有效率,因为一次内存分配的操作非常昂贵;
另外,垃圾收集不会立即执行,所以对象被销毁后依然会存在垃圾收集池中一段时间;
这些技术的后或是很难真正了解一个Python程序内部的内存使用和释放情况,因为当从进程外部观察时,某一段代码可能不会分配固定数量的内存,观察多行代码的内存占有趋势可能比只观察一行代码更有参考价值。

heapy:内存堆调查工具

借助heapy可以深入解释器内部查看内存堆中有多少对象被使用以及它们是否被垃圾收集时,是解决内存泄漏(可能由于某个对象的引用隐藏于一个复杂系统中)的有效工具;还可以通过审查去考察是否如预期的那样生成对象。使用heapy需要通过pip安装guppy包。

pip install guppy

使用方法:

def calc_pure_python(draw_output, desired_width, max_iterations):
    ...
    while xcoord < x2:
        x.append(xcoord)
        xcoord += x_step
    from guppy import hpy; hp = hpy()
    print("heapy after creating y and x lists of floats")
    h = hp.heap()
    print(h) 
    print
    zs = []
    cs = []
    for ycoord in y:
        for xcoord in x:
            zs.append(complex(xcoord, ycoord))
            zs.append(complex(c_real, c_imag))
    print("heapy after creating zs and cs using complex numbers")
    h = hp.heap()
    print(h)

用dis模块检查CPython字节码

dis模块是Pyhon内建的包,通过传给它一段代码或者一个模块可以查看到基于栈的CPython虚拟机运行的字节码,增强对Python代码模型的理解,可以了解到为什么某些编码风格会比其他的更快,同时还能帮助使用Cython这样的工具,他跳出了Python的范畴,能够生成C代码。使用方法:

import dis
dis.dis(module or function name)