啥叫做基准测试(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。就目前,你已经学会如何分析你的代码,查找代码瓶颈。

如何对你的Python代码进行基准测试的更多相关文章

  1. 随机森林入门攻略(内含R、Python代码)

    随机森林入门攻略(内含R.Python代码) 简介 近年来,随机森林模型在界内的关注度与受欢迎程度有着显著的提升,这多半归功于它可以快速地被应用到几乎任何的数据科学问题中去,从而使人们能够高效快捷地获 ...

  2. 改改Python代码,运行速度还能提升6万倍

    这份最新研究指出,在后摩尔定律时代,人类所获得的的算力提升将更大程度上来源于计算堆栈的「顶层」,即软件.算法和硬件架构,这将成为一个新的历史趋势. 很多人学习python,不知道从何学起.很多人学习p ...

  3. 可爱的豆子——使用Beans思想让Python代码更易维护

    title: 可爱的豆子--使用Beans思想让Python代码更易维护 toc: false comments: true date: 2016-06-19 21:43:33 tags: [Pyth ...

  4. if __name__== "__main__" 的意思(作用)python代码复用

    if __name__== "__main__" 的意思(作用)python代码复用 转自:大步's Blog  http://www.dabu.info/if-__-name__ ...

  5. Python 代码风格

    1 原则 在开始讨论Python社区所采用的具体标准或是由其他人推荐的建议之前,考虑一些总体原则非常重要. 请记住可读性标准的目标是提升可读性.这些规则存在的目的就是为了帮助人读写代码,而不是相反. ...

  6. 一行python代码实现树结构

    树结构是一种抽象数据类型,在计算机科学领域有着非常广泛的应用.一颗树可以简单的表示为根, 左子树, 右子树. 而左子树和右子树又可以有自己的子树.这似乎是一种比较复杂的数据结构,那么真的能像我们在标题 ...

  7. [Dynamic Language] 用Sphinx自动生成python代码注释文档

    用Sphinx自动生成python代码注释文档 pip install -U sphinx 安装好了之后,对Python代码的文档,一般使用sphinx-apidoc来自动生成:查看帮助mac-abe ...

  8. 上传自己的Python代码到PyPI

    一.需要准备的事情 1.当然是自己的Python代码包了: 2.注册PyPI的一个账号. 二.详细介绍 1.代码包的结构: application \application __init__.py m ...

  9. 如何在batch脚本中嵌入python代码

    老板叫我帮他测一个命令在windows下消耗的时间,因为没有装windows那个啥工具包,没有timeit那个命令,于是想自己写一个,原理很简单: REM timeit.bat echo %TIME% ...

随机推荐

  1. 放大的X 【杭电-2655】 附题

    /* 放大的X Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Su ...

  2. Jenkins2.x Pipeline持续集成交互

    原文地址:http://blog.csdn.net/aixiaoyang168/article/details/72818804 Pipeline的几个基本概念: Stage: 阶段,一个Pipeli ...

  3. redis主从和主从切换

    redis数据量增加,导致内存不够用,要迁移分离redis和程序: 1. 在新redis服务器上,启动一个redis实例,配置和master配置一致,不同的是配置文件中修改并启用 slave-read ...

  4. javaweb项目开发错误代码

    HTTP状态码(HTTP Status Code) 一些常见的状态码为:200 - 服务器成功返回网页 404 - 请求的网页不存在 503 - 服务不可用 所有状态解释:点击查看 1xx(临时响应) ...

  5. Google I/O 2014 大会总结 Android开发新方向

    昨天晚上,Google I/O 2014大会召开,会上主要展示了下面几个部分的创新内容: Android L 操作系统 首先是界面,谷歌又一次设计了一套 UI 规范.并称之为"Materia ...

  6. 1.const

    在C++中,const 的含义并没有改变,只是对细节进行了一些调整,以下是最主要的两点. 一.C++中的 const 更像编译阶段的 #define 先来看下面的两条语句: ; int n = m; ...

  7. C语言基础(13)-函数

    一. 函数的原型和调用 在使用函数前必须定义或者声明函数. double circle(double r); int main() { ); printf("length = %f\n&qu ...

  8. MySQL 汉字转拼音

    一 . fristPinyin : 此函数是将一个中文字符串的第一个汉字转成拼音字母 (例如:"中国人"->Z) )) ) CHARSET utf8 BEGIN ); )), ...

  9. Tomcat闲聊

    上面是Tomcat的执行过程. 下面说几个概念: 1.JSP:这个就是在html文件中嵌入Java代码之后重新生成的一个文件(.jsp的文件) 2.servlet:就是一个处理动态资源的小程序,一般位 ...

  10. collection动画

    Collection View 动画 吴迪 12 May 2014 分享文章 UICollectionView 和相关类的设置非常灵活和强大.但是灵活性一旦增强,某种程度上也增加了其复杂性: UICo ...