profile模块测试性能再优化程序
1.概述
当我们写好了程序后,想要测试下程序中某一部分运行的性能指标,python提供了profile模块可以在任意需要测试的代码上拿到测试的性能数据。
2.profile模块概述
python内置了两种profiler,一种是由profile模块提供的纯python版本,还有一种是由cProfile模块提供的C扩展版本。这个版本在执行测评过程中,对受测程序的影响比较小,测评结果更加准确。相反纯Python版本开销会比较大,测评结果有所偏差。
2.1.使用cProfile测评性能
下面使用cProfile模块提供的profile测评一个函数执行的性能指标,然后使用pstats模块提供的Stats统计输出测试结果。
'''普通的排序方式向列表中插入1万个整数'''
# Example 1
def insertion_sort(data):
result = []
for value in data:
# 使用普通的排序方式,向列表中插入数据
insert_value(result, value)
return result
# Example 2
# array是列表,value是向列表插入的数据
def insert_value(array, value):
# 获取列表中的值 i是索引,existing是值
for i, existing in enumerate(array):
# 如果列表中的这个值大于传入的值,则将value插入到列表i的位置,
# i上原本的值向后移动一位,实现从小到大排序
if existing > value:
array.insert(i, value)
return
array.append(value)
# Example 3
from random import randint
# 生成10000
max_size = 10**4
# 随机生成1万个整数赋值给data,for是为了循环1万次,randint是产生1万次随机整数,放到列表。
data = [randint(0, max_size) for _ in range(max_size)]
# 调用insertion_sort 排序
test = lambda: insertion_sort(data)
'''使用cProfile测评性能'''
# Example 4
from cProfile import Profile
profiler = Profile()
profiler.runcall(test)
'''使用Stats输出统计结果'''
# Example 5
from pstats import Stats
# 创建stats对象,将测试结果传入构造器
stats = Stats(profiler)
# 过滤无关的路径信息
stats.strip_dirs()
# 排序
stats.sort_stats('cumulative')
# 输出结果
stats.print_stats()
运行上面的示例代码,下面对测试输出结果做一个简单介绍。
- ncalls:测试期间调用函数次数
- tottime:执行这个被测函数本身花费的时间,不包含改函数调用其他函数花费时间。
- percall:执行这个被测函数花费的平均时间,tottime除以ncalls得到平均花费时间。
- cumtime:执行这个函数,以及该函数调用其他函数所花费的时间
- percall:执行这个函数,以及该函数调用其他函数花费的平均时间,cumtime除以ncalls得到平均花费时间
- filename:lineno:被执行的函数名称
从下面的统计结果可以从cumtime列看出,在执行insertion_sort所花费时间中有大部分时间都耗在了insert_value函数上,所以这个函数就是我们优化的重点。
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 2.015 2.015 no_70.py:31(<lambda>)
1 0.004 0.004 2.015 2.015 no_70.py:8(insertion_sort)
10000 1.999 0.000 2.012 0.000 no_70.py:16(insert_value)
9988 0.013 0.000 0.013 0.000 {method 'insert' of 'list' objects}
12 0.000 0.000 0.000 0.000 {method 'append' of 'list' objects}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
2.2.打印调用栈
如果我们测试的代码中调用了工具类函数,使用上面的方式输出的结果显示时间都耗在了工具类函数上,但是不能够证明是哪个业务调用工具类函数消耗的时间,因此我们需要打印它的调用栈信息。通过调用栈可以知道谁调用了工具类,花费了多少时间。
下面通过一个调用工具类函数示例展示,打印调用栈信息。
'''工具类函数'''
# Example 8
def my_utility(a, b):
c = 1
for i in range(100):
c += a * b
'''first_func,second_func 调用了工具类函数'''
def first_func():
for _ in range(1000):
my_utility(4, 5)
def second_func():
for _ in range(10):
my_utility(1, 3)
'''调用first_func,second_func函数间接调用工具类函数'''
def my_program():
for _ in range(20):
first_func()
second_func()
'''测试性能,输出结果'''
from sys import stdout as STDOUT
profiler = Profile()
profiler.runcall(my_program)
stats = Stats(profiler, stream=STDOUT)
stats.strip_dirs()
stats.sort_stats('cumulative')
stats.print_stats()
运行上面的代码输出了性能测试结果,从结果中看出大部分时间都耗在了my_utility工具类函数,他被调用了20200次。从这个结果中没有告诉我们为什么会被调用这么多次,都是被谁调用的。
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.175 0.175 no_70.py:67(my_program)
20 0.005 0.000 0.174 0.009 no_70.py:58(first_func)
20200 0.170 0.000 0.170 0.000 no_70.py:52(my_utility)
20 0.000 0.000 0.002 0.000 no_70.py:62(second_func)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
为了解决上面的问题,我们可以改用print_callers方法来统计打印结果,他会输出调用栈信息。
stats = Stats(profiler, stream=STDOUT)
stats.strip_dirs()
stats.sort_stats('cumulative')
# 改用print_callers 输出调用栈信息
stats.print_callers()
运行上面的代码,输出调用栈信息。下面输出信息中,左边是被调用函数,右边是主调用函数。这样就可以看到my_utility函数被两个函数调用,调用次数最多的是first_func函数。
Ordered by: cumulative time
Function was called by...
ncalls tottime cumtime
no_70.py:67(my_program) <-
no_70.py:58(first_func) <- 20 0.005 0.174 no_70.py:67(my_program)
no_70.py:52(my_utility) <- 20000 0.169 0.169 no_70.py:58(first_func)
200 0.002 0.002 no_70.py:62(second_func)
no_70.py:62(second_func) <- 20 0.000 0.002 no_70.py:67(my_program)
{method 'disable' of '_lsprof.Profiler' objects} <-