分析 Python 代码

性能分析是一种找出程序耗时情况的技术。有了这些统计数据,我们可以找到程序的“热点”并思考改进方法。有时,意外位置的热点也可能暗示程序中存在错误。

在本教程中,我们将了解如何在 Python 中使用性能分析工具。具体来说,您将看到:

  • 如何使用 `timeit` 模块比较小段代码
  • 如何使用 `cProfile` 模块对整个程序进行性能分析
  • 如何在现有程序中调用性能分析器
  • 性能分析器不能做什么

用我的新书《Python for Machine Learning启动您的项目,包括分步教程和所有示例的Python 源代码文件。

让我们开始吧。

分析 Python 代码。图片来源:Prashant Saini。保留部分权利。

教程概述

本教程分为四个部分;它们是:

  • 分析小片段
  • profile 模块
  • 在代码中使用分析器
  • 注意事项

分析小片段

当您被问到在 Python 中实现相同功能的多种方法时,一个角度是检查哪种方法更有效。在 Python 的标准库中,我们有 `timeit` 模块,它允许我们进行一些简单的性能分析。

例如,要连接许多短字符串,我们可以使用字符串的 `join()` 函数或 `+` 运算符。那么,我们如何知道哪个更快呢?考虑以下 Python 代码:

这将在变量 `longstr` 中生成一个长字符串 `012345....`。另一种编写方式是:

为了比较这两种方法,我们可以在命令行中执行以下操作:

这两个命令将产生以下输出:

上述命令用于加载 `timeit` 模块并传入一行代码进行测量。在第一种情况下,我们有两行语句,它们作为两个单独的参数传递给 `timeit` 模块。同理,第一个命令也可以表示为三行语句(通过将 for 循环分成两行),但每行的缩进需要正确引用:

`timeit` 的输出是在多次运行(默认为 5 次)中找到最佳性能。每次运行都会执行提供的语句几次(动态确定)。时间报告为在最佳运行中执行语句一次的平均时间。

虽然 `join` 函数在字符串连接方面确实比 `+` 运算符快,但上述计时并不是一个公平的比较。这是因为我们在循环中实时使用 `str(x)` 创建短字符串。更好的方法如下:

产生以下结果:

`-s` 选项允许我们提供“设置”代码,该代码在性能分析之前执行,并且不计时。在上述示例中,我们在开始循环之前创建了短字符串列表。因此,创建这些字符串的时间不会在“每循环”计时中测量。上述结果表明 `join()` 函数比 `+` 运算符快两个数量级。`-s` 选项更常见的用法是导入库。例如,我们可以比较 Python 的 `math` 模块中的平方根函数与 NumPy 中的平方根函数,并使用指数运算符 `**` 如下:

上述命令产生以下测量结果,我们看到在这个特定示例中,`math.sqrt()` 最快,而 `numpy.sqrt()` 最慢:

如果您想知道为什么 NumPy 最慢,那是因为 NumPy 是为数组优化的。在以下替代方案中,您将看到其出色的速度:

结果是:

如果您愿意,也可以在 Python 代码中运行 `timeit`。例如,以下代码与上述类似,但会为您提供每次运行的总原始计时:

在上述代码中,每次运行执行语句 10,000 次;结果如下。您可以看到最佳运行中每次循环大约 98 微秒的结果:

Profile 模块

从微观角度关注一个或两个语句的性能。我们很可能有一个很长的程序,并且想知道是什么导致它运行缓慢。这发生在我们可以考虑替代语句或算法之前。

程序运行缓慢通常是由于两个原因:某一部分运行缓慢,或者某一部分运行次数过多,累积起来占用太多时间。我们称这些“性能瓶颈”为热点。让我们看一个例子。考虑以下使用爬山算法为感知器模型寻找超参数的程序:

假设我们将此程序保存在文件 `hillclimb.py` 中,我们可以通过命令行运行性能分析器,如下所示:

输出将是:

程序正常输出会首先打印,然后打印性能分析器的统计信息。从第一行我们看到,程序中的 `objective()` 函数运行了 101 次,耗时 4.89 秒。但这 4.89 秒主要花费在其调用的函数上,该函数本身仅耗时 0.001 秒。来自依赖模块的函数也经过了性能分析。因此,您也会看到很多 NumPy 函数。

上述输出很长,可能对您没有用,因为它很难分辨哪个函数是热点。实际上,我们可以对上述输出进行排序。例如,要查看哪个函数被调用次数最多,我们可以按 `ncalls` 排序:

其输出如下:它表示 Python 字典中的 `get()` 函数是使用最多的函数(但它在程序完成的 5.6 秒总时间中仅消耗了 0.03 秒):

其他排序选项如下:

排序字符串 含义
calls 调用计数
cumulative 累积时间
cumtime 累积时间
file 文件名
filename 文件名
module 文件名
ncalls 调用计数
pcalls 原始调用计数
line 行号
name 函数名
nfl 名称/文件/行
stdname 标准名称
time 内部时间
tottime 内部时间

