[python]用profile协助程序性能优化

转自:http://blog.csdn.net/gzlaiyonghao/article/details/1483728

本文最初发表于恋花蝶的博客http://blog.csdn.net/lanphaday,欢迎转载,但请务必保留原文完整,并保留本声明。

[python]profile协助程序性能优化

上帝说:“选择了脚本,就不要考虑性能。”我是很支持这句话的,使用脚本要的就是开发速度、良好的扩展性以及可维护性。可惜到了最后,我们的程序难免会运行得太慢,我们的客户不能忍受,这时候,我们就不得不考虑对代码的性能进行优化了。

程序运行慢的原因有很多,比如存在太多的劣化代码(如在程序中存在大量的“.”操作符),但真正的原因往往是比较是一两段设计并不那么良好的不起眼的程序,比如对一序列元素进行自定义的类型转换等。因为程序性能影响是符合80/20法则的,即20%的代码的运行时间占用了80%的总运行时间(实际上,比例要夸张的多,通常是几十行代码占用了95%以上的运行时间),靠经验就很难找出造成性能瓶颈的代码了。这时候,我们需要一个工具——profile!最近我手上的项目也在一些关键的地方遇到了性能问题,那时已经接近项目完工日期,幸好因为平时的代码模块化程度比较高,所以通过profile分析相关的独立模块,基本上解决了性能问题。通过这件事,让我下决心写一篇关于profile的文章,分享一下profile的使用心得。

初识profile

profile是python的标准库。可以统计程序里每一个函数的运行时间,并且提供了多样化的报表。使用profile来分析一个程序很简单,举例说如果有一个程序如下:


def foo():

sum = 0

for i in range(100):

sum += i

return sum

if __name__ == "__main__":

foo()

现在要用profile分析这个程序,很简单,把if程序块改为如下:


if __name__ == "__main__":

import profile

profile.run("foo()")

我们仅仅是import了profile这个模块,然后以程序的入口函数名为参数调用了profile.run这个函数,程序运行的输出如下:


         5 function calls in 0.143 CPU seconds

Ordered by: standard name

ncalls tottime percall cumtime percall filename:lineno(function)

1    0.000    0.000    0.000    0.000 :0(range)

1    0.143    0.143    0.143    0.143 :0(setprofile)

1    0.000    0.000    0.000    0.000 <string>:1(?)

1    0.000    0.000    0.000    0.000 prof1.py:1(foo)

1    0.000    0.000    0.143    0.143 profile:0(foo())

0    0.000             0.000          profile:0(profiler)

上图显示了prof1.py里函数调用的情况,根据图表我们可以清楚地看到foo()函数占用了100%的运行时间,foo()函数是这个程序里名至实归的热点。

除了用这种方式,profile还可以直接用python解释器调用profile模块来剖分py程序,如在命令行界面输入如下命令:

python -m profile prof1.py

产生的输出跟直接修改脚本调用profile.run()函数有一样的功效。

profile的统计结果分为ncalls, tottime, percall, cumtime, percall, filename:lineno(function)等若干列:

ncalls 函数的被调用次数
tottime 函数总计运行时间,除去函数中调用的函数运行时间
percall 函数运行一次的平均时间,等于tottime/ncalls
cumtime 函数总计运行时间,含调用的函数运行时间
percall 函数运行一次的平均时间,等于cumtime/ncalls
filename:lineno(function) 函数所在的文件名,函数的行号,函数名

通常情况下,profile的输出都直接输出到命令行,而且默认是按照文件名排序输出的。这就给我们造成了障碍,我们有时候希望能够把输出保存到文件,并且能够以各种形式来查看结果。profile简单地支持了一些需求,我们可以在profile.run()函数里再提供一个实参,就是保存输出的文件名;同样的,在命令行参数里,我们也可以加多一个参数,用来保存profile的输出。

用pstats自定义报表

