当尝试优化缓慢的函数或模块时,通过性能分析可以更好地了解代码的性能瓶颈。在Python中,可以使用cProfile模块对代码进行快速的性能分析。下面是两个不同版本的代码片段,可用于对代码段进行性能分析。这些代码片段改编自cProfile文档中的“Profile”示例。多年来,我一直使用这些代码片段来缩小性能问题的范围。
基本版本
此版本适用于对模块级别代码进行性能分析。将以下两个代码块粘贴到目标代码周围:
import cProfile, pstats
profiler = cProfile.Profile()
profiler.enable()
...
profiler.disable()
pstats.Stats(profiler).sort_stats(pstats.SortKey.CUMULATIVE).print_stats(100)
第一个代码块创建并启动性能分析器。第二个代码块停止它并打印前100个函数,按累积调用时间排序。
例如,对一些类定义进行性能分析:
from django.db import models
import cProfile, io, pstats
profiler = cProfile.Profile()
profiler.enable()
class DanceKind(models.IntegerChoices):
FANDANGO = 1
TANGO = 2
class Dance(models.Model):
kind = models.IntegerField(choices=DanceKind.choices)
profiler.disable()
out = io.StringIO()
pstats.Stats(profiler).sort_stats(pstats.SortKey.CUMULATIVE).print_stats(100)
当模块导入时,您将看到来自print_stats()调用的输出:
$ python manage.py shell
18305 function calls (17540 primitive calls) in 0.044 seconds
Ordered by: cumulative time
List reduced from 552 to 100 due to restriction <100>
ncalls ... cumtime percall filename:lineno(function)
97/2 ... 0.044 0.022 {built-in method builtins.__build_class__}
1 ... 0.043 0.043 /.../django/db/models/base.py:95(__new__)
6 ... 0.043 0.007 /.../django/db/models/base.py:369(add_to_class)
1 ... 0.043 0.043 /.../django/db/models/options.py:175(contribute_to_class)
1 ... 0.043 0.043 /.../django/utils/connection.py:14(__getattr__)
1 ... 0.043 0.043 /.../django/utils/connection.py:56(__getitem__)
1 ... 0.043 0.043 /.../django/db/utils.py:191(create_connection)
1 ... 0.043 0.043 /.../django/db/utils.py:103(load_backend)
1 ... 0.043 0.043 /.../importlib/__init__.py:108(import_module)
2/1 ... 0.043 0.043 <frozen importlib._bootstrap>:1192(_gcd_import)
...
表格显示了函数的调用情况,以文件名、行号和名称为标识。它们按累积时间(“cumtime”)排序,即在该函数中花费的总时间(包括其被调用方),按最长时间优先排序。
请注意,如果两个代码块之间的代码段引发异常,则性能分析器将继续运行。通常不用担心这个问题,因为模块级别的异常通常会停止程序。
以下是已润色的文章:
上下文管理器和装饰器版本
这个版本适用于对函数或函数部分进行分析。通过使用 @contextlib.contextmanager,它既可以用作上下文管理器,也可以用作装饰器。
首先,将以下定义粘贴到目标模块中:
from contextlib import contextmanager
@contextmanager
def profile_and_print():
import cProfile, pstats
profiler = cProfile.Profile()
profiler.enable()
try:
yield
finally:
profiler.disable()
pstats.Stats(profiler).sort_stats(pstats.SortKey.CUMULATIVE).print_stats(100)
通过使用 try/finally,如果发生异常,分析器将始终处于禁用状态。只有在成功时才会打印配置文件,尽管您可以通过缩进最后一行来更改此设置。
要将其用作函数装饰器:
from django.shortcuts import render
from example.models import Dance
...
@profile_and_print()
def index(request):
context = {
"top_dancers": get_top_dancers(),
"total_dances": Dance.objects.count(),
}
return render(request, "index.html", context)
以及作为上下文管理器:
def index(request):
context = {
"top_dancers": get_top_dancers(),
"total_dances": Dance.objects.count(),
}
with profile_and_print():
return render(request, "index.html", context)
在任一情况下,运行分析代码块将再次为该部分打印分析结果:
4092 function calls (4012 primitive calls) in 0.008 seconds
Ordered by: cumulative time
List reduced from 376 to 100 due to restriction <100>
ncalls ... cumtime percall filename:lineno(function)
1 ... 0.008 0.008 /.../django/shortcuts.py:17(render)
1 ... 0.007 0.007 /.../django/template/loader.py:52(render_to_string)
1 ... 0.006 0.006 /.../django/template/loader.py:5(get_template)
...
可能的调整
以下是您可能想要修改此段代码的几种方式。
排序
要按其他指标排序,请使用从 此表 中选择 SortKey.CUMULATIVE 之外的其他值。最有用的是:
- SortKey.TIME,用于大多数内部时间,即在函数内部花费的时间,不包括对其他函数的调用。最慢的函数通常是优化的易发现目标。
- SortKey.CALLS,用于调用次数。可能有方法可以消除重复的函数调用,例如使用缓存。
显示
默认情况下,Stats 类将输出到标准输出。要输出到标准错误,请在创建时传递 stream=sys.stderr:
pstats.Stats(profiler, stream=sys.stderr).sort_stats(
pstats.SortKey.CUMULATIVE,
).print_stats(100)
要保存到文件,请将 print_stats() 替换为 dump_stats():
pstats.Stats(profiler).sort_stats(
pstats.SortKey.CUMULATIVE,
).dump_stats("profile.stats")
您可以使用可视化工具(如 tuna)使用此文件。
总结
在Python中,使用 cProfile 模块可以快速进行性能分析,以了解代码的性能问题。这篇文章提供了两个版本的代码片段,一个适用于对模块级别代码进行性能分析,另一个适用于对函数或函数部分进行性能分析。您可以按照代码片段的指示将其粘贴到目标代码周围,然后运行并查看分析结果。分析结果将按照累积调用时间排序,并且可以根据需要进行排序和显示。性能分析器将始终处于禁用状态,只有在成功时才会打印配置文件。