优化——一般原则与分析技术
3 个优化规则
进行任何类型的优化时,请注意以下几个规则。
- 首先要能工作。
- 从用户的角度考虑。
- 保持代码的可读性。
首先要能工作
第一个目标是使它正常工作。优化工作不应该阻碍这个首要目标。
在到你的代码正常工作以及你准备好调优之前,不要做任何以下这些事情。
- 开始编写全局字典以缓存函数的数据。
- 考虑使用C语言或者混合语言(如 Cython)外部化一部分代码。
- 查找一些进行基本计算的外部库。
另一方面,使用像 NumPy 这样的库可以缓解特定功能的开发,并且最终产生更简单和更快的代码。
保持代码的可读性和可维护性
优化不应该使你的代码不可读。在保持代码的可读性和可维护性与为了优化而搞的代码面目全非之间,要有一个平衡。当你达到了 90% 的优化目标,剩下的 10% 将使你的代码完全不可读,那么你最好停止那里的工作或者寻找其他的解决方案。
优化策略
不要试图猜测如何使程序更快。通常,通过查看代码是很难找到瓶颈的,所以,需要一套工具来找到真正的问题。
良好的优化策略可以从 3 个步骤开始。
- 找到另外的罪魁祸首:确保第三方服务器或资源没有故障。
- 扩展硬件:确保资源充足。
- 编写速度测试:创建具有速度目标的场景。
查找瓶颈
可以通过以下方法找到应用程序的瓶颈。
- 分析 CPU 使用情况。
- 分析内存使用情况。
- 分析网络使用情况。
分析 CPU 使用情况
瓶颈的第一个来源是你的代码。标准库提供执行代码分析所需的所有工具。它们基于确定性方法。
确定性分析器(deterministic profiler)通过在最底层添加定时器来测量在每个函数中花费的时间。这引入了一点开销,但提供了一个查找哪里消耗时间的好办法。另一方面,统计分析器(statisticalprofiler)对指令指针的使用进行采样,并且不对代码进行操作。
有两种方法来分析代码。
-
宏观分析(Macro-profiling):当程序运行时,对整个程序进行分析,并生成统计数据。
通过在特殊模式下运行应用程序来完成宏观分析,在此模式下,会检测解释器并收集有关代码使用的统计信息。Python 为此提供了几个工具。
- profile:这是一个纯Python实现。
- cProfile:这是一个C实现,提供与 profile 工具相同的接口,但具有较少的开销。
宏观分析是一个检测函数的好方法,可以发现函数中的问题或者至少是它附近的问题。
-
微观分析(Micro-profiling):通过手动装置测量程序的精确部分。
当找到慢速函数时,有时需要进行更多的测试工作,只测试程序的一部分。通过在速度测试中手动检测一部分代码来完成测试。
例如,可以从装饰器使用 cProfile 模块。
确定性分析器将根据计算机正在进行的操作提供结果,因此结果可能每次都有变化。多次重复相同的测试并且做平均求得更准确的结果。
-
测量 Pystones
当测量执行时间时,结果取决于计算机硬件。为了能够产生通用测量,最简单的方法测量固定序列的代码基准速度,并计算出它的比率。由此,函数所花费的时间可以转换为一个比较通用的值,可以在任何计算机上比较。
Python 在其 test 包中提供了一个基准测试工具,用于测量一个精选的操作序列的持续时间。结果是每秒钟计算机能够执行的 pystones 数量。
拥有 pystones 将允许你在测试中使用此装饰器,以便于你在执行时间上设置断言。这些测试可以在任何计算机上运行,并让开发人员避免速度回归。当应用程序的一部分已优化时,他们将能够在测试中设置其最大执行时间,并确保它不会被进一步的更改所破坏。
分析内存使用
在优化应用程序时可能遇到的另一个问题是内存消耗。使用传统的分析可以很容易检测到该问题,因为消耗很多的内存会引起系统交换,会涉及大量CPU的工作,这很容易被检测到。
-
Python 如何处理内存
当你使用 CPython 实现时,内存使用情况可能是 Python 中最难分析的事情。虽然像 C 这样的语言允许你获得任何元素的内存大小,但 Python 永远不会让你知道一个给定的对象消耗了多少内存。这是由于语言的动态特性,实际上,语言的使用者不能直接访问内存管理器。
CPython 使用引用计数来管理对象分配。这是确定性算法,它确保当对象的引用计数变为 0 时,将会触发对象释放。
CPython 实现中的其他微优化也使预测实际内存使用更加困难。例如,指向同一短字符串或小整数值的两个变量可能或可能不指向内存中的同一对象实例。
Python 中的引用计数很方便,你无需手动跟踪对象的对象引用,因此你不必手动销毁它们。如果不注意使用数据结构的方式,内存可能会以不受控制的方式增长。
通常,消耗内存的情况主要有如下几种。
- 不受控制的缓存。
- 全局注册实例并且不跟踪其使用情况的对象工厂,例如每次调用查询时即时使用的数据库连接器创建者。
- 未正确结束的线程。
- 使用
__del__
方法并涉及循环的对象也是内存消费者。
-
分析内存
如果你没有引用内存,你不能释放它,这种情况被称为内存泄漏(memory leak)。在 Python 中,没有为用户提供底层的内存管理,所以我们宁愿处理泄漏的引用即对不再需要但未被删除的对象的引用。
Python 中的内存问题主要是由意外或计划外的资源获取模式引起的。Python 中大多数所谓的内存泄漏主要是由软件的过度复杂性和其组件之间的微小交互造成的,这些交互真的很难跟踪。为了发现和查找软件的这些缺陷,你需要知道在程序中的实际内存使用情况。
Python 不会轻易释放内存,而倾向于继续持有,以防再次需要。有几个工具可以抓取内存快照并计算加载对象的数量和大小。
- Memprof(http://jmdana.github.io/memprof ):该工具声明支持 Python 2.6,2.7,3.1,3.2 和 3.3 以及一些符合 POSIX 的系统(Mac OS X 和 Linux)。
- memory_profiler(https://pypi.python.org/pypi/memory_profiler ):该工具声明支持与 Memprof 相同的 Python 版本和系统。
- Pympler(http://pythonhosted.org/Pympler ):该库声明支持 Python 2.5,2.6,2.7,3.1,3.2,3.3 和 3.4,并且与操作系统无关。
- objgraph(参考 http://mg.pov.lt/objgraph )是一个简单的工具,用于创建对象引用的图表,可以用于在 Python 中寻找内存泄漏。它不是一个完全独立的工具,需要 Graphviz 才能创建内存使用图。
-
C 代码内存泄漏
如果 Python 代码看起来完全正常,但是在循环访问隔离函数时,内存仍然增加,则泄漏可能位于 C 端。
Python 核心代码是相当健壮的,并且对泄漏也进行了测试。
在 C 中进行内存调试不太容易,因此在深入扩展内部之前,请确保正确诊断问题的根源。隔离具有类似于单元测试的代码的可疑包,这是一个比较常用的做法。
- 为每个 API 单元或者引起内存泄漏的扩展的可疑的功能编写单独的测试。
- 在一个循环中独立进行任意长时间的测试(每次运行一次测试)。
- 从外部观察哪个测试功能会随着时间增加内存使用。
Valgrind 是一个著名的通用工具,它可以在编译代码中处理内存泄漏。它是一个用于构建动态分析工具的完整的探测框架。
分析网络使用情况
问题可能是配置错误的集线器,低带宽网络链路,或甚至是大量的使计算机多次发送相同的数据包的流量冲突。
要了解发生了什么,首先要研究以下 3 个领域:
- 使用以下工具观察网络流量。
- ntop:http://www.ntop.org(仅限 Linux)。
- wireshark:https://www.wireshark.org(以前命名为 Ethereal)。
- 使用 net-snmp 跟踪不正常或错误配置的设备(http://www.net-snmp.org)。
- 使用统计工具 Pathrate 来估量两台计算机之间的带宽。参见 http://www.cc.gatech.edu。