profile解决了我们的一个需求,还有一个需求:以多种形式查看输出,我们可以通过profile的另一个类Stats来解决。在这里我们需要引入一个模块pstats,它定义了一个类Stats,Stats的构造函数接受一个参数——就是profile的输出文件的文件名。Stats提供了对profile输出结果进行排序、输出控制等功能,如我们把前文的程序改为如下:


# …略

if __name__ == "__main__":

import profile

profile.run("foo()", "prof.txt")

import pstats

p = pstats.Stats("prof.txt")

p.sort_stats("time").print_stats()

引入pstats之后,将profile的输出按函数占用的时间排序,输出如下:


Sun Jan 14 00:03:12 2007    prof.txt

5 function calls in 0.002 CPU seconds

Ordered by: internal time

ncalls tottime percall cumtime percall filename:lineno(function)

1    0.002    0.002    0.002    0.002 :0(setprofile)

1    0.000    0.000    0.002    0.002 profile:0(foo())

1    0.000    0.000    0.000    0.000 G:/prof1.py:1(foo)

1    0.000    0.000    0.000    0.000 <string>:1(?)

1    0.000    0.000    0.000    0.000 :0(range)

0    0.000             0.000          profile:0(profiler)

Stats有若干个函数,这些函数组合能给我们输出不同的profile报表,功能非常强大。下面简单地介绍一下这些函数:

strip_dirs() 用以除去文件名前名的路径信息。
add(filename,[…]) 把profile的输出文件加入Stats实例中统计
dump_stats(filename) 把Stats的统计结果保存到文件
sort_stats(key,[…]) 最重要的一个函数,用以排序profile的输出
reverse_order() 把Stats实例里的数据反序重排
print_stats([restriction,…]) 把Stats报表输出到stdout
print_callers([restriction,…])
输出调用了指定的函数的函数的相关信息
print_callees([restriction,…]) 输出指定的函数调用过的函数的相关信息

这里最重要的函数就是sort_stats和print_stats,通过这两个函数我们几乎可以用适当的形式浏览所有的信息了,下面来详细介绍一下。

sort_stats()接受一个或者多个字符串参数,如”time”、”name”等,表明要根据哪一列来排序,这相当有用,例如我们可以通过用time为key来排序得知最消耗时间的函数,也可以通过cumtime来排序,获知总消耗时间最多的函数,这样我们优化的时候就有了针对性,也就事半功倍了。sort_stats可接受的参数如下:

‘ncalls’ 被调用次数
‘cumulative’ 函数运行的总时间
‘file’ 文件名
‘module’ 文件名
‘pcalls’ 简单调用统计(兼容旧版,未统计递归调用)
‘line’ 行号
‘name’ 函数名
‘nfl’ Name/file/line
‘stdname’ 标准函数名
‘time’ 函数内部运行时间(不计调用子函数的时间)

另一个相当重要的函数就是print_stats——用以根据最后一次调用sort_stats之后得到的报表。print_stats有多个可选参数,用以筛选输出的数据;print_stats的参数可以是数字也可以是perl风格的正则表达式,相关的内容通过其它渠道了解,这里就不详述啦,仅举三个例子:

print_stats(“.1”, “foo:”)

这个语句表示将stats里的内容取前面的10%,然后再将包含”foo:”这个字符串的结果输出。

print_stats(“foo:”,”.1”)

这个语句表示将stats里的包含”foo:”字符串的内容的前10%输出。

print_stats(10)

这个语句表示将stats里前10条数据输出。

实际上,profile输出结果的时候相当于这样调用了Stats的函数:

p.strip_dirs().sort_stats(-1).print_stats()

其中sort_stats函数的参数是-1,这也是为了与旧版本兼容而保留的。sort_stats可以接受-1,0,1,2之一,这四个数分别对应”stdname”, “calls”, “time”和”cumulative”;但如果你使用了数字为参数,那么pstats只按照第一个参数进行排序,其它参数将被忽略。

hotshot——更好的profile

