标准解释器优化

介绍

PyPy 标准解释器的一个优势(实际上也是一个激励目标)是它比 CPython 具有更高的灵活性和可配置性。

一个例子是,我们可以提供同一个对象的多个实现(例如列表),而不会对应用程序级代码造成任何差异。这使得为特定情况提供优化的类型专用实现变得容易,而不会影响常规情况下的实现。

本文档描述了几个这样的优化。大多数优化默认情况下是禁用的。此外,对于许多优化来说,它们在实际应用中是否值得(它们确实让一些微基准测试快得多,并且使用更少的内存,但这并不意味着太多)尚不清楚。如果您在这方面有任何观察,请告诉我们!顺便说一句:替代对象实现是进入 PyPy 开发的一个好方法,因为您只需要了解 PyPy 的一小部分就可以完成它们。而且它们也很有趣!

对象优化

整数优化

缓存小整数

与 CPython 类似,可以启用小整数对象的缓存,以避免在进行简单算术运算时每次都进行分配。每次创建一个新的整数对象时,都会检查该整数是否足够小,可以从缓存中检索。

此选项默认情况下是禁用的,您可以使用 –objspace-std-withprebuiltint 选项启用此功能。

整数作为标记指针

在使用整数时,节省内存的更激进方法是“小整数”整数实现。它是另一种整数实现,用于只需要 31 位(或在 64 位机器上需要 63 位)的整数。这些整数通过将最低位设置为区分它们与普通指针来表示为标记指针。这完全避免了装箱步骤,从而节省了时间和内存。

您可以使用 –objspace-std-withsmalllong 选项启用此功能。

字典优化

字典策略

字典策略是字典(和列表)的一种实现方法,它允许使用字典数据的专门表示,同时仍然能够在以后需要时切换回通用表示。

字典策略始终启用,默认情况下,对于仅包含字符串键、仅包含 Unicode 键和仅包含整数键的字典,存在特殊策略。如果使用其中一种专门策略,则字典查找可以使用更快的哈希和比较来处理字典键。当然,也存在针对通用键的策略。

标识字典

我们还有一种专门针对“按标识”比较的类实例键的策略,这是默认策略,除非你覆盖了 __hash____eq____cmp__。此策略仅与新式类一起使用。

映射字典

映射字典是与字典策略一起使用的特殊表示。此字典策略仅用于实例字典,并尝试使实例字典使用更少的内存(实际上,通常内存行为应该与使用 __slots__ 类似)。

其原理如下:同一类的多数实例具有非常相似的属性,甚至在执行 __init__() 时以相同的顺序将这些键添加到字典中。这意味着所有这些实例的字典看起来非常相似:它们具有相同的键集,但每个实例的值不同。共享字典所做的是将这些公共键存储到一个公共结构对象中,从而节省了单个实例字典中的空间:实例字典的表示只包含一个值列表。

列表优化

范围列表

范围列表解决了 xrange 内置函数解决得不好的同一个问题:range 即使结果列表只用于迭代,也会分配内存。范围列表是列表的不同实现。它们仅作为对 range 的调用的结果而创建。只要结果列表在未被修改的情况下使用,该列表只存储范围的开始、结束和步长。只有当有人修改列表时,才会创建实际的列表。这提供了 xrange 的内存和速度行为以及 range 的通用性,并且使 xrange 基本上变得无用。

此功能默认情况下作为 –objspace-std-withliststrategies 选项的一部分启用。

用户类优化

方法缓存

引入了一个方法缓存,其中存储了方法查找的结果(这可能涉及在类的基类中进行多次查找)。方法缓存中的条目使用从要查找的名称、调用站点(即字节码对象和当前程序计数器)以及发生查找的类型的特殊“版本”计算的哈希值存储(每次类型或其基类之一发生更改时,此版本都会递增)。在随后的查找中,可以使用缓存的版本,只要实例没有遮蔽其任何类属性。

此功能默认情况下启用。

解释器优化

特殊字节码

LOOKUP_METHOD & CALL_METHOD

Python 面向对象编程版本的一个不寻常特性是“绑定方法”的概念。虽然这个概念很干净也很强大,但对象的分配和初始化并非没有性能成本。我们已经实现了一对字节码来减轻这种成本。

对于给定的方法调用 obj.meth(x, y),标准字节码如下所示

LOAD_GLOBAL     obj      # push 'obj' on the stack
LOAD_ATTR       meth     # read the 'meth' attribute out of 'obj'
LOAD_GLOBAL     x        # push 'x' on the stack
LOAD_GLOBAL     y        # push 'y' on the stack
CALL_FUNCTION   2        # call the 'obj.meth' object with arguments x, y

我们通过将方法查找与方法调用分开来改进这一点,这与其他一些方法不同,但使用值栈作为缓存而不是构建临时对象。我们扩展了字节码编译器以(可选地)为 obj.meth(x, y) 生成以下代码

LOAD_GLOBAL     obj
LOOKUP_METHOD   meth
LOAD_GLOBAL     x
LOAD_GLOBAL     y
CALL_METHOD     2

LOOKUP_METHOD 包含与 LOAD_ATTR 完全相同的属性查找逻辑 - 因此完全保留语义 - 但将两个值推送到栈上而不是一个。这两个值是绑定方法对象的“内联”版本:im_funcim_self,即分别对应底层 Python 函数对象和对 obj 的引用。这只有在属性实际上引用类中的函数对象时才有可能;当这种情况不成立时,LOOKUP_METHOD 仍然会推送两个值,但其中一个 (im_func) 只是 LOAD_ATTR 本来会返回的常规结果,而另一个 (im_self) 是解释器级别的 None 占位符。

在推送参数后,上述示例中栈的布局如下(栈向上增长)

y (第二个参数)
x (第一个参数)
obj (im_self)
function object (im_func)

CALL_METHOD N 字节码通过检查 N 个参数下方栈中的 im_self 条目来模拟绑定方法调用:如果它不是 None,则它被认为是调用栈中 im_func 对象的额外第一个参数。

总体影响

这些各种优化对性能的影响毫不奇怪地取决于正在运行的程序。使用默认的多字典实现,该实现只是对字符串键字典进行特殊情况处理,在所有基准测试中都是一个明显的胜利,将结果提高了 15-40%。

另一个优化,或者更确切地说是一组优化,具有统一的良好效果,是两个“方法优化”,即方法缓存以及 LOOKUP_METHOD 和 CALL_METHOD 操作码。在一个高度面向对象的基准测试(richards)中,它们结合起来可以使速度提高近 50%,即使在极度非面向对象的 pystone 基准测试中,改进也超过了 20%。

在构建 pypy 时,所有普遍有用的优化默认情况下都处于打开状态,除非你使用 --opt 选项显式降低翻译优化级别。