如何找出我的 Python 代码的瓶颈

以战略性的方式调试性能问题——开发人员通常需要优化代码以使其性能更好。这听起来像是正确的做法,但并非总是如此。代码优化是一个模糊的术语。没有明确的目标,你很容易掉进兔子洞,浪费时间。在本文中…

如何找出我的 Python 代码的瓶颈

以战略性的方式调试性能问题

如何找出我的 Python 代码的瓶颈

通常情况下,开发人员需要优化代码以使其性能更好。这听起来像是正确的做法,但并非总是如此。代码优化是一个模糊的术语。没有明确的目标,你很容易掉进兔子洞,浪费时间。在本文中,我想告诉你一种找到代码瓶颈的战略方法——从了解是否应该优化代码到找出导致性能问题的函数。

什么时候应该优化?

我们所说的代码优化是指重写代码,使程序执行得更快,并使用更少的内存、CPU、磁盘空间或网络带宽。那么,是不是每个程序都需要时不时地进行优化呢?不会。通常,只有少数类型的程序需要优化。

实时应用

Apache Kafka 和 RabbitMQ 等成熟的实时应用程序以高吞吐量和低延迟着称。许多公司已将它们安装在自己的环境中。由于设置不同,性能可能会因情况而异。

有几个基准可以帮助您评估性能。如果您发现它与基准测试相比不够快,您可以微调配置或垂直或水平扩展硬件。[0]

具有 SLA 的应用程序

另一种类型的程序是具有严格服务水平协议 (SLA) 的程序。换句话说,程序必须在一定的时限内交付结果,否则可能会影响业务。显然,SLA是一个硬门槛,但不要等到越界,那就太迟了。其中一项预防措施是监控过程并在发现增加趋势或倾斜模式时发送警报。

其他有异常行为的应用程序

进行代码优化的原因不是因为开发人员的上帝感觉(尽管它有帮助),而是因为数字和事实。在这种情况下,监控起着至关重要的作用。它可以帮助开发人员了解程序在晴天的行为,并从那里发现下雨天。例如,资源(CPU/内存/磁盘)使用率呈上升趋势、峰值时间激增、可疑日志等。根本原因可能多种多样——资源不足、代码中的错误、来自外面等

经验法则是,如果程序与所需速度相比不够快,则应优化代码,无论是基准测试、SLA 还是过去的平均性能。进行监控和警报可以提供对系统运行状况的可见性,并帮助开发人员了解使用或行为的趋势以采取预先行动。

优化什么? – 硬件

当您对数字感到失望时,下一步就是采取行动。开发人员很容易被各种解决方案所淹没。大多数性能问题与运行速度过慢或使用过多内存有关。

一种潜在的解决方案是拥有更好的硬件。借助云计算,可以在几秒钟内完成添加更多内核或内存。但与任何解决方案一样,它也需要权衡取舍。添加更多资源是一种简单快捷的解决方案,可以有效解决紧迫的生产问题。但是,如果这是您解决性能问题的唯一解决方案,那么您的程序最终将非常昂贵。水平扩展会导致成倍的成本,而垂直扩展可能会达到云提供商可以为单台机器提供的限制。根据程序的设计,这可能需要将代码从单机重构为分布式系统。

不要假设硬件是唯一的解决方案,在并行化代码之前,请先在具有单个 CPU 和有限内存的机器上优化代码。在许多情况下,花时间在代码优化上是值得的。例如,算法优化——将时间复杂度从 O(n²) 提高到 O(nlogn) 或 O(n),语言切换——使用基于 CPython 的库或使用缓存来减少繁重的 I/O 操作量等.

下一个问题是——我怎么知道代码的哪一部分应该优化,因为重写整个代码绝对不是一个明智的决定。在下一节中,我将介绍 Python 中的程序分析,帮助开发人员快速找出繁重的操作并从那里改进。

优化什么? – 软件

