python性能分析
阅读目录
- 调优简介
- Python基于事件的性能分析器的简单示例代码
- Linux统计式性能分析器OProfile(http://oprofile.sourceforge.net/news/)的分析结果:
- 性能分析的重要性
- 性能分析的内容
- 内存消耗和内存泄漏
- 过早优化的风险
- 运行时间复杂度
- 性能分析最佳实践
调优简介
什么是性能分析
没有优化过的程序通常会在某些子程序(subroutine)上消耗大部分的CPU指令周期(CPU cycle)。性能分析就是分析代码和它正在使用的资源之间有着怎样的关系。
例如,性能分析可以告诉你一个指令占用了多少CPU时间,或者整个程序消耗了多少内存。
性能分析是通过使用一种被称为性能分析器(profiler)的工具,对程序或者二进制可执行文件(如果可以拿到)的源代码进行调整来完成的。
性能分析软件有两类方法论:基于事件的性能分析(event-based profiling)和统计式性能分析(statistical profiling)。
支持这类基于事件的性能分析的编程语言主要有以下几种。
Java:JVMTI(JVM Tools Interface,JVM工具接口)为性能分析器提供了钩子,可以跟踪诸如函数调用、线程相关的事件、类加载之类的事件。 .NET:和Java一样,.NET运行时提供了事件跟踪功能(https://en.wikibooks.org/wiki/Intro-duction_to_Software_Engineering/Testing/Profiling#Methods_of_data_gathering)。 Python: 开发者可以用 sys.setprofile 函数,跟踪 python_[call|return|exception]或 c_[call|return|exception] 之类的事件。
基于事件的性能分析器(event-based profiler,也称为轨迹性能分析器,tracing profiler)是通过收集程序执行过程中的具体事件进行工作的。
这些性能分析器会产生大量的数据。基本上,它们需要监听的事件越多,产生的数据量就越大。这导致它们不太实用,在开始对程序进行性能分析时也不是首选。
但是,当其他性能分析方法不够用或者不够精确时,它们可以作为最后的选择。
Python基于事件的性能分析器的简单示例代码
import sys def profiler(frame, event, arg): print ‘PROFILER: %r %r‘ % (event, arg) sys.setprofile(profiler) #simple (and very ineficient) example of how to calculate the Fibonacci sequence for a number. def fib(n): if n == 0: return 0 elif n == 1: return 1 else: return fib(n-1) + fib(n-2) def fib_seq(n): seq = [ ] if n > 0: seq.extend(fib_seq(n-1)) seq.append(fib(n)) return seq print fib_seq(2)
执行结果:
统计式性能分析器以固定的时间间隔对程序计数器(program counter)进行抽样统计。这样做可以让开发者掌握目标程序在每个函数上消耗的时间。
由于它对程序计数器进行抽样,所以数据结果是对真实值的统计近似。不过,这类软件足以窥见被分析程序的性能细节,查出性能瓶颈之所在。
它使用抽样的方式(用操作系统中断),分析的数据更少,对性能造成的影响更小。
Linux统计式性能分析器OProfile(http://oprofile.sourceforge.net/news/)的分析结果:
Function name,File name,Times Encountered,Percentage "func80000","statistical_profiling.c",30760,48.96% "func40000","statistical_profiling.c",17515,27.88% "func20000","static_functions.c",7141,11.37% "func10000","static_functions.c",3572,5.69% "func5000","static_functions.c",1787,2.84% "func2000","static_functions.c",768,1.22% func1500","statistical_profiling.c",701,1.12% "func1000","static_functions.c",385,0.61% "func500","statistical_profiling.c",194,0.31%
下面我们使用statprof进行分析:
import statprof def profiler(frame, event, arg): print ‘PROFILER: %r %r‘ % (event, arg) #simple (and very ineficient) example of how to calculate the Fibonacci sequence for a number. def fib(n): if n == 0: return 0 elif n == 1: return 1 else: return fib(n-1) + fib(n-2) def fib_seq(n): seq = [ ] if n > 0: seq.extend(fib_seq(n-1)) seq.append(fib(n)) return seq statprof.start() try: print fib_seq(20) finally: statprof.stop() statprof.display()
执行结果:
$ python test.py [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765] % cumulative self time seconds seconds name 100.00 0.01 0.01 test.py:15:fib 0.00 0.01 0.00 test.py:21:fib_seq 0.00 0.01 0.00 test.py:20:fib_seq 0.00 0.01 0.00 test.py:27:<module> --- Sample count: 2 Total time: 0.010000 seconds
注意上面代码我们把计算fib_seq的参数从2改成20,因为执行时间太快的情况下,statprof是获取不到任何信息的。
性能分析的重要性
性能分析并不是每个程序都要做的事情,尤其对于那些小软件来说,是没多大必要的(不像那些杀手级嵌入式软件或专门用于演示的性能分析程序)。性能分析需要花时间,而且只有在程序中发现了错误的时候才有用。但是,仍然可以在此之前进行性能分析,捕获潜在的bug,这样可以节省后期的程序调试时间。
我们已经拥有测试驱动开发、代码审查、结对编程,以及其他让代码更加可靠且符合预期的手段,为什么还需要性能分析?
随着我们使用的编程语言越来越高级(几年间我们就从汇编语言进化到了JavaScript),我们愈加不关心CPU循环周期、内存配置、CPU寄存器等底层细节了。新一代程序员都通过高级语言学习编程技术,因为它们更容易理解而且开箱即用。但它们依然是对硬件和与硬件交互行为的抽象。随着这种趋势的增长,新的开发者越来越不会将性能分析作为软件开发中的一个步骤了。
如今,随便开发一个软件就可以获得上千用户。如果通过社交网络一推广,用户可能马上就会呈指数级增长。一旦用户量激增,程序通常会崩溃,或者变得异常缓慢,最终被客户无情抛弃。
上面这种情况,显然可能是由于糟糕的软件设计和缺乏扩展性的架构造成的。毕竟,一台服务器有限的内存和CPU资源也可能会成为软件的瓶颈。但是,另一种可能的原因,也是被证明过许多次的原因,就是我们的程序没有做过压力测试。我们没有考虑过资源消耗情况;我们只保证了测试已经通过,而且乐此不疲。
性能分析可以帮助我们避免项目崩溃夭折,因为它可以相当准确地为我们展示程序运行的情况,不论负载情况如何。因此,如果在负载非常低的情况下,通过性能分析发现软件在I/O操作上消耗了80%的时间,那么这就给了我们一个提示。是产品负载过重时,内存泄漏就可能发生。性能分析可以在负载真的过重之前,为我们提供足够的证据来发现这类隐患。
性能分析的内容
运行时间
如果你对运行的程序有一些经验(比如说你是一个网络开发者,正在使用一个网络框架),可能很清楚运行时间是不是太长。
例如,一个简单的网络服务器查询数据库、响应结果、反馈到客户端,一共需要100毫秒。但是,如果程序运行得很慢,做同样的事情需要花费60秒,你就得考虑做性能分析了。
import datetime tstart = None tend = None def start_time(): global tstart tstart = datetime.datetime.now() def get_delta(): global tstart tend = datetime.datetime.now() return tend - tstart def fib(n): return n if n == 0 or n == 1 else fib(n-1) + fib(n-2) def fib_seq(n): seq = [ ] if n > 0: seq.extend(fib_seq(n-1)) seq.append(fib(n)) return seq start_time() print "About to calculate the fibonacci sequence for the number 30" delta1 = get_delta() start_time() seq = fib_seq(30) delta2 = get_delta() print "Now we print the numbers: " start_time() for n in seq: print n delta3 = get_delta() print "====== Profiling results =======" print "Time required to print a simple message: %(delta1)s" % locals() print "Time required to calculate fibonacci: %(delta2)s" % locals() print "Time required to iterate and print the numbers: %(delta3)s" %locals() print "====== ======="
执行结果:
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 31 32 33 34 35 36 37 38 39 |
|
可见计算部分是最消耗时间的。
发现瓶颈
只要你测量出了程序的运行时间,就可以把注意力移到运行慢的环节上做性能分析。一般瓶颈由下面的一种或者几种原因组成:
* 重的I/O操作,比如读取和分析大文件,长时间执行数据库查询,调用外部服务(比如HTTP请求),等等。 * 现了内存泄漏,消耗了所有的内存,导致后面的程序没有内存来正常执行。 * 未经优化的代码频繁执行。 * 可以缓存时密集的操作没有缓存,占用了大量资源。
I/O关联的代码(文件读/写、数据库查询等)很难优化,因为优化有可能会改变程序执行I/O操作的方式(通常是语言的核心函数操作I/O)。相反,优化计算关联的代码(比如程序使用的算法很糟糕),改善性能会比较容易(并不一定很简单)。这是因为优化计算关联的代码就是改写程序。
内存消耗和内存泄漏
内存消耗不仅仅是关注程序使用了多少内存,还应该考虑控制程序使用内存的数量。跟踪程序内存的消耗情况比较简单。最基本的方法就是使用操作系统的任务管理器。
它会显示很多信息,包括程序占用的内存数量或者占用总内存的百分比。任务管理器也是检查CPU时间使用情况的好工具。
在下面的top截图中,你会发现一个简单的Python程序(就是前面那段程序)几乎占用了全部CPU(99.8%),内存只用了0.1%。
当运行过程启动之后,内存消耗会在一个范围内不断增加。如果发现增幅超出范围,而且消
耗增大之后一直没有回落,就可以判断出现内存泄漏了。
过早优化的风险
优化通常被认为是一个好习惯。但是,如果一味优化反而违背了软件的设计原则就不好了。在开始开发一个新软件时,开发者经常犯的错误就是过早优化(permature optimization)。如果过早优化代码,结果可能会和原来的代码截然不同。它可能只是完整解决方案的一部分,还可能包含因优化驱动的设计决策而导致的错误。
一条经验法则是,如果你还没有对代码做过测量(性能分析)
优化往往不是个好主意。首先,应该集中精力完成代码,然后通过性能分析发现真正的性能瓶颈,最后对代码进行优化。
运行时间复杂度
运行时间复杂度(Running Time Complexity,RTC)用来对算法的运行时间进行量化。它是对算法在一定数量输入条件下的运行时间进行数学近似的结果。因为是数学近似,所以我们可以用这些数值对算法进行分类。
RTC常用的表示方法是大O标记(big O notation)。数学上,大O标记用于表示包含无限项的
函数的有限特征(类似于泰勒展开式)。如果把这个概念用于计算机科学,就可以把算法的运行
时间描述成渐进的有限特征(数量级)。
主要模型有:
常数时间——O(1):比如判断一个数是奇数还是偶数、用标准输出方式打印信息等。对于理论上更复杂的操作,比如在字典(或哈希表)中查找一个键的值,如果算法合理,就 可以在常数时间内完成。技术上看,在哈希表中查找元素的消耗时间是O(1)平均时间,这意味着每次操作的平均时间(不考虑特殊情况)是固定值O(1)。
线性时间——O(n):比如查找无序列表中的最小元素、比较两个字符串、删除链表中的最后一项
对数时间——O(logn):对数时间(logarithmic time)复杂度的算法,表示随着输入数量的增加,算法的运行时间会达到固定的上限。随着输入数量的增加,对数函数开始增长很快,然后慢慢减速。 它不会停止增长,但是越往后增长的速度越慢,甚至可以忽略不计。比如:二分查找(binary search)、计算斐波那契数列(用矩阵乘法)。
线性对数时间——O(nlogn):把前面两种时间类型组合起来就变成了线性对数时间(linearithmic time)。随着x的增大,算法的运行时间会快速增长。 比如归并排序(merge sort)、堆排序(heap sort)、快速排序(quick sort,至少是平均运行时间)
阶乘时间——O(n!):阶乘时间(factorial time)复杂度的算法是最差的算法。其时间增速特别快,图都很难画。比如:用暴力破解搜索方法解货郎担问题(遍历所有可能的路径)。
平方时间——O(n 2 ):平方时间是另一个快速增长的时间复杂度。输入数量越多,需要消耗的时间越长(大多数算法都是这样,这类算法尤其如此)。 平方时间复杂度的运行效率比线性时间复杂度要慢。比如冒泡排序(bubble sort)、遍历二维数组、插入排序(insertion sort)
速度:对数>线性>线性对数>平方>阶乘, 要考虑最好情况、正常情况和最差情况。
性能分析最佳实践
建立回归测试套件、思考代码结构、耐心、尽可能多地收集数据(其他数据资源,如网络应用的系统日志、自定义日志、系统资源快照(如操作系统任务管理器))、数据预处理、数据可视化
python中最出名的性能分析库:cProfile、line_profiler。
前者是标准库:https://docs.python.org/2/library/profile.html#module-cProfile。
后者参见:https://github.com/rkern/line_profiler。
专注于CPU时间。
声明:原文链接:my.oschina.net/u/1433482/blog/709219