登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  python教程

Python profile 和 cProfile 的计时开销有什么区别

来源:17golang原创

时间:2026-10-29 18:29:45 182浏览 收藏

profile 的纯 Python 实现会给被测程序带来较明显的事件处理开销;cProfile 使用 C 扩展,通常更适合日常定位热点。但“开销较低”不等于零开销,也不能给出对所有程序通用的减速倍数。两者报表用于找时间花在哪里,比较代码方案快慢应另做无剖析计时。

Python 剖析工具官方文档:https://docs.python.org/3.14/library/profile.html

本文示例明确采用 CPython 3.14 的接口。较新的 Python 文档对工具组织和弃用状态已有调整:当前 profile 文档 标注其自 3.15 弃用、计划在 3.17 移除,并说明 cProfile 作为兼容别名保留。运行环境不同,应先看对应版本文档,不把本例当作所有版本的长期部署方案。

先把小项目的观察目标说清楚

一个文本汇总脚本可能没有复杂算法,却反复调用小函数。给它加上剖析器后,总耗时可能发生明显变化。如果据此认定业务函数突然变慢,就混入了观察工具自身的成本。

我会把这类判断拆为两项:先用 cProfile 看调用结构和高累计耗时函数,再脱离剖析器测原任务。这里表达的是工具选择方法,并非声称做过本文这组实测。小项目只输出输入一致的校验值、外部经过时间和函数报表,不预填任何性能数字。

同一业务任务与两种剖析器及外部计时边界
结构说明图:profile 与 cProfile 是替代的观察方式,外部计时覆盖任务和剖析开销;连线不表示执行先后。

用同一个任务接入两种剖析器

保存为 profiler_probe.py。代码为本文原创,未在制作过程中执行;读者运行时得到的数据才是对应环境的观察值。

import argparse
import cProfile
import platform
import profile
import pstats
import time

def token_score(text):
    # 刻意保留小函数调用,以便观察调用密集型任务。
    return len(text.strip().casefold())

def summarize(lines):
    total = 0
    for text in lines:
        total += token_score(text)
    return total

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("mode", choices=("baseline", "profile", "cprofile"))
    args = parser.parse_args()
    # 所有模式共用相同输入;数据准备与导入不在观察区间内。
    lines = (" Alpha ", "Beta", " GAMMA ") * 20000
    profiler = None
    if args.mode == "profile":
        profiler = profile.Profile()
    elif args.mode == "cprofile":
        profiler = cProfile.Profile()

    start = time.perf_counter()
    if profiler is None:
        result = summarize(lines)
    else:
        # 两个类都支持 runcall,避免误用 profile 不支持的 enable。
        result = profiler.runcall(summarize, lines)
    elapsed = time.perf_counter() - start
    print(platform.python_implementation(), platform.python_version())
    print("mode=", args.mode, "checksum=", result, "elapsed_s=", elapsed)
    if profiler is not None:
        # 报表格式化放到外部计时结束后,单独观察函数累计时间。
        pstats.Stats(profiler).strip_dirs().sort_stats(
            pstats.SortKey.CUMULATIVE
        ).print_stats(8)

if __name__ == "__main__":
    main()
# 在同一 CPython 3.14 环境分别运行;每条命令建立独立进程。
python profiler_probe.py baseline
python profiler_probe.py profile
python profiler_probe.py cprofile

输入构造、模块导入与报表打印都放在外部计时区间之外,便于围绕一次 summarize 调用讨论。剖析器对象也提前创建;因此这不是整进程启动时间比较。runcall 的包装开销仍计入带剖析器的外部耗时,正是此处希望观察的部分。

checksum 用来提示读者三个模式是否仍处理同一个任务,不是性能数据。某次结果不同,应先解释输入或逻辑差异,再谈耗时。单轮很短时不要把小数位差异直接解释为真实改进,也不要把一条结果当作工具固定开销。

解释开销为什么不只是一次读时钟

剖析器需要响应函数调用、返回等事件并维护统计。profile 在 Python 层处理这些工作,cProfile 的核心由 C 实现,成本不同。任务的函数调用密度也会影响观察扰动;大量短函数与少量长调用不能简单套同一个比例。这与 3.14 官方介绍 对两种实现的定位一致。

外部耗时与剖析报表内部统计的含义关系
结构说明图:外部经过时间与函数内部、累计时间的口径不同;图中没有实测数值或性能排名。
观察项用途不应作出的推论
外部 elapsed_s观察该调用区间的整体经过时间等同于某一行报表的时间
ncalls观察调用数量调用最多就是最值得优化
tottime关注函数自身耗时包含其全部子调用成本
cumtime关注函数连同子调用的累计耗时各行相加等于总耗时

报表中上层与下层的累计时间会覆盖同一段调用,所以不能把 cumtime 列逐行累加为总量。本文没有给两个剖析器强行配置相同自定义时钟;外部统一使用 perf_counter,只保证外部观察区间的时钟口径一致,不能由此声称内部记账误差也完全相同。

把速度判断放回无剖析测量

如果目标是判断修改后的 summarize 是否更快,就把被测代码放在不启用任何剖析器的环境中。下面单独保存为 timing_probe.py,与前一个文件放在一起:

import timeit
from profiler_probe import summarize

# 使用同一份输入,脱离剖析器再观察任务耗时;不自动证明优化有效。
lines = (" Alpha ", "Beta", " GAMMA ") * 20000
samples = timeit.repeat(lambda: summarize(lines), number=10, repeat=5)
print("每轮十次调用的秒数:", samples)

根据 timeit 官方说明,repeat 返回多次计时结果,number 是每轮执行次数。此处每项是十次调用的总秒数,不是单次耗时;若展示单次值,应除以十并标明单位。timeit 默认暂时关闭垃圾回收,若任务依赖回收成本,应明确采用相应设置,而不是把结果直接等同于实际服务延迟。

至少随结果保留解释器版本、平台、输入规模和测量范围。同一机器交替观察不同模式与候选方案,可减少固定顺序带来的误判;仍需留意系统负载和预热。本文脚本只帮助读者形成可比较的记录,没有自动计算“profile 比 cProfile 慢几倍”的结论。

常见问题

profile 的校准能消除运行变慢吗?

校准用于补偿统计中的部分事件开销,不会让已发生的处理时间消失。它也不是可跨机器、跨计时器直接复用的万能常数。不需要研究或扩展纯 Python 剖析实现时,通常没有必要为日常热点定位选它。

cProfile 能直接证明 Python 写法不如 C 函数快吗?

不能直接根据带剖析器的结果做这种基准判断。两种实现被观测时的扰动可能不同,应把结论限制在热点线索,再用脱离剖析器、输入一致的测量回答速度问题。

示例可以放进生产请求里长期运行吗?

这份小项目用于开发环境理解开销和口径。它打印报表,也没有服务隔离、请求采样或并发治理设计,不能直接当作生产监控方案。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>