为了找出生产应用程序中的性能瓶颈,开发人员需要可行的见解。一种现代方法是应用分析来突出显示最慢的代码,该代码是消耗大部分资源(如 CPU 和内存)的区域。分析可以是一次性操作,也可以是生产(或类似生产环境)中的持续过程。不同的分析器使用不同的方法来收集信息,因此具有不同的运行时开销。为您的用例选择正确的分析器很重要。

在本节中,我将介绍两种不同的分析方法和几种可视化结果的方法。

Example

在本文中,我们将分析这个 repo 的代码——一个计算入射波在 2D 对象上的散射的简单程序。[0]

确定性分析器

您可能听说过 cProfile,这是 Python 中的内置分析器。 cProfile 是一个确定性分析器,旨在反映所有函数调用、函数返回和异常事件都受到监视的事实,并为这些事件之间的间隔进行精确计时。[0]

这种类型的分析器可以收集高分辨率的分析信息,但有一个主要缺点:高开销。你可以想象,如果应用程序有大量的函数调用,那么分析器最终会收集到太多的记录。如果函数很小,那么由于分析器本身的开销,结果可能不准确。

不过,这里是如何在此示例中使用 cProfile。 cProfile 的总执行时间为 89.03 秒。

python -m cProfile scatter.py

如果没有 cProfile,程序需要 80.50 秒,快 10%。因此,不建议在生产执行期间使用。

cProfile 的输出如下所示,其中每一行都是执行期间的函数调用。它记录了调用次数、函数花费的总时间、在这个函数和所有子函数中花费的累计时间等。

但是这个表对于人类来说很难解释,因为它包含了太多我们并不关心的信息,比如 Python 的内部函数。此外,我们不知道每个函数如何与其他函数相关,以及较慢函数的输入是什么。如果一个函数被多个函数调用,则很难弄清楚是哪条路径和相应的输入导致了缓慢。

一种解决方案是在图表中可视化结果以了解函数之间的关系。由于 cProfile 不提供任何可视化,我们需要使用像 snakeviz 和 gprof2dot 这样的库来做到这一点。[0][1]

snakeviz

我们将在命令行中使用 cProfile 来创建配置文件并使用 snakeviz 来解释结果。 Snakeviz 有两种可视化风格——Icicle 和 Sunburst。在函数中花费的时间由矩形的宽度或弧的角度范围表示。

python -m cProfile -o  scatter .prof  scatter .py 
# Execution time : 86.27 sec
snakeviz scatter.prof

gprof2dot

gprof2dot 创建了另一种可视化类型——点图。我更喜欢这种类型的图表,因为它清楚地显示了函数之间的关系,并且颜色对比更容易发现重函数。

gprof2dot --colour-nodes-by-selftime -f pstats output.pstats | \
dot -Tpng -o output.png

统计分析器

如果我们只是想大致了解在本地笔记本电脑上开发和调试应用程序时的性能,那么 cProfile 就可以了。但不建议在生产中使用它,因为可能会对性能产生明显影响。这就是统计分析器来救援的地方。

它通过定期采集执行状态样本来衡量应用程序的性能。这种方法不如确定性分析准确,但它的开销也更少。由于其开销较小,它可用于监控生产中正在进行的流程。对于某些应用程序,分析必须是生产中的一个连续过程,而在其他环境中很难发现性能问题。

我将列出一些最流行的统计分析器。他们中的大多数都有一个内置的可视化解决方案,所以我们不需要额外的包。

pyinstrument[0]

pyinstrument 是一个统计 python 分析器,它每 1 毫秒记录一次调用堆栈,而不是记录整个跟踪。您可以直接从命令行调用 pyinstrument:

pyinstrument scatter.py

您还可以生成交互式网页:

pyinstrument -r html -o output.html scatter.py

与 cProfile 的原始输出不同,pyinstrument 为您提供了一个突出显示的函数调用树,它更容易解释。它也比 cProfile 花费的时间更少。