因为profile本身的机制(如使用精确到毫秒的计时器等)导致在相当多情况下profile模块的“测不准”问题相当严重。hotshot大部分都是用C实现的,相对于profile模块它的计时函数对性能剖分的影响就小得多,而且支持以行为单位统计运行时间。美中不足的是hotshot不支持多线程的程序,确切来说,是它的计时核心有个关于临界段的bug;更加不幸的是,hotshot已经不再被维护,而且可能在未来的python版本中会从标准库中移除。不过,对于没有使用多线程的程序而言,hotshot目前仍然是非常好的剖分器。

hotshot有一个Profile类,它的构造函数原型如下:

class Profile( logfile[, lineevents[, linetimings]])

logfile参数是保存剖分统计结果的文件名,lineevents表示是否统计每一行源码的运行时间,默认为0,即以函数执行时间为统计粒度,linetimings为是否记录时间信息,默认为1。下面仍然是示例:


# …略

if __name__ == "__main__":

import hotshot

import hotshot.stats

prof = hotshot.Profile("hs_prof.txt", 1)

prof.runcall(foo)

prof.close()

p = hotshot.stats.load("hs_prof.txt")

p.print_stats()

输出:


         1 function calls in 0.003 CPU seconds

Random listing order was used

ncalls tottime percall cumtime percall filename:lineno(function)

1    0.003    0.003    0.003    0.003 i:/prof1.py:1(foo)

0    0.000             0.000          profile:0(profiler)

我们可以看到来自hotshot的干扰信息比profile的少了很多,这也有利于我们分析数据找出热点。不过正如我在前面代码中使用prof = hotshot.Profile("hs_prof.txt", 1)一样,我发现使lineevents=1跟忽略linveevents参数没有什么不同,还请大家赐教。

使用hotshot能够更加灵活地统计程序的运行情况,因为hotshot.Profile提供了下面一系列的函数:

run(cmd) 执行一段脚本,跟profile模块的run()函数一样功能
runcall(func, *args, **keywords) 调用一个函数,并统计相关的运行信息
runctx(cmd, globals, locals) 指定一段脚本的执行环境,执行脚本并统计运行信息

通过这几个函数,我们可以非常方便地建立测试的桩模块,不必再像使用profile那样手工地编写很多驱动模块了。hotshot.Profile还提供其它有用的函数,具体请参考相关的manual。

Python 2.5注意事项

因为hotshot不能用于多线程,而且它的优势仅在速度,所以python 2.5版本声明不再维护hotshot模块,并且可能在以后的版本中移除它。有去必有来,取而代之的就是cProfile,与cPickle等模块类似,cProfile要比profile模块更快些。cProfile的接口跟profile是一样的,只要在使用到profile的地方用cProfile替换就可以在以前的项目中使用它。

pstats在python 2.5版本中也产生了一些微妙的变化,pstats.Stats的构造函数增加了一个默认参数,变为:

class Stats( filename[, stream=sys.stdout[, ...]])

对我们而言,这是没有坏处的,stream参数给了我们把profile统计报表保存到文件的机会,这正是我们需要的。

综上所述,如果你使用的python 2.5版,我建议你使用cProfile。

小巧实用的瑞士军刀——timeit

如果我们某天心血来潮,想要向list里append一个元素需要多少时间或者想知道抛出一个异常要多少时间,那使用profile就好像用牛刀杀鸡了。这时候我们更好的选择是timeit模块。

timeit除了有非常友好的编程接口,也同样提供了友好的命令行接口。首先来看看编程接口。timeit模块包含一个类Timer,它的构造函数是这样的:

class Timer( [stmt=‘pass‘ [, setup=‘pass‘ [, timer=<timer function>]]])

stmt参数是字符串形式的一个代码段,这个代码段将被评测运行时间;setup参数用以设置stmt的运行环境;timer可以由用户使用自定义精度的计时函数。

timeit.Timer有三个成员函数,下面简单介绍一下:

timeit( [number=1000000])

timeit()执行一次Timer构造函数中的setup语句之后,就重复执行number次stmt语句,然后返回总计运行消耗的时间。

repeat( [repeat=3 [, number=1000000]])