如果程序需要一些时间才能完成,为了以不同的排序顺序查找性能分析结果而多次运行程序是不合理的。实际上,我们可以保存性能分析器的统计数据以进行进一步处理,如下所示:

与上述类似,它将运行程序。但这不会将统计数据打印到屏幕上,而是将其保存到文件中。之后,我们可以使用 `pstats` 模块,如下所示,打开统计文件并提供一个提示来操作数据:

例如,我们可以使用 `sort` 命令更改排序顺序,并使用 `stats` 打印我们上面看到的内容:

您会注意到上面的 `stats` 命令允许我们提供一个额外的参数。该参数可以是正则表达式,用于搜索函数,这样只会打印匹配的函数。因此,这是一种提供搜索字符串进行过滤的方法。

想开始学习机器学习 Python 吗?

立即参加我为期7天的免费电子邮件速成课程(附示例代码)。

点击注册,同时获得该课程的免费PDF电子书版本。

这个 `pstats` 浏览器允许我们看到不仅仅是上面的表格。`callers` 和 `callees` 命令向我们展示了哪个函数调用了哪个函数,调用了多少次,以及花费了多少时间。因此,我们可以将其视为函数级统计信息的细分。如果您有很多函数相互调用,并且想知道时间是如何在不同场景中花费的,这会很有用。例如,这表明 `objective()` 函数仅由 `hillclimbing()` 函数调用,而 `hillclimbing()` 函数调用了其他几个函数:

在代码中使用分析器

上面的例子假设您有一个完整的程序保存在文件中,并且您对整个程序进行了性能分析。有时,我们只关注整个程序的一部分。例如,如果我们加载一个大型模块,它需要时间来启动,我们希望将其从性能分析器中排除。在这种情况下,我们只能为某些行调用性能分析器。以下是一个修改自上述程序的示例:

它将输出以下内容:

注意事项

将性能分析器与 Tensorflow 模型一起使用可能不会产生您预期的结果,特别是如果您为模型编写了自定义层或自定义函数。如果您正确地完成了,Tensorflow 应该在模型执行之前构建计算图,因此逻辑将改变。性能分析器输出因此不会显示您的自定义类。

对于一些涉及二进制代码的高级模块也是如此。性能分析器可以看到您调用了一些函数并将其标记为“内置”方法,但它无法进一步深入编译代码。

以下是 MNIST 分类问题的 LeNet5 模型的简短代码。如果您尝试对其进行性能分析并打印前 15 行,您将看到一个包装器占据了大部分时间,并且在此之外什么也无法显示:

在下面的结果中,`TFE_Py_Execute` 被标记为“内置”方法,它在总运行时间 39.6 秒中消耗了 30.1 秒。请注意,tottime 与 cumtime 相同,这意味着从性能分析器的角度来看,所有时间似乎都花在此函数上,并且它没有调用任何其他函数。这说明了 Python 性能分析器的局限性。

最后,Python 的性能分析器仅为您提供时间统计数据,而不提供内存使用情况。您可能需要为此目的寻找其他库或工具。

进一步阅读

标准库模块 `timeit`、`cProfile` 和 `pstats` 在 Python 文档中都有其文档:

标准库的性能分析器非常强大,但并非唯一。如果您想要更可视化的东西,可以尝试 Python Call Graph 模块。它可以使用 GraphViz 工具生成函数如何相互调用的图片:

无法深入编译代码的局限性可以通过不使用 Python 的性能分析器,而是使用针对编译程序的性能分析器来解决。我最喜欢的是 Valgrind:

但要使用它,您可能需要重新编译 Python 解释器以开启调试支持。

总结

在本教程中,我们了解了什么是性能分析器及其功能。具体来说:

  • 我们知道如何使用 `timeit` 模块比较小段代码
  • 我们看到 Python 的 `cProfile` 模块可以为我们提供有关时间花费的详细统计数据
  • 我们学习了如何使用 `pstats` 模块处理 `cProfile` 的输出进行排序或过滤

掌握机器学习 Python!

Python For Machine Learning

更自信地用 Python 编写代码

...从学习实用的 Python 技巧开始

在我的新电子书中探索如何实现
用于机器学习的 Python

它提供自学教程数百个可运行的代码,为您提供包括以下技能:
调试性能分析鸭子类型装饰器部署等等...

向您展示高级 Python 工具箱,用于
您的项目


查看内容

对《分析 Python 代码》的 3 条评论

  1. simo 2022 年 6 月 7 日 凌晨 2:54 #

    嘿,阿德里安,
    好文章。
    在我看来,最好深入研究 Cython 作为加速代码的方法。

  2. Jürgen A. Erhard 2022 年 6 月 18 日 凌晨 4:02 #

    没有提到行分析器。糟糕的文章。

    • Luis 2023 年 1 月 20 日 晚上 7:38 #

      多么美妙地评价别人的作品。

发表评论

Machine Learning Mastery 是 Guiding Tech Media 的一部分,Guiding Tech Media 是一家领先的数字媒体出版商,专注于帮助人们了解技术。访问我们的公司网站以了解更多关于我们的使命和团队的信息。