从树中可以明显看出,computerScatteredWaveElement 函数是瓶颈。我真的很喜欢 pyinstrument 的简单性,它已经成为我的首选 Python 分析器。

pyinstrument 提供了一个 Python 接口来分析你的代码块。它帮助开发人员只关注最有趣的部分。[0][1]

from pyinstrument import Profiler

profiler = Profiler()
profiler.start()

computerScatteredWaveElement()

profiler.stop()
profiler.print()

根据其文档,pyinstrument 能够分析异步函数。例如,我有一个简单的 asyc 函数:[0]

import asyncio
from pyinstrument import Profiler
async def main():
p = Profiler()
with p:
await asyncio.sleep(5)
p.print()
asyncio.run(main())

输出看起来像这样。它通过跟踪主线程来工作。在主线程之外花费的任何时间都归因于等待。

pyinstrument 的一个缺点是它只能分析主线程,因此不能在多线程应用程序中使用。例如,我有一个简单的多线程代码:

def thread_function(name):
time.sleep(2)
if __name__ == "__main__":
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
executor.map(thread_function, range(3))

如您所见,输出忽略了其他 2 个线程。

yappi[0]

yappi 代表另一个 python 分析器。它支持多线程、asyncio 和 gevent 代码的分析。由于它是用 C 设计的,因此与用 Python 设计的分析器相比,它运行得更快。它还支持对代码的特定部分进行分析。

这是多线程应用程序的分析输出。您可以通过 yappi.get_thread_stats() 检查每个线程的性能。

threads = yappi.get_thread_stats()
for thread in threads:
yappi.get_func_stats(ctx_id=thread.id).print_all()

py-spy[0]

py-spy 是另一个统计分析器。一个重要功能是您可以将探查器附加到现有流程。这使得它非常适合长期运行的生产应用程序。

py-spy 获取应用程序的 PID 或您要运行的 python 程序的命令行。它从不同的 python 进程读取内存,出于安全原因可能不允许这样做。在某些操作系统中,您需要以 root 用户身份运行它。

sudo py-spy record -o profile.svg -- python scatter.py
# Execution time : 78.78 sec
# Or
sudo py-spy record -o profile.svg --pid

输出是一个冰柱图,类似于snakeviz。

其他分析器

在这里,我列出了一些其他选项供您探索和尝试。

plop:一个低开销的分析器

plop 是一个低开销的分析器,自 2019 年以来一直没有开发。它具有网络风格的可视化,其中圆圈的大小基于函数所花费的时间。箭头粗细表示函数被调用的频率。

python -m plop.collector scatter.py
# Execution time : 73.72 sec
# profile output saved to profiles/scatter.py-20220418-1013-32.plop
# overhead was 1.1695748642208658e-05 per sample
# (0.0011695748642208657%)
python -m plop.viewer --datadir=profiles/

内存分析器

除了分析 CPU 时间之外,有时了解内存使用情况也很重要。 memory-profiler 是一个 python 模块,用于监控进程的内存消耗以及对内存消耗的逐行分析。它提供了一个装饰器接口,所以它不会创建太多的样板代码。[0]

输出显示内存使用情况和每行的增量。

Conclusion

我希望你觉得这篇文章有用!我们已经讨论了何时(不)优化我们的应用程序以及在必要时如何优化。一种快速的解决方案是改进硬件,但这不是一个可持续的解决方案,因为它最终会达到硬件的极限。另一种选择是分析应用程序并重构导致性能问题的代码部分。

有几种类型的分析器。请务必了解每个分析器的优缺点,然后选择适合您用例的分析器。如果您有任何意见或想法,请告诉我。调试愉快!干杯!

Reference

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
扎眼的阳光的头像扎眼的阳光普通用户
上一篇 2022年4月24日
下一篇 2022年4月24日

相关推荐