repeat()函数以number为参数调用timeit函数repeat次,并返回总计运行消耗的时间

print_exc( [file=None])

print_exc()函数用以代替标准的tracback,原因在于print_exc()会输出错行的源代码,如:


>>> t = timeit.Timer("t = foo()/nprint t")      ß被timeit的代码段

>>> t.timeit()

Traceback (most recent call last):

File "<pyshell#12>", line 1, in -toplevel-

t.timeit()

File "E:/Python23/lib/timeit.py", line 158, in timeit

return self.inner(it, self.timer)

File "<timeit-src>", line 6, in inner

foo()         ß标准输出是这样的

NameError: global name ‘foo‘ is not defined

>>> try:

t.timeit()

except:

t.print_exc()

Traceback (most recent call last):

File "<pyshell#17>", line 2, in ?

File "E:/Python23/lib/timeit.py", line 158, in timeit

return self.inner(it, self.timer)

File "<timeit-src>", line 6, in inner

t = foo()        ßprint_exc()的输出是这样的,方便定位错误

NameError: global name ‘foo‘ is not defined

除了可以使用timeit的编程接口外,我们也可以在命令行里使用timeit,非常方便:

python timeit.py [-n N] [-r N] [-s S] [-t] [-c] [-h] [statement ...]

其中参数的定义如下:

-n N/--number=N

statement语句执行的次数

-r N/--repeat=N

重复多少次调用timeit(),默认为3

-s S/--setup=S

用以设置statement执行环境的语句,默认为”pass”

-t/--time

计时函数,除了Windows平台外默认使用time.time()函数,

-c/--clock

计时函数,Windows平台默认使用time.clock()函数

-v/--verbose

输出更大精度的计时数值

-h/--help

简单的使用帮助

小巧实用的timeit蕴藏了无限的潜能等待你去发掘,我在这里就不提供实例啦~~

后记

原本我只打算写写profile的使用及自己应用的一些心得的,但仔细阅读了相关的manual之后发现实用的东西很多很多,所以就罗罗嗦嗦地写了这么多,自己的应用经验和心得只好容后再述了。在写这篇介绍的时候,我想起自己以前写过的一个A*算法的python实现,没有进行过任何优化,所以打算以它为实例,以后抽空写上一篇有比较实用的例子贯穿全文的文档吧。

本文撰写的时候参考了python 2.3/2.4/2.5版本的manual,文中介绍的内容大部分适用于python 2.3或以上的版本,其中cProfile需要2.5版本才能支持。

[python]用profile协助程序性能优化

时间: 2024-11-05 15:57:38

[python]用profile协助程序性能优化的相关文章

Java程序性能优化——性能调优层次

为了提升系统性能,开发人员可以从系统的各个角度和层次对系统进行优化.除了最常见的代码优化外,在软件架构上.JVM虚拟机层.数据库以及操作系统层都可以通过各种手段进行调优,从而在整体上提升系统的性能. 设计调优 设计调优处于所有调优手段的上层,它往往需要在软件开发之前进行.在软件开发之初,软件架构师就应该评估系统可能存在的各种潜在的问题,并给出合理的设计方案.由于软件设计和架构对软件整体有决定性的影响,所以,设计调优对系统性能的影响也是最大的.如果说,代码优化.JVM优化都是对系统微观层面上"量&

程序性能优化之SQL篇

如果说功能是程序的躯体,那么性能就是程序的灵魂.完整的功能可以保证程序的躯体是健全的,而良好的性能才是程序灵魂的象征,本文就程序的性能优化做简单的介绍. 最近对程序的性能的体会尤为深刻.最近做了一个数据查询和显示的功能,从7张表大概1500条数据中查询25条数据并且显示出来,时间消耗1秒钟.我的计算机参数为:CPU:i5处理器,4G内存.这个执行速度相当的慢,好在我查询的数据量比较小,等待时间不是很多,但是程序性能优化确实刻不容缓. 仔细分析了程序,发现有很多地方都是需要修改的,我们先从数据库开

