没有优化过的程序一般会在某些子程序(subroutine)上消耗大部分的CPU指令周期(CPU cycle)。性能分析就是分析代码和它正在使用的资源之间有着怎样的关系。例如,性能分析能够告诉你一个指令占用了多少CPU时间,或者整个程序消耗了多少内存。性能分析是经过使用一种被称为性能分析器(profiler)的工具,对程序或者二进制可执行文件(若是能够拿到)的源代码进行调整来完成的。html
性能分析软件有两类方法论:基于事件的性能分析(event-based profiling)和统计式性能分析(statistical profiling)。python
支持这类基于事件的性能分析的编程语言主要有如下几种。git
基于事件的性能分析器(event-based profiler,也称为轨迹性能分析器,tracing profiler)是经过收集程序执行过程当中的具体事件进行工做的。这些性能分析器会产生大量的数据。基本上,它们须要监听的事件越多,产生的数据量就越大。这致使它们不太实用,在开始对程序进行性能分析时也不是首选。可是,当其余性能分析方法不够用或者不够精确时,它们能够做为最后的选择。程序员
Python基于事件的性能分析器的简单示例代码github
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)
执行结果:算法
$ python test.py PROFILER: 'call' None PROFILER: 'call' None PROFILER: 'call' None PROFILER: 'call' None PROFILER: 'return' 0 PROFILER: 'c_call' <built-in method append of list object at 0x7f113d7f67a0> PROFILER: 'c_return' <built-in method append of list object at 0x7f113d7f67a0> PROFILER: 'return' [0] PROFILER: 'c_call' <built-in method extend of list object at 0x7f113d7e0d40> PROFILER: 'c_return' <built-in method extend of list object at 0x7f113d7e0d40> PROFILER: 'call' None PROFILER: 'return' 1 PROFILER: 'c_call' <built-in method append of list object at 0x7f113d7e0d40> PROFILER: 'c_return' <built-in method append of list object at 0x7f113d7e0d40> PROFILER: 'return' [0, 1] PROFILER: 'c_call' <built-in method extend of list object at 0x7f113d7e0758> PROFILER: 'c_return' <built-in method extend of list object at 0x7f113d7e0758> PROFILER: 'call' None PROFILER: 'call' None PROFILER: 'return' 1 PROFILER: 'call' None PROFILER: 'return' 0 PROFILER: 'return' 1 PROFILER: 'c_call' <built-in method append of list object at 0x7f113d7e0758> PROFILER: 'c_return' <built-in method append of list object at 0x7f113d7e0758> PROFILER: 'return' [0, 1, 1] [0, 1, 1] PROFILER: 'return' None PROFILER: 'call' None PROFILER: 'c_call' <built-in method discard of set object at 0x7f113d818960> PROFILER: 'c_return' <built-in method discard of set object at 0x7f113d818960> PROFILER: 'return' None PROFILER: 'call' None PROFILER: 'c_call' <built-in method discard of set object at 0x7f113d81d3f0> PROFILER: 'c_return' <built-in method discard of set object at 0x7f113d81d3f0> PROFILER: 'return' None
统计式性能分析器以固定的时间间隔对程序计数器(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 "====== ======="
执行结果:
$ python test.py About to calculate the fibonacci sequence for the number 30 Now we print the numbers: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 10946 17711 28657 46368 75025 121393 196418 317811 514229 832040 ====== Profiling results ======= Time required to print a simple message: 0:00:00.000064 Time required to calculate fibonacci: 0:00:01.430740 Time required to iterate and print the numbers: 0:00:00.000075 ====== =======
可见计算部分是最消耗时间的。
只要你测量出了程序的运行时间,就能够把注意力移到运行慢的环节上作性能分析。通常瓶颈由下面的一种或者几种缘由组成:
* 重的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标记用于表示包含无限项的
函数的有限特征(相似于泰勒展开式)。若是把这个概念用于计算机科学,就能够把算法的运行
时间描述成渐进的有限特征(数量级)。
主要模型有:
速度:对数>线性>线性对数>平方>阶乘, 要考虑最好状况、正常状况和最差状况。
创建回归测试套件、思考代码结构、耐心、尽量多地收集数据(其余数据资源,如网络应用的系统日志、自定义日志、系统资源快照(如操做系统任务管理器))、数据预处理、数据可视化
python中最出名的性能分析库:cProfile、line_profiler。
前者是标准库:https://docs.python.org/2/library/profile.html#module-cProfile。
后者参见:https://github.com/rkern/line_profiler。
专一于CPU时间。
审稿的博客:http://www.blog.pythonlibrary.org/