python分析程序

源代码: Lib/profile.pyLib/pstats.py


简介

cProfileprofile 提供 deterministic profiling 关于python程序。一 profile 是一组统计数据,描述程序各个部分执行的频率和时间。这些统计信息可以通过 pstats 模块。

python标准库提供了同一分析接口的两种不同实现:

  1. cProfile 建议大多数用户使用;它是一个具有合理开销的C扩展,适合分析长时间运行的程序。基于 lsprof 由布雷特·罗森和特德·夏洛特贡献。

  2. profile ,一个纯python模块,其接口被模仿 cProfile 但这会给被分析的程序增加大量开销。如果您试图以某种方式扩展探查器,那么使用此模块可能会更容易完成任务。最初由Jim Roskind设计和编写。

注解

探查器模块的设计目的是为给定的程序提供执行概要,而不是为了基准测试(为此,存在 timeit 以获得合理准确的结果)。这尤其适用于将Python代码与C代码进行基准测试:分析程序引入了Python代码的开销,但没有引入C级函数,因此C代码看起来比任何Python代码都要快。

即时用户手册

本节提供给“不想阅读本手册”的用户。它提供了非常简短的概述,并允许用户快速对现有应用程序执行分析。

要分析接受单个参数的函数,可以执行以下操作:

import cProfile
import re
cProfile.run('re.compile("foo|bar")')

(使用) profile 而不是 cProfile 如果您的系统上没有后者。)

以上操作将运行 re.compile() 打印配置文件结果如下:

      197 function calls (192 primitive calls) in 0.002 seconds

Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1    0.000    0.000    0.001    0.001 <string>:1(<module>)
     1    0.000    0.000    0.001    0.001 re.py:212(compile)
     1    0.000    0.000    0.001    0.001 re.py:268(_compile)
     1    0.000    0.000    0.000    0.000 sre_compile.py:172(_compile_charset)
     1    0.000    0.000    0.000    0.000 sre_compile.py:201(_optimize_charset)
     4    0.000    0.000    0.000    0.000 sre_compile.py:25(_identityfunction)
   3/1    0.000    0.000    0.000    0.000 sre_compile.py:33(_compile)

第一行表示监控了197个调用。在这些调用中,192个是 primitive ,表示调用不是通过递归诱导的。下一行: Ordered by: standard name ,指示使用最右列中的文本字符串对输出进行排序。列标题包括:

NCALL

调用号码。

总计时间

在给定函数中花费的总时间(不包括调用子函数的时间)

佩雷尔

是的商 tottime 除以 ncalls

累积时间

是在这个和所有子函数中花费的累计时间(从调用到退出)。这个数字是准确的 even 对于递归函数。

佩雷尔

是的商 cumtime 除以原始调用

文件名:lineno(函数)

提供每个函数的各自数据

