当测量执行时间时,结果取决于计算机硬件。为了能够产生通用测量,最简单的方法
测量固定序列的代码基准速度,并计算出它的比率。由此,函数所花费的时间可以转换为
一个比较通用的值,可以在任何计算机上比较。
许多通用的基准测试工具可用于测量计算机性能。令人惊讶
的是,许多年前创建的一些仍在使用。例如,Whetstone 创建
于1972 年,它是一个用Algol 60 编写的计算机性能分析器(见
http://en.wikipedia.org/wiki/Whetstone_%28benchmark%29)。
它用于测量每秒百万Whetstone 指令条数(MWIPS)。陈旧
的和现代的CPU 的结果表保存在http://freespace.virgin.net/
roy.longbottom/whetstone%20results.htm。
Python 在其test 包中提供了一个基准测试工具,用于测量一个精选的操作序列的持
续时间。结果是每秒钟计算机能够执行的pystones 数量,在现代硬件上通常约为一秒,
如下所示:
from test import pystone
pystone.pystones()
(1.0500000000000007, 47619.047619047589)
该比率可用于将分析的持续时间转换为pystones 的数量:
from test import pystone
benchtime, pystones = pystone.pystones()
def seconds_to_kpystones(seconds):
… return (pystones*seconds) / 1000
…
…
seconds_to_kpystones(0.03)
1.4563106796116512
seconds_to_kpystones(1)
48.543689320388381
seconds_to_kpystones(2)
97.087378640776762
seconds_to_kpystones 返回千个ρystones 的数量。如果你想编写一些速度断言,
这个转换可以包含在你的测试中。
拥有pystones 将允许你在测试中使用此装饰器,以便于你在执行时间上设置断言。
这些测试可以在任何计算机上运行,并让开发人员避免速度回归。当应用程序的一部分已
优化时,他们将能够在测试中设置其最大执行时间,并确保它不会被进一步的更改所破坏。
这种方法当然不是理想的并且100%准确的,但是它至少比以硬编码的执行时间断言更好,
这些断言是以秒表示的原始值。
11.3.2 分析内存使用
在优化应用程序时可能遇到的另一个问题是内存消耗。如果一个程序开始消耗了很多
的内存,系统就会开始交换,在你的应用程序中可能有一个地方,有太多的对象被创建,
或者你不打算保留的对象由于一些无意的引用仍然保持存活。使用传统的分析可以很容易
检测到该问题,因为消耗很多的内存会引起系统交换,会涉及大量CPU 的工作,这很容易
被检测到。但有时它不明显,必须分析内存使用情况。
1.Python 如何处理内存
当你使用CPython 实现时,内存使用情况可能是Python 中最难分析的事情。虽然像C
这样的语言允许你获得任何元素的内存大小,但Python 永远不会让你知道一个给定的对象消
耗了多少内存。这是由于语言的动态特性,实际上,语言的使用者不能直接访问内存管理器。
内存管理的一些原始细节已经在第7 章中解释过了。我们已经知道CPython 使用引用
计数来管理对象分配。这是确定性算法,它确保当对象的引用计数变为0 时,将会触发对
象释放。尽管是确定性的,这个过程难以手动跟踪而且代码库也非常的复杂。此外,在引
用计数级别上释放对象并不一定意味着解释器释放了实际的进程的堆内存。根据CPython
解释器的编译标志,系统环境或运行时上下文,内部内存管理层可能会决定留下一些空闲
内存块,用于将来重新分配,而不是完全释放。
CPython 实现中的其他微优化也使预测实际内存使用更加困难。例如,指向同一短字
符串或小整数值的两个变量可能或可能不指向内存中的同一对象实例。
尽管相当吓人,看似复杂,Python 中的内存管理有着非常好的文档(参考https://docs.
python.org/3/c-api/memory.html)。注意,在大多数情况下,在调试内存问题时,可以忽略前
面提到的微优化。此外,引用计数大致基于一个简单的语句:如果给定的对象不再被引用,
它被删除。换句话说,函数中的所有本地引用都会被解释器删除。
• 离开函数。
• 确保对象不再使用。
因此,保留在内存中的对象如下。
• 全局对象。
• 仍以某种方式引用的对象。
小心参数输入输出的边缘情况。如果在参数中创建了一个对象,并且函数返回该对象,
那么参数引用将仍然存在。如果将其作为默认值使用,可能会导致以下意外结果:
def my_function(argument={}): # 不良实践
… if ‘1’ in argument:
… argument[‘1’] = 2
… argument[‘3’] = 4
… return argument
…
my_function()
{‘3’: 4}
res = my_function()
res[‘4’] = ‘I am still alive!’
print my_function()
{‘3’: 4, ‘4’: ‘I am still alive!’}
这就是为什么应该总是像这样使用不可变对象,如下:
def my_function(argument=None): # 良好的实践
… if argument is None:
… argument = {} # 每次都是创建新的字典
… if ‘1’ in argument:
… argument[‘1’] = 2
… argument[‘3’] = 4
… return argument
…
my_function()
{‘3’: 4}
res = my_function()
res[‘4’] = ‘I am still alive!’
print my_function()
{‘3’: 4}
Python 中的引用计数很方便,你无需手动跟踪对象的对象引用,因此你不必手动销毁
它们。虽然这引入了另一个问题,但是开发人员从来不需要清除内存中的实例,如果开发
人员不注意他们使用数据结构的方式,内存可能会以不受控制的方式增长。
通常,消耗内存的情况主要有如下几种。
• 不受控制的缓存。
• 全局注册实例并且不跟踪其使用情况的对象工厂,例如每次调用查询时即时使用的
数据库连接器创建者。
• 未正确结束的线程。
• 使用__del__方法并涉及循环的对象也是内存消费者。在旧版本的Python(在 3.4版本之前),垃圾回收器不会打破循环,因为它不能确定应该首先删除哪个对象。
因此,这会导致泄漏内存。在大多数情况下不应该使用此方法。
不幸的是,在使用Python/C API 的C 扩展中,引用计数的管理必须使用Py_INCREF()
和Py_DECREF()宏手动完成。我们在第7 章中讨论了处理引用计数和引用所有权的注
意事项,所以你应该已经知道这是一个相当棘手的话题,带有各种陷阱。这就是大多数
内存问题是由C 扩展引起的语音,这些扩展编写的不太合理。