【编者按】本文做者为 Bryan Helmig,主要介绍 Python 应用性能分析的三种进阶方案。文章系国内 ITOM 管理平台 OneAPM 编译呈现。html
咱们应该忽略一些微小的效率提高,几乎在 97% 的状况下,都是如此:过早的优化是万恶之源。—— Donald Knuthpython
若是不先想一想Knuth的这句名言,就开始进行优化工做,是不明智的。然而,有时你为了得到某些特性不假思索就写下了O(N^2) 这样的代码,虽然你很快就忘记它们了,它们却可能反咬你一口,给你带来麻烦:本文就是为这种状况而准备的。git
本文会介绍用于快速分析Python程序的一些有用工具和模式。主要目标很简单:尽快发现问题,修复问题,并确认修复是行之有效的。github
在教程开始前,要先写一个简单的概要测试来演示延迟。你可能须要引入一些最小数据集来重现可观的延迟。一般一或两秒的运行时间,已经足够在发现问题时,让你作出改进了。数据库
此外,进行一些基础测试来确保你的优化不会修改缓慢代码的行为也是有必要的。在分析和调整时,你也能够屡次运行这些测试,做为基准。api
那么如今,咱们来看第一个分析工具。缓存
计时器是简单、灵活的记录执行时间的方法。你能够把它放到任何地方,而且几乎没有反作用。本身建立计时器很是简单,而且能够根据你的喜爱定制化。例如,一个简单的计时器能够这么写:服务器
import time def timefunc(f): def f_timer(*args, **kwargs): start = time.time() result = f(*args, **kwargs) end = time.time() print f.__name__, 'took', end - start, 'time' return result return f_timer def get_number(): for x in xrange(5000000): yield x @timefunc def expensive_function(): for x in get_number(): i = x ^ x ^ x return 'some result!' # prints "expensive_function took 0.72583088875 seconds" result = expensive_function()
固然,你能够用上下文管理器来加强它的功能,添加一些检查点或其余小功能:数据结构
import time class timewith(): def __init__(self, name=''): self.name = name self.start = time.time() @property def elapsed(self): return time.time() - self.start def checkpoint(self, name=''): print '{timer} {checkpoint} took {elapsed} seconds'.format( timer=self.name, checkpoint=name, elapsed=self.elapsed, ).strip() def __enter__(self): return self def __exit__(self, type, value, traceback): self.checkpoint('finished') pass def get_number(): for x in xrange(5000000): yield x def expensive_function(): for x in get_number(): i = x ^ x ^ x return 'some result!' # prints something like: # fancy thing done with something took 0.582462072372 seconds # fancy thing done with something else took 1.75355315208 seconds # fancy thing finished took 1.7535982132 seconds with timewith('fancy thing') as timer: expensive_function() timer.checkpoint('done with something') expensive_function() expensive_function() timer.checkpoint('done with something else') # or directly timer = timewith('fancy thing') expensive_function() timer.checkpoint('done with something')
有了计时器,你还须要进行一些“挖掘”工做。 封装一些更为高级的函数,而后肯定问题根源之所在,进而深刻可疑的函数,不断重复。当你发现运行特别缓慢的代码以后,修复它,而后进行测试以确认修复成功。ide
提示:不要忘了便捷的 timeit 模块!将它用于小段代码块的基准校验比实际测试更加有用。
计时器的优势:容易理解和实施,也很是容易在修改先后进行对比,对于不少语言都适用。
计时器的缺点:有时候,对于很是复杂的代码库而已太过简单,你可能会花更多的时间建立、替换样板代码,而不是修复问题!
内建分析器就好像大型枪械。虽然很是强大,可是有点不太好用,有时,解释和操做起来比较困难。
你能够点此阅读更多关于内建分析模块的内容,可是内建分析器的基本操做很是简单:你启用和禁用分析器,它能记录全部的函数调用和执行时间。接着,它能为你编译和打印输出。一个简单的分析器用例以下:
import cProfile def do_cprofile(func): def profiled_func(*args, **kwargs): profile = cProfile.Profile() try: profile.enable() result = func(*args, **kwargs) profile.disable() return result finally: profile.print_stats() return profiled_func def get_number(): for x in xrange(5000000): yield x @do_cprofile def expensive_function(): for x in get_number(): i = x ^ x ^ x return 'some result!' # perform profiling result = expensive_function()
在上面代码中,控制台打印的内容以下:
5000003 function calls in 1.626 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 5000001 0.571 0.000 0.571 0.000 timers.py:92(get_number) 1 1.055 1.055 1.626 1.626 timers.py:96(expensive_function) 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
如你所见,它给出了不一样函数调用的详细数据。可是,它遗漏了一项关键信息:是什么缘由,致使函数运行如此缓慢?
然而,这对于基础分析来讲是个好的开端。有时,可以帮你尽快找到解决方案。我常常在开始调试过程时,把它做为基本测试,而后再深刻测试某个不是运行缓慢,就是调用频繁的特定函数。
内建分析器的优势:没有外部依赖,运行很是快。对于快速的概要测试很是有用。
内建分析器的缺点:信息相对有限,须要进一步的调试;报告不太直观,尤为是对于复杂的代码库。
若是内建分析器是大型枪械,line profiler就比如是离子炮。它很是的重量级且强大,使用起来也很是有趣。
在这个例子里,咱们会用很是棒的kernprof line-profiler,做为 line_profiler PyPi包。为了方便使用,咱们会再次用装饰器进行封装,同时也能够防止咱们把它留在生产代码里(由于它比蜗牛还慢)。
try: from line_profiler import LineProfiler def do_profile(follow=[]): def inner(func): def profiled_func(*args, **kwargs): try: profiler = LineProfiler() profiler.add_function(func) for f in follow: profiler.add_function(f) profiler.enable_by_count() return func(*args, **kwargs) finally: profiler.print_stats() return profiled_func return inner except ImportError: def do_profile(follow=[]): "Helpful if you accidentally leave in production!" def inner(func): def nothing(*args, **kwargs): return func(*args, **kwargs) return nothing return inner def get_number(): for x in xrange(5000000): yield x @do_profile(follow=[get_number]) def expensive_function(): for x in get_number(): i = x ^ x ^ x return 'some result!' result = expensive_function()
若是运行上面的代码,就会看到如下的报告:
Timer unit: 1e-06 s File: test.py Function: get_number at line 43Total time: 4.44195 s Line # Hits Time Per Hit % Time Line Contents ============================================================== 43 def get_number(): 44 5000001 2223313 0.4 50.1 for x in xrange(5000000): 45 5000000 2218638 0.4 49.9 yield x File: test.py Function: expensive_function at line 47Total time: 16.828 s Line # Hits Time Per Hit % Time Line Contents ============================================================== 47 def expensive_function(): 48 5000001 14090530 2.8 83.7 for x in get_number(): 49 5000000 2737480 0.5 16.3 i = x ^ x ^ x 50 1 0 0.0 0.0 return 'some result!'
如你所见,这是一个很是详细的报告,能让你彻底洞悉代码的运行状况。和内建的cProfiler不一样,它能分析核心语言特性的耗时,好比循环或导入,而且给出不一样代码行的耗时累计值。
这些细节能让咱们更容易理解函数内部原理。 此外,若是须要研究第三方库,你能够将其导入,直接输到装饰器中。
提示:将测试函数封装为装饰器,再将问题函数做为参数传进去就行了!
Line Profiler 的优势:有很是直接和详细的报告。可以追踪第三方库里的函数。
Line Profiler 的缺点:由于系统开销巨大,会比实际执行时间慢一个数量级,因此不要用它进行基准测试。同时,它是外部工具。
你应该使用简单的工具(好比计时器或内建分析器)对测试用例(特别是那些你很是熟悉的代码)进行基本检查,而后使用更慢但更加细致的工具,好比 line_profiler
,深刻检查函数内部。
十有八九,你会发现一个愚蠢的错误,好比在循环内重复调用,或是使用了错误的数据结构,消耗了90%的函数执行时间。在进行快速(且使人满意的)调整以后,问题就能获得解决。
若是你仍然以为程序运行太过缓慢,而后开始进行对比属性访问(ttribute accessing)方法,或调整相等检查(equality checking)方法等晦涩的调整,你可能已经拔苗助长了。你应该考虑以下方法:
1.忍受缓慢或者预先计算/缓存
2.从新思考整个实施方法
3.使用更多的优化数据结构(经过 Numpy,Pandas等)
4.编写一个 C扩展
注意,优化代码会带来有罪恶感的快乐!寻找加速Python的合理方法颇有趣,可是不要由于加速,破坏了自己的逻辑。易读的代码比运行速度更重要。实施缓存,每每是最简单的解决方法。
教程到此为止,但愿你从此的Python性能分析可以如鱼得水!
PS: 点此查看代码实例。此外,点此学习如何如鱼得水地调试 Python 程序。
OneAPM 能帮你查看 Python 应用程序的方方面面,不只可以监控终端的用户体验,还能监控服务器性能,同时还支持追踪数据库、第三方 API 和 Web 服务器的各类问题。想阅读更多技术文章,请访问 OneAPM 官方技术博客。
本文转自 OneAPM 官方博客