当第一列中有两个数字时(例如 3/1 ,表示该函数循环。第二个值是基元调用数,前者是调用总数。注意,当函数不重复出现时,这两个值是相同的,并且只打印一个数字。

不必在配置文件运行结束时打印输出,您可以通过将文件名指定给 run() 功能:

import cProfile
import re
cProfile.run('re.compile("foo|bar")', 'restats')

这个 pstats.Stats 类从文件中读取配置文件结果,并以各种方式对其进行格式化。

档案 cProfileprofile 也可以作为脚本调用以分析另一个脚本。例如::

python -m cProfile [-o output_file] [-s sort_order] (-m module | myscript.py)

-o 将配置文件结果写入文件而不是stdout

-s 指定 sort_stats() 排序值以对输出进行排序。这只适用于 -o 未提供。

-m 指定正在分析模块而不是脚本。

3.7 新版功能: 增加了 -m 选择权 cProfile .

3.8 新版功能: 增加了 -m 选择权 profile .

这个 pstats 模块的 Stats 类具有多种方法来操作和打印保存到配置文件结果文件中的数据::

import pstats
from pstats import SortKey
p = pstats.Stats('restats')
p.strip_dirs().sort_stats(-1).print_stats()

这个 strip_dirs() 方法从所有模块名称中删除了无关路径。这个 sort_stats() 方法根据打印的标准模块/行/名称字符串对所有条目进行排序。这个 print_stats() 方法打印出所有统计数据。您可以尝试以下排序调用:

p.sort_stats(SortKey.NAME)
p.print_stats()

第一次调用将按函数名对列表进行排序,第二次调用将打印出统计信息。下面是一些有趣的实验调用:

p.sort_stats(SortKey.CUMULATIVE).print_stats(10)

这将按函数中的累积时间对配置文件进行排序,然后只打印最重要的十行。如果你想了解什么算法需要时间,上面这一行就是你要使用的。

如果您要查看哪些函数循环了很多时间,并且花费了大量时间,那么您可以执行以下操作:

p.sort_stats(SortKey.TIME).print_stats(10)

根据在每个函数中花费的时间进行排序,然后打印前十个函数的统计信息。

您也可以尝试:

p.sort_stats(SortKey.FILENAME).print_stats('__init__')

这将按文件名对所有统计信息进行排序,然后只打印类init方法的统计信息(因为它们的拼写为 __init__ 在他们里面)。作为最后一个例子,您可以尝试:

p.sort_stats(SortKey.TIME, SortKey.CUMULATIVE).print_stats(.5, 'init')

此行使用时间的主键和累积时间的次键对统计信息进行排序,然后打印出一些统计信息。具体来说,该列表首先被剔除到50%(回复: .5 )其原始大小,然后仅包含 init 并打印该子列表。

如果您想知道什么函数称为上述函数,那么现在可以 (p 仍按最后一个条件排序)do::

p.print_callers(.5, 'init')

您将得到每个列出的函数的调用者列表。

如果您想要更多的功能,您必须阅读手册,或者猜测以下功能的作用:

p.print_callees()
p.add('restats')

作为脚本调用, pstats 模块是用于读取和检查配置文件转储的统计浏览器。它有一个简单的面向行的接口(使用 cmd )以及交互式帮助。

profilecProfile 模块参考

两个 profilecProfile 模块提供以下功能:

profile.run(command, filename=None, sort=- 1)

此函数接受一个可以传递给 exec() 函数和可选文件名。在所有情况下,此例程执行:

exec(command, __main__.__dict__, __main__.__dict__)

并从执行中收集分析统计信息。如果没有文件名,则此函数将自动创建 Stats 实例并打印一个简单的分析报告。如果指定了排序值,则将其传递给 Stats 实例来控制结果的排序方式。

profile.runctx(command, globals, locals, filename=None, sort=- 1)

此功能类似于 run() ,并添加参数以提供 命令 字符串。此例程执行:

exec(command, globals, locals)

并像在 run() 以上功能。

class profile.Profile(timer=None, timeunit=0.0, subcalls=True, builtins=True)

此类通常仅在需要比 cProfile.run() 功能提供。

可以提供一个自定义计时器,用于测量通过 计时器 参数。这必须是一个返回表示当前时间的单个数字的函数。如果数字是整数,则 时间单位 指定一个乘数,用于指定每个时间单位的持续时间。例如,如果计时器返回以千秒为单位测量的时间,则时间单位为 .001 .

直接使用 Profile 类允许在不将配置文件数据写入文件的情况下格式化配置文件结果:

import cProfile, pstats, io
from pstats import SortKey
pr = cProfile.Profile()
pr.enable()
# ... do something ...
pr.disable()
s = io.StringIO()
sortby = SortKey.CUMULATIVE
ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
ps.print_stats()
print(s.getvalue())

这个 Profile 类也可以用作上下文管理器(仅在 cProfile 模块。看见 上下文管理器类型 ):

import cProfile

with cProfile.Profile() as pr:
    # ... do something ...

pr.print_stats()

在 3.8 版更改: 添加了上下文管理器支持。

enable()

开始收集分析数据。只有在 cProfile .

disable()

停止收集分析数据。只有在 cProfile .

create_stats()

停止收集分析数据,并在内部将结果记录为当前配置文件。

print_stats(sort=- 1)

创建一个 Stats 对象,并将结果打印到stdout。

dump_stats(filename)

将当前配置文件的结果写入 filename .

run(cmd)

通过分析命令 exec() .

runctx(cmd, globals, locals)

通过分析命令 exec() 具有指定的全局和本地环境。

runcall(func, /, *args, **kwargs)

简况 func(*args, **kwargs)

注意,只有当被调用的命令/函数实际返回时,分析才会工作。如果译员被终止(例如通过 sys.exit() 在被调用的命令/函数执行期间调用)不会打印分析结果。

这个 Stats 等级

分析器数据的分析是使用 Stats 类。

class pstats.Stats(*filenames or profile, stream=sys.stdout)

此类构造函数从 filename (或文件名列表)或 Profile 实例。输出将打印到由指定的流 流动 .

由上述构造函数选择的文件必须由相应版本的 profilecProfile .具体来说,有 no 此探查器的未来版本保证了文件兼容性,并且与其他探查器生成的文件不兼容,或者同一个探查器在不同的操作系统上运行。如果提供多个文件,则相同函数的所有统计信息都将合并,以便在单个报告中考虑多个进程的总体视图。如果需要将其他文件与现有 Stats 对象 add() 可以使用方法。

不是从文件中读取配置文件数据,而是 cProfile.Profileprofile.Profile 对象可以用作配置文件数据源。

Stats 对象具有以下方法:

strip_dirs()

此方法用于 Stats 类从文件名中删除所有前导路径信息。它对于减小打印输出的大小以适应(接近)80列非常有用。此方法修改对象,剥离的信息将丢失。在执行一个条带操作之后,对象被认为是以“随机”的顺序包含其条目,就像在对象初始化和加载之后一样。如果 strip_dirs() 使两个函数名不可区分(它们位于同一文件名的同一行,并且具有相同的函数名),然后将这两个条目的统计信息累积到一个条目中。

add(*filenames)

这种方法的 Stats 类将其他分析信息累积到当前分析对象中。其参数应引用由相应版本的 profile.run()cProfile.run() . 相同名称(re:文件、行、名称)函数的统计信息自动累积到单个函数统计信息中。

dump_stats(filename)

将加载的数据保存到 Stats 对象到名为 filename . 如果文件不存在,则创建该文件;如果文件已存在,则覆盖该文件。这相当于 profile.ProfilecProfile.Profile 类。

sort_stats(*keys)

此方法修改 Stats 对象,根据提供的条件对其进行排序。参数可以是字符串或sortkey枚举,用于标识排序的基础(例如: 'time''name'SortKey.TIMESortKey.NAME )。sortkey枚举参数比字符串参数有优势,因为它更健壮,更不容易出错。

如果提供了多个键,则当前面选择的所有键都相等时,将使用其他键作为辅助条件。例如, sort_stats(SortKey.NAME, SortKey.FILE) 将根据其函数名对所有条目排序,并通过文件名排序来解析所有绑定(相同的函数名)。

对于字符串参数,只要缩写词不含糊,缩写词就可以用于任何键名称。

以下是有效的字符串和sortkey:

有效字符串参数

有效枚举

意义

'calls'

SortKey.CALLS

调用计数

'cumulative'

SortKey.CUMULATIVE

累积时间

'cumtime'

不适用

累积时间

'file'

不适用

文件名

'filename'

SortKey.FILENAME

文件名

'module'

不适用

文件名

'ncalls'

不适用

调用计数

'pcalls'

SortKey.PCALLS

原始调用计数

'line'

SortKey.LINE

行号

'name'

SortKey.NAME

函数名

'nfl'

SortKey.NFL

name/file/line

'stdname'

SortKey.STDNAME

标准名称

'time'

SortKey.TIME

内部时间

'tottime'

不适用

内部时间

请注意,统计信息的所有排序都是按降序排列的(首先放置最耗时的项目),其中,按名称、文件和行号搜索是按升序排列的(按字母顺序)。两者之间的细微差别 SortKey.NFLSortKey.STDNAME 标准名称是一种打印出来的名称,这意味着嵌入的行号以一种奇怪的方式进行比较。例如,第3、20和40行(如果文件名相同)将按字符串顺序20、3和40显示。相反, SortKey.NFL 对行号进行数值比较。事实上, sort_stats(SortKey.NFL) 是一样的 sort_stats(SortKey.NAME, SortKey.FILENAME, SortKey.LINE) .

由于向后兼容的原因,数字参数 -1012 是允许的。它们被解释为 'stdname''calls''time''cumulative' 分别。如果使用此旧样式格式(数字),则只使用一个排序键(数字键),其他参数将被静默忽略。

3.7 新版功能: 添加了sortkey枚举。

reverse_order()

此方法用于 Stats 类反转对象中基本列表的顺序。请注意,默认情况下,升序和降序是根据选择的排序键正确选择的。

print_stats(*restrictions)

此方法用于 Stats 类打印报告,如中所述 profile.run() 定义。

打印顺序基于最后一个 sort_stats() 对对象进行的操作(需遵守 add()strip_dirs()

提供的参数(如果有)可用于将列表限制为重要条目。最初,列表被视为一组完整的概要函数。每个限制要么是一个整数(选择行数),要么是一个介于0.0和1.0之间(包括0.0和1.0)的小数(选择行的百分比),要么是一个将被解释为正则表达式的字符串(模式与打印的标准名称匹配)。如果提供了几个限制,则按顺序应用这些限制。例如::

print_stats(.1, 'foo:')

首先将打印限制在列表的前10%,然后仅打印属于文件名的函数 .*foo: . 相反,命令:

print_stats('foo:', .1)

将列表限制为具有文件名的所有函数 .*foo: ,然后继续只打印前10%的内容。

print_callers(*restrictions)

此方法用于 Stats 类打印一个列表,其中列出了在概要数据库中调用每个函数的所有函数。订单与 print_stats() ,并且限制参数的定义也是相同的。每个调用者都在自己的线路上报告。根据生成统计信息的探查器,格式略有不同:

  • profile ,在每个调用方之后的括号中显示一个数字,以显示进行了多少次此特定调用。为了方便起见,第二个无括号的数字会重复右侧函数所花费的累计时间。

  • cProfile ,每个调用方前面都有三个数字:进行此特定调用的次数,以及此特定调用方调用当前函数时在该函数中花费的总时间和累计时间。

print_callees(*restrictions)

此方法用于 Stats 类打印由指定函数调用的所有函数的列表。除了调用方向的反转(re:called vs was called by),参数和顺序与 print_callers() 方法。

get_stats_profile()

此方法返回StatsProfile的一个实例,其中包含函数名到FunctionProfile实例的映射。每个FunctionProfile实例都包含与函数的配置文件相关的信息,如函数运行所用的时间、调用它的次数等。。。

3.9 新版功能: 添加了以下数据类:StatsProfile、FunctionProfile。添加了以下函数:get_stats_profile。

什么是确定性分析?

Deterministic profiling 是为了反映这样一个事实 函数调用函数返回exception 对事件进行监控,并对这些事件之间的间隔进行精确计时(在此期间用户代码正在执行)。相反, statistical profiling (本模块不做)随机抽取有效的指令指针,并推断出花费时间的位置。后一种技术传统上需要较少的开销(因为不需要对代码进行检测),但只提供了花费时间的相对指示。

在Python中,由于在执行期间有一个解释器处于活动状态,因此不需要存在插入指令的代码来进行确定性分析。python自动提供 hook (可选回调)每个事件。此外,python的解释性质往往会给执行增加太多的开销,因此确定性分析只会在典型应用程序中增加很小的处理开销。结果是,确定性分析并没有那么昂贵,但是提供了大量关于Python程序执行的运行时统计信息。

调用计数统计信息可用于识别代码中的错误(意外计数),以及识别可能的内联扩展点(高调用计数)。内部时间统计数据可用于识别应仔细优化的“热循环”。累积时间统计应用于识别算法选择中的高级错误。请注意,此探查器中对累积时间的异常处理允许将算法的递归实现的统计信息直接与迭代实现进行比较。

局限性

一个限制与定时信息的准确性有关。确定性轮廓仪存在一个涉及精度的基本问题。最明显的限制是,底层的“时钟”只以大约0.001秒的速度(通常)滴答作响。因此,没有比底层时钟更精确的测量了。如果进行了足够的测量,那么“误差”就会趋于平均。不幸的是,删除第一个错误会导致第二个错误源。

第二个问题是,从分派事件到探查器调用获取实际时间,“需要一段时间”。 gets 时钟的状态。同样,从获取时钟值(然后将其存起来)到用户代码再次执行,退出探查器事件处理程序时存在一定的延迟。因此,多次调用或调用多个函数的函数通常会累积此错误。以这种方式累积的误差通常小于时钟的精度(小于一个时钟滴答),但是 can 积累并变得非常重要。

问题更重要的是 profile 较低的开销 cProfile . 因此, profile 提供了一种为给定平台校准自身的方法,以便可以概率地(平均地)消除此错误。校正轮廓仪后,它会更准确(至少在平方意义上),但有时会产生负数(当调用计数非常低,概率之神对你不利时为-)。做 not 请注意配置文件中的负数。他们应该 only 如果已校准轮廓仪,并且结果实际上比未校准的结果更好,则显示。

校准

的探查器 profile 模块从每个事件处理时间中减去一个常量,以补偿调用时间函数的开销,并将结果存储起来。默认情况下,常量为0。以下程序可用于获得给定平台的更好常数(参见 局限性 )。地址:

import profile
pr = profile.Profile()
for i in range(5):
    print(pr.calibrate(10000))

该方法直接在探查器下执行参数给定的Python调用数,并测量这两个调用的时间。然后,它计算每个探查器事件的隐藏开销,并将其作为浮点返回。例如,在运行Mac OS X的1.8GHz Intel Core i5上,使用python的time.process_time()作为计时器,神奇的数字大约是4.04e-6。

这个练习的目的是得到一个相当一致的结果。如果您的计算机是 very 快速,或者你的计时器功能分辨率很差,你可能要通过100000,甚至1000000,才能得到一致的结果。

当你有一个一致的答案时,有三种方法可以使用它:

import profile

# 1. Apply computed bias to all Profile instances created hereafter.
profile.Profile.bias = your_computed_bias

# 2. Apply computed bias to a specific Profile instance.
pr = profile.Profile()
pr.bias = your_computed_bias

# 3. Specify computed bias in instance constructor.
pr = profile.Profile(bias=your_computed_bias)

如果你有选择的话,你最好选择一个较小的常量,然后你的结果“不太经常”会在配置文件统计中显示为负数。

使用自定义计时器

如果要更改当前时间的确定方式(例如,强制使用墙时钟时间或经过的处理时间),请将所需的计时函数传递给 Profile 类构造函数::

pr = profile.Profile(your_time_func)

然后,生成的探查器将调用 your_time_func . 取决于您是否使用 profile.ProfilecProfile.Profileyour_time_func 的返回值将以不同的方式解释:

profile.Profile

your_time_func 应该返回一个数字,或者一个和当前时间的数字列表(比如 os.times() 返回)。如果函数返回单个时间数字,或者返回的数字列表的长度为2,那么您将得到一个特别快的调度例程版本。

请注意,您应该为您选择的计时器函数校准探查器类(请参见 校准 )对于大多数机器,返回一个单独整数值的计时器将在分析期间以较低的开销提供最佳结果。 (os.times()漂亮的 错误,因为它返回一个浮点值的元组)。如果您希望以最干净的方式替换一个更好的计时器,请派生一个类并硬连接一个最能处理计时器调用的替换调度方法,以及适当的校准常量。

cProfile.Profile

your_time_func 应返回单个数字。如果它返回整数,还可以使用第二个参数调用类构造函数,该参数指定一个时间单位的实际持续时间。例如,如果 your_integer_time_func 返回以千秒为单位测量的时间,您将构造 Profile 实例如下:

pr = cProfile.Profile(your_integer_time_func, 0.001)

作为 cProfile.Profile 类不能被校准,自定义计时器函数应该小心使用,并且应该尽可能快。为了使用自定义计时器获得最佳结果,可能需要在内部的C源代码中对其进行硬编码。 _lsprof 模块。

python 3.3在 time 可以用来精确测量工艺或壁钟时间。例如,请参见 time.perf_counter() .