Java程序性能优化技巧

多线程.集合.网络编程.内存优化.缓冲..spring.设计模式.软件工程.编程思想 1.生成对象时,合理分配空间和大小new ArrayList(100); 2.优化for循环Vector vect = new Vector(1000);for( inti=0; i<vect.size(); i++){ ...}for循环部分改写成:int size = vect.size();for( int i=0; i>size; i++){ ...} 如果size=1000,就可以减少1000次si

《Java程序性能优化》学习笔记 Ⅰ设计优化

豆瓣读书:http://book.douban.com/subject/19969386/ 第一章 Java性能调优概述 1.性能的参考指标 执行时间: CPU时间: 内存分配: 磁盘吞吐量: 网络吞吐量: 响应时间: 2.木桶定律   系统的最终性能取决于系统中性能表现最差的组件,例如window系统内置的评分就是选取最低分.可能成为系统瓶颈的计算资源如,磁盘I/O,异常,数据库,锁竞争,内存等. 性能优化的几个方面,如设计优化,Java程序优化,并行程序开发及优化,JVM调优,Java性能调

[JAVA] java程序性能优化

一.避免在循环条件中使用复杂表达式 在不做编译优化的情况下,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快. 例子: import java.util.vector; class cel { void method (vector vector) { for (int i = 0; i < vector.size (); i++) // violation ; // ... } } 更正: class cel_fixed { void metho

Java程序性能优化之代理模式

代理模式的用处很多,有的是为了系统安全,有的是为了远程调用,这里我们,主要探讨下由于程序性能优化的延迟加载. 首先我们来看下代理模式设计 先首先简单阐述下什么叫代理模式吧 代理设计模式有一个接口,另外还有真实主题类和代理类,真实类和代理类都实现了接口,代理类和真实主题类是关联和聚合关系.客户端与接口关联. 代理分为静态代理和动代态代理所谓静态代理是为真实主题手动创建一个代理,而动态代理则是jvm在运行时运用字节码加载技术自动创建一个代理,并不用关心接口和真是主题类 具体如何实现 哦,对了差点忘了

Java程序性能优化:代码优化

现在计算机的处理性能越来越好,加上JDK升级对一些代码的优化,在代码层针对一些细节进行调整可能看不到性能的明显提升, 但是我觉得在开发中注意这些,更多的是可以保持一种性能优先的意识,对一些敲代码时间比较短的同学挺有意义的. 一 循环条件下,循环体和判断条件中,都要避免对使用复杂表达式,减少对变量的重复计算 1.在循环中应该避免使用复杂的表达式. 在循环中,循环条件会被反复计算,应该避免把一些计算放在循环进行的部分中,程序将会运行的更快.比如: for(int i=0;i<list.size();

关于程序性能优化基础的一些个人总结

性能点: I/O,系统调用,并发/锁,内存分配,内存拷贝,函数调用消耗,编译优化,算法 I/O性能优化: I/O性能主要耗费点:系统调用,磁盘读写,网络通讯等 优化点:减少系统调用次数,减少磁盘读写次数,减少阻塞等待 优化手段: a. 使用非阻塞模式 b. 使用带缓存的I/O,减少磁盘读写次数 c. I/O多路复用,select/poll/epoll d. 异步I/O 系统调用: 耗费点:用户态和系统态切换时耗 优化点:减少不必要的系统调用 优化手段: a. I/O操作,根据具体情况,使用std

Java程序性能优化——设计优化

原文出自:http://blog.csdn.net/anxpp/article/details/51914119,转载请注明出处,谢谢! 1.前言 OK,之前写了一篇文章:"23种设计模式介绍以及在Java中的应用"详细介绍了如何将设计模式应用到Java编程中,而本文旨在介绍如何利用他们优化我们的程序,使其性能更佳. 设计模式的详细介绍请参照上面链接中的文章,不是本文的重点. 而Java程序的性能优化,不一定就仅仅是以提高系统性能为目的的,还可能是以用户体验.系统可维护性等为目的. 2