一 、背景
在开发中,执行某程序时遇见一种现象,随着时间执行越久,执行某个方法的时间在不断变长。为了有效定位是哪个方法,特此去学习了下Python性能分析的方法。
二 、cProfile模块
cProfile是Python的默认的性能分析器,主要用于测量CPU的耗时,不关注内存的消耗。通过cProfile可以分析每一个函数的执行时间和执行次数,进而对函数进行时间上的优化。话不多说,先上代码,如下是基于cProfile写的性能分析装饰器。
定义分析结果的存储路径:
1 2
| file_dir = os.path.dirname(os.path.abspath(__file__)) file_base = os.path.join(file_dir, 'analysis')
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| def time_analysis(filename: t.Optional[str] = None) -> t.Any: """ 代码执行过程性能分析 将分析结果保存到指定文件 如未指定文件名,则使用原始函数名 """
sort_by = 'tottime' time_base = os.path.join(file_base, 'time_analysis')
def wrapper(func): create_dir(time_base)
@functools.wraps(func) def inner(*args, **kwargs): open_profile = os.environ.get('PROFILE') true_filename = filename or func.__name__ file_path = os.path.join(time_base, true_filename) if open_profile: profile = cProfile.Profile() profile.enable() res = func(*args, **kwargs) profile.disable() ps = pstats.Stats(profile).sort_stats(sort_by) ps.dump_stats(file_path) else: res = func(*args, **kwargs) return res
return inner
return wrapper
|
函数说明:
上述程序中使用的profile.print_stats()将分析结果打印到控制台,ps.dump_stats(file_path)将分析结果以二进制的形式持久化到文件中。这里我是将每个函数的分析结果都保存对应的文件夹下,当装饰器工厂函数不带参数,则保存的文件名默认为函数名,反之保存的为文件名为传进来的参数filename。
测试用例:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @performance_analysis() def test(): return 'my name is syz'
try: if not os.environ.get('PROFILE'): os.environ['PROFILE'] = 'True' res = test() except Exception as e: print(e) finally: os.environ.pop('PROFILE', None)
|
结果:
1 2 3 4 5 6 7
| 2 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 performance_analysis.py:64(test) 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
|
说明:
1.ncalls: 函数调用的次数
2.tottime: 函数的总的运行时间,除掉函数中调用子函数的运行时间
3.percall: (第一个 percall)等于tottime/ncalls
4.cumtime: 函数及其所有子函数的调用运行的时间,即函数开始调用到返回的时间
5.percall: (第二个 percall)即函数运行一次的平均时间,等于 cumtime/ncalls
6.filename: lineno(function):每个函数调用的具体信息
因此要分析哪一个函数耗时,应该关注tottime产生的时间。这样便可以容易地定位到耗时的函数,进行优化。
三 、 memory_profiler
memory_profiler模块用于按行分析python程序执行过程中所增减的内存量,适用于分析出导致内存溢出的源头。
安装模块:pip install memory_profile
1 2 3 4 5 6 7
| @profile def test_func(): a = ['a'] * 1024 * 1024 * 100 b = list(['nihao']) a = 'a' * 1024 * 1024 del a test_func()
|
分析结果:
1 2 3 4 5 6 7 8
| Line ============================================================ 33 65.8 MiB 65.8 MiB 1 @profile 34 def test_func(): 35 865.8 MiB 800.0 MiB 1 a = ['a'] * 1024 * 1024* 100 36 865.8 MiB 0.0 MiB 1 b = list(['nihao']) 37 66.8 MiB -799.0 MiB 1 a = 'a' * 1024*1024 38 65.8 MiB -1.0 MiB 1 del a
|
通过结果可以看到,对于同名变量的赋值,不会产生新的内存,而新值会替代旧值,在a指针指向的原内存块上进行覆盖,但是对不同变量,则会在内存中开辟其他内存块。
接下来,对profile装饰器再封装下,提供性能分析开启开关,通过设置环境变量开启,以及分析结果文件归档。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| def memory_analysis(filename: t.Optional[str] = None) -> t.Any: """ 每行代码的内存消耗分析 将分析结果保存到指定文件 如未指定文件名,则使用原始函数名 """ memory_base = os.path.join(file_base, 'memory_analysis')
def wrapper(func): create_dir(memory_base)
@functools.wraps(func) def inner(*args, **kwargs): open_profile = os.environ.get('PROFILE') true_filename = filename or func.__name__ file_path = os.path.join(memory_base, true_filename) if open_profile: with open(file_path, 'w', encoding='utf-8') as f: res = memory_profiler.profile(func, stream=f)(*args, **kwargs) else: res = func(*args, **kwargs) return res
return inner
return wrapper
|
四 、读取分析结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| def read_analysis_res(filename: str, mode: str) -> None: """ 读取分析结果 mode: time or memory """ assert mode in ['time', 'memory'], "请在time|memory中选择模式"
file_path = os.path.join(os.path.join(file_base, f'{mode}_analysis', filename)) if mode == 'time': ps = pstats.Stats(file_path) ps.strip_dirs().sort_stats(-1).print_stats() else: with open(file_path, 'r', encoding='utf-8') as f: sys.stdout.write(f.read())
|
说明:传入文件名以及分析模式(时间性能or内存开销), 将结果显示在控制台。