啥叫做基准测试(benchmark)代码?其实主要就是测试(benchmarking)和分析(profiling)你的代码执行多快,并且找到代码瓶颈(bottlenecks)在哪里。 执行该动作的主要的目的就是优化(optimization)。也许因为业务需要你并且让你的代码执行更快一些。 当面临这种情况时,你就需要找出你的代码是在哪些部分慢的。
本文覆盖如何使用不同工具进行代码测试。
timeit
Python再带一个模块timeit。你可以使用它来对小代码进行计时。timeit模块使用针对平台的时间函数,你可以得到非常准确的一个时间结果。
timeit模块有个命令行接口,也可以通过导入的方式。我们将从命令行中使用开始。打开终端执行如下命令:
$ python3 -m timeit -s "[ord(x) for x in ‘abcdfghi‘]" 100000000 loops, best of 3: 0.00907 usec per loop $ python3 -m timeit -s "[chr(int(x)) for x in ‘123456789‘]" 100000000 loops, best of 3: 0.00897 usec per loop
发生啥啦?好吧,当你执行Python命令并且传递-m参数时,你是在告诉解释器去找该模块并且作为主要程序执行。 -s告知timeit模块执行一次代码。然后timeit就行执行代码三次,并且返回该三次的平均结果。该例子太过减少,它们 之间的区别可能不太明显。
你的机器上运行的结果可能跟我的会有一些偏差。
我们再写个简单函数,看看是否可以从命令行测试
# simple_func.py def my_function(): try: 1 / 0 except ZeroDivisionError: pass
整个函数的功能就是抛出一个异常然后直接忽略。很烂的例子。要通过命令使用timeit来测试,我们需要在命令空间中导入它,确保工作目录在当前文件目录下,然后执行:
$ python3 -m timeit "import simple_func; simple_func.my_function()" 1000000 loops, best of 3: 0.753 usec per loop
这里我们就导入该模块并且直接my_function()函数。注意我们使用了;来分来导入和执行代码。现在可以开始学习如何在Python脚本代码内部使用timeit了。
导入timeit测试
在代码中使用timeit模块也是相当容易的。我们还是使用之前很烂的例子:
def my_function(): try: 1 / 0 except ZeroDivisionError: pass if __name__ == "__main__": import timeit setup = "from __main__ import my_function" print(timeit.timeit("my_function()", setup=setup))
我们执行通过执行该脚本来验证。我们导入了timeit模块,然后调用timeit函数,参数的setup字符串将导入要测试函数到timeit的作用域中。现在 我们实现自己的装饰器测试函数。
使用装饰器
实现自己的装饰器计时函数是一件很有趣的事情,虽然不一定有timeit精确。
import random import time def timerfunc(func): """ A timer decorator """ def function_timer(*args, **kwargs): """ A nested function for timing other functions """ start = time.time() value = func(*args, **kwargs) end = time.time() runtime = end - start msg = "The runtime for {func} took {time} seconds to complete" print(msg.format(func=func.__name__, time=runtime)) return value return function_timer @timerfunc def long_runner(): for x in range(5): sleep_time = random.choice(range(1, 5)) time.sleep(sleep_time) if __name__ == "__main__": long_runner()
该例子中,我们导入了Python标准库的random和time模块。然后我们创建了装饰器函数。你将注意到它接收了一个函数以及 包含了另一函数。嵌套的函数在调用实际函数时将抓取时间,然后等待函数返回并抓取结束时间。现在我们知道函数所花费的时间,我们将它 打印出来。当然该装饰器函数也需要将实际函数执行返回的结果返回回来。
接下来的函数就是包装了该计时装饰器函数。你可以看到这里使用了随机休眠,该操作只是用来模拟耗时长程序。 也许你用来执行连接数据库(或者较大查询),跑线程或其他事情。
每次跑这段代码,结果都会有些不同。自己试试!
创建计时上下文管理器
一些码农可能更喜欢使用上下文的方式来测试小段代码。我们来构建看看:
import random import time class MyTimer: def __init__(self): self.start = time.time() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): end = time.time() runtime = end - self.start msg = ‘The function took {time} seconds to complete‘ print(msg.format(time=runtime)) def long_runner(): for x in range(5): sleep_time = random.choice(range(1, 5)) time.sleep(sleep_time) if __name__ == "__main__": with MyTimer(): long_runner()
本例中,我们使用__init__方法来开始我们计时,__enter__方法不做任何事情,仅仅返回它自己。__exit__方法 中抓取了结束时间,计算了总执行时间并打印。
然后我们就使用上下文管理器来测试我们执行的代码。
cProfile
Python内建了分析器。profile和cProfile模块。profile模块是纯Python实现,在测试过程中可能会增加很多额外的开支。所以推荐还是使用更快的cProfile。
我们不会过细的讲解该模块,我们来看看一些例子。
>>> import cProfile >>> cProfile.run("[x for x in range(1500)]") 4 function calls in 0.000 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 0.000 0.000 <string>:1(<listcomp>) 1 0.000 0.000 0.000 0.000 <string>:1(<module>) 1 0.000 0.000 0.000 0.000 {built-in method exec} 1 0.000 0.000 0.000 0.000 {method ‘disable‘ of ‘_lsprof.Profiler‘ objects}
我们来了解一下内容。第一行显示了有4个函数调用。下一行告诉结果排序方式。根据文档,standard name指的是最右边的列。 这里我们有好多列数据:
- ncalls: 调用了多少次
- tottime: 函数所花费的总时间
- percall: ncalls / tottime
- cumtime: 涵盖了该函数下的所有子函数(甚至地柜)函数所话费的时间
- 第二个percall: cumtime / 原始钓鱼那个
- filename: fileno (function) 每个函数响应信息
你也可以像timeit模块一样在命令行执行cProfile。主要的区别就是需要传递Python脚本而不是代码作为参数。
python3 -m cProfile test.py
尝试执行下该命令,看看执行结果。
line_profiler
有个第三方提供的项目叫line_profiler,它用来分析每行代码执行时间。它也有一个叫做kenprof的脚本使用line_profiler来分析Python应用和脚本。只需使用pip来安装该包。
pip install line_profiler
在使用line_profiler之前我们需要一些代码来分析。不过我向先告诉你命令行中如何调用line_profiler.实际上需要执行kernprof脚本来调用line_profiler。
kernprof -l silly_functions.py
执行完后将会打印出:Wrote profile results to silly_functions.py.lpro。该文件不能直接查看,是一个二进制文件。 当我们执行kernprof时,其实是在__builtins__命名空间中注入LineProfiler实例。该实例称为profile,用来作为装饰器函数。让我们来写实际脚本:
import time @profile def fast_function(): print("I‘m fast function!") @profile def slow_function(): time.sleep(2) print("I‘m slow function!") if __name__ == "__main__": fast_function() slow_function()
现在我们拥有使用了profile装饰的函数。如果你直接执行该脚本将会报NameError错误,因为并没有定义profile。 所以记得再实际使用时移除@profile装饰器函数。
我们来学习如果查看分析器的结果。有两种办法,第一使用line_profiler模块读结果文件:
$ python -m line_profiler silly_functions.py.lprof
还有种是在执行kernprof时带上-v参数:
$ kernprof -l -v silly_functions.py
无论哪种办法你都将得到如下结果:
I‘m fast function! I‘m slow function! Wrote profile results to silly_functions.py.lprof Timer unit: 1e-06 s Total time: 4e-05 s File: silly_functions.py Function: fast_function at line 3 Line # Hits Time Per Hit % Time Line Contents ============================================================== 3 @profile 4 def fast_function(): 5 1 40 40.0 100.0 print("I‘m fast function!") Total time: 2.00227 s File: silly_functions.py Function: slow_function at line 7 Line # Hits Time Per Hit % Time Line Contents ============================================================== 7 @profile 8 def slow_function(): 9 1 2002114 2002114.0 100.0 time.sleep(2) 10 1 157 157.0 0.0 print("I‘m slow function!")
我们可以看到结果中显示了每行代码的执行时间相关信息。有6列内容。我们来看看每列代表的意思。
- Line#: 分析的代码行数
- Hits: 执行次数
- Time: 该行代码执行时间
- Per Hit: 每次执行时间
- % Time: 函数中所占时间比率
- Line Contents: 执行的代码
如果你用IPython,你可能想知道IPython的魔术方法%lprun可以分析制定函数,甚至是具体代码。
memory_profiler
另一个用来分析的第三方包叫做memory_profiler.它用来分析每行代码在进程中所消耗的内存。我们也使用pip来安装它。
pip install memory_profiler
装好后,我们也需要代码来评测。memory_prifiler执行方式和line_profiler非常相似,它将在__builtins__注入profiler装饰器来测试函数。
@profile def mem_func(): lots_of_numbers = list(range(1500)) x = [‘letters‘] * (5 ** 10) del lots_of_numbers return None if __name__ == "__main__": mem_func()
本例中我们创建了1500整形list。然后我们又创建了9765625(5的10次方)字符串李彪。最后我们删除了第一个list并返回。memory_profiler没有提供另外一个脚本来让你执行该程序,而是直接通过-m参数来调用模块。
$ python -m memory_profiler memo_prof.py Filename: memo_prof.py Line # Mem usage Increment Line Contents ================================================ 1 13.133 MiB 0.000 MiB @profile 2 def mem_func(): 3 13.387 MiB 0.254 MiB lots_of_numbers = list(range(1500)) 4 87.824 MiB 74.438 MiB x = [‘letters‘] * (5 ** 10) 5 87.938 MiB 0.113 MiB del lots_of_numbers 6 87.938 MiB 0.000 MiB return None
结果的提供的信息就非常直观了。包含代码行数和改行执行完后的内存使用,接下来是内存增加量,最后是执行的代码。
memory_profiler也提供mprof用来创建完整的内存使用报表,而不是每行结果。
$ mprof run memo_prof.py mprof: Sampling memory every 0.1s running as a Python program...
mprof也提供来图示来查看应用内存消耗,需要查看图表前我们需要先安装matplotlib
$ pip install matplotlib
然后执行:
$ mprof plot
我们得到如下图:
profilehooks
最后个要介绍的第三方包是profilehooks.它拥有一系列包装器来分期函数。安装:
pip install profilehooks
安装完后,我们修改上次例子替换成profilehooks:
from profilehooks import profile @profile def mem_func(): lots_of_numbers = list(range(1500)) x = [‘letters‘] * (5 ** 10) del lots_of_numbers return None if __name__ == "__main__": mem_func()
仅仅要做的是导入profilehooks并且包装到要测试的函数中。执行后:
*** PROFILER RESULTS *** mem_func (profhooks.py:3) function called 1 times 2 function calls in 0.036 seconds Ordered by: cumulative time, internal time, call count ncalls tottime percall cumtime percall filename:lineno(function) 1 0.036 0.036 0.036 0.036 profhooks.py:3(mem_func) 1 0.000 0.000 0.000 0.000 {method ‘disable‘ of ‘_lsprof.Profiler‘ objects} 0 0.000 0.000 profile:0(profiler)
输出的结果和cProfile是一样的,你可以看看我之前对这些列的描述。profilehooks提供了两个装饰器。我们来看看第一个timecall,它提供了函数执行时间。
from profilehooks import timecall @timecall def mem_func(): lots_of_numbers = list(range(1500)) x = [‘letters‘] * (5 ** 10) del lots_of_numbers return None if __name__ == "__main__": mem_func()
执行后你将看到如下输出:
mem_func (profhooks2.py:3): 0.056 seconds
整个包装其输出整个函数执行时间,和timeit是一样的。
另一个profilehooks提供的装饰器是coverage。它用来打印函数每行代码涵盖数。不过我看来用处到不是很大,你可以自己试试:
*** COVERAGE RESULTS *** mem_func (profhooks3.py:3) function called 1 times @coverage def mem_func(): 1: lots_of_numbers = list(range(1500)) 1: x = [‘letters‘] * (5 ** 10) 1: del lots_of_numbers 1: return None
最后要告诉你的是,你也可以通过-m参数来从命令行中执行profilehooks.
python -m profilehooks mymodule.py
profilehooks是一个新包,不过我到觉得应该会很有前途。
总结
本文涵盖了需要信息。分享了如何使用Python内建模块,timeit和cProfile来测试和分期你的代码,我还学会了如何自己包装计时包装 器和上下文管理器。然后我们还学习了一些第三方包:line_profiler, memory_profiler和profilehooks。就目前,你已经学会如何分析你的代码,查找代码瓶颈。