本节书摘来自异步社区《Python高性能编程》一书中的第2章,第2.1节,作者[美] 戈雷利克 (Micha Gorelick),胡世杰,徐旭彬 译,更多章节内容可以访问云栖社区“异步社区”公众号查看。
第2章 通过性能分析找到瓶颈
读完本章之后你将能够回答下列问题
- 如何找到代码中速度和RAM的瓶颈?
- 如何分析CPU和内存使用情况?
- 我应该分析到什么深度?
- 如何分析一个长期运行的应用程序?
- 在CPython台面下发生了什么?
- 如何在调整性能的同时确保功能的正确?
性能分析帮助我们找到瓶颈,让我们在性能调优方面做到事半功倍。性能调优包括在速度上巨大的提升以及减少资源的占用,也就是说让你的代码能够跑得“足够快”以及“足够瘦”。性能分析能够让你用最小的代价做出最实用的决定。
任何可以测量的资源都可以被分析(不仅是CPU!)。我们在本章将分析CPU的时间和内存的占用。你也可以将同样的技术用于分析网络带宽和磁盘I/O。
如果一个程序跑得太慢或占用了太多RAM,那么你一定希望把有问题的代码修正。当然,你完全可以跳过性能分析,修正你认为可能有问题的地方——但是小心,你很有可能“修正了”错误的地方。比起依靠你的直觉,更有效率的做法是先进行性能分析,做出一个假设,然后再改动你的代码结构。
人有时候懒点比较好。先进行性能分析让你能够迅速定位需要被解决的瓶颈,然后你就可以用最小的改动获得你需要的性能提升。如果你回避性能分析直接进行优化,那么你很有可能最终付出了更多的努力。优化应该总是基于性能分析的结果。
2.1 高效地分析性能
性能分析的首要目标是对受测系统进行测试来发现哪里太慢(或占用太多RAM,或导致太多磁盘I/O或网络I/O)。性能分析一般会导致额外的性能开销(一般会慢10到100倍),但你依然希望你的代码尽可能像是在真正的环境中一样运行。所以要以测试用例的方式将你需要测试的那部分系统独立出来。最好这个测试用例已经使用了一套自己的模块。
本章介绍的第一个基本技术包括IPython的%timeit魔法函数,time.time(),以及一个计时修饰器。你可以使用这些技术来了解语句和函数的行为。
然后我们会学习cProfile(2.6节),告诉你如何使用这个内建工具来了解代码中哪些函数耗时最长。这将让你站在高处俯瞰你的问题,使你能够将注意力集中到关键函数上。
接下来,我们会去看line_profiler(2.8节),这个工具能够对你选定的函数进行逐行分析。其结果将包含每行被调用的次数以及每行花费的时间百分比。这恰能让你知道是哪里跑得慢以及为什么。
有了line_profiler的结果,你就有了足够的信息去使用编译器(第7章)。
在第6章(例6-8),你将学到如何使用perf stat命令来了解最终执行于CPU上的指令的个数以及CPU缓存的利用率。这让你能够进一步调优矩阵操作。读完本章后你应该去看看那个例子。
line_profiler之后,我们会演示heapy(2.10节),它可以追踪Python内存中所有的对象——这对于消灭奇怪的内存泄漏特别有用。如果你的系统需要持续运行,那么你会对dowser(2.11节)感兴趣,它让你能够通过一个Web浏览器界面审查一个持续运行的进程中的实时对象。
为了帮助你了解为什么你的RAM占用特别高,我们会给你演示memory_profiler(2.9节)。它能以图的形式展示RAM的使用情况随时间的变化,这样你就可以向你的同事们解释为什么某个函数占用了比预期更多的RAM。
备忘
无论你用什么方法分析代码性能,都必须记得用足够的单元测试覆盖你的代码。单元测试能帮助你避免愚蠢的错误并让你的结果可重现。没有单元测试风险极大。
在编译或重写你的算法之前始终进行性能分析。你需要证据来决定最有效的优化手段。
最后,我们还会给你介绍CPython中的Python字节码(2.12节),这样你就能够了解在其台面下发生了什么。具体来说,了解基于栈的Python虚拟机如何运行将帮助你明白为什么某个编程风格会跑得比别人慢。
在结束本章之前,我们会回顾如何在性能分析中集成单元测试(2.13节),让代码跑得更有效的同时维持正确。
最后我们将讨论性能分析的策略(2.14节),这样你就能够可靠地分析你的代码并收集正确的数据来验证你的假设。在这里,你将了解到动态CPU频率以及TurboBoost等特性能够如何歪曲你的性能分析结果以及如何禁用这些功能。
为了讲解所有这些步骤,我们需要以一个便于分析的函数为例。下一节我们介绍Julia集合。这是一个对RAM有一点饥渴的CPU密集型函数,而且它还具有非线性的行为(这样我们就无法轻易预测其结果),这意味着我们需要在运行时分析其性能而没法进行线下调查。