2.1 高效地剖析
剖析的首要目标是对有代表性的系统进行测试,找出速度缓慢(或占用RAM过多、导致大量磁盘I/O或网络I/O)的部分。剖析通常会带来额外的开销(通常可能导致速度变为原来的1/100~1/10),而你希望在尽可能接近实际情况的环境中运行代码。因此,你需要提取测试用例,并将要测试的系统部分隔离——最理想的情况是它位于一组独立的模块中。
本章首先介绍最基本的剖析方法,这包括IPython命令%timeit、函数time.time()以及计时装饰器。你可以使用这些方法来搞清楚语句和函数的行为。
然后,本章介绍了cProfile(见2.6节),演示了如何使用内置工具cProfile来搞清楚代码中哪些函数耗时最长。这让你能够对问题有大致认识,进而专注于那些关键函数。
接下来,本章介绍line_profiler(见2.8节),line_profiler让你能够逐行剖析选定的函数,结果包括每行的执行次数及时间占比,这些信息让你能够知道哪些代码运行缓慢以及其中的原因。
有了line_profiler的结果后,你就掌握了接着使用编译器所需的信息(见第7章)。
在第6章,你将学习如何使用perf stat来搞明白以下两个方面:最终在CPU中执行了多少个指令;CPU缓存的利用情况。这让你能够对矩阵运算进行高级优化。阅读完本章后,你就应该去看看示例6-8。
如果要优化的是长期运行的系统,则使用line_profiler进行剖析后,你还可能想使用py-spy来了解正在运行的Python进程。
为帮助你搞明白RAM占用量高的原因,我们将演示memory_profiler(见2.9节)。它特别适用于在带标注的图表中跟踪一段时间内RAM的占用量,让你能够向同事解释有些函数的RAM占用量超过预期的原因。
警告:不管使用哪种代码剖析方法,都别忘了单元测试的代码覆盖率必须足够高。单元测试有助于避免愚蠢的错误、确保结果可重现。没有单元测试,你将身处险境。
务必先对代码进行剖析,再编译或重写算法,因为要确定提高代码速度的最有效方式,必须有理有据。
接下来,本章将介绍CPython中的Python字节码(见2.11.1节),这让你能够知道幕后发生的事情。具体地说,知道基于栈的Python虚拟机的工作原理后,有助于你明白为何有些编码方式的运行速度比其他方式要慢。
然后本章将复习如何在剖析过程中结合使用单元测试,在提高代码运行效率的同时确保它们正确无误(见2.12节)。
最后,本章将讨论剖析策略(见2.13节),让你能够可靠地剖析代码,并收集验证假设所需的正确数据。届时你将了解到,动态CPU频率缩放(CPU frequency scaling)以及诸如Turbo Boost等特性可能导致剖析结果不准确,因此你将学习如何禁用它们。
为演示前述各种剖析步骤,我们需要一个易于分析的函数。2.2节将介绍朱利亚集合,这是一个内存占用量较高的CPU密集型函数,它还具有非线性特征(即结果难以预测),这意味着需要在运行时进行剖析,而无法做离线分析。