PyPy

PyPy 是一个 Python 的实现,同时也是一个动态语言实现框架。

本章假定读者熟悉如字节码和常量叠算等有关解释器和编译器的基本概念。

19.1. 一点历史

Python 是一个高层次动态编程语言。它是由荷兰程序员 Guido van Rossum 在20世纪80年代末发明的。Guido 最初的实现是一个用 C 语言编写的传统的字节解释器,人称 CPython。现在有许多其他的 Python 实现。其中最引人注目的有用 Java 编写的并允许 Java 代码接口的 Jython,用 C# 编写并允许和微软 .NET 框架接口的IronPython,以及本章的主题 PyPy。 CPython 仍然是使用最广泛的实现,也是当前唯一支持下一代 Python 3 语言的实现((译者注:翻译时 PyPy 已经支持 Python 3 了))。本章将谈谈让 PyPy 与其他 Python 实现乃至其他任何动态语言实现都有所不同的一些设计决策。

19.2. PyPy 概览

除了微不足道的少量 C 代码,PyPy 完全是用 Python 写成的。PyPy 代码树包含两个主要部分:Python 解释器和 RPython 翻译工具链。 Python 解释器是面向程序员的运行库,人们使用 PyPy 作为 Python 实现时会进行调用。它实际上是用 Python 的一个子集 Restricted Python(通常缩写为 RPython)写成的。用 RPython 编写 Python 解释器的目的是让解释器可以输出给 PyPy 的另一个重要组成部分——RPython 翻译工具链。 RPython 翻译器会把 RPython 代码转换为一个选定的低级语言,最常用的是 C。这使得PyPy成为一个自我托管的实现,也就是说它是用它自己实现的语言写成的。我们在本章后文中还会看到,RPython 翻译也让 PyPy 成为一个普适的动态语言实现框架。

PyPy 强大的抽象使之成为最灵活的 Python 实现。从不同的垃圾回收到各种翻译优化参数,它有近200个不同的配置选项。

19.3. Python 解释器

由于 RPython 是 Python 的真子集,PyPy Python 解释器可以不经翻译地在另一个 Python 实现上运行。当然,这会非常慢,但这样我们就可以快速测试解释器的变化。这也让我们可以使用普通的 Python 调试工具来调试解释器。 PyPy 解释器的大多数测试可以同时运行在无翻译和有翻译的解释器上。这让开发时的快速测试成为可能,并保证了有翻译和无翻译的解释器的行为一致。

在大多数情况下,PyPy Python 解释器的细节和 CPython 非常类似,PyPy 和 CPython 在解释时使用的字节码和数据结构几乎完全一样。两者之间的主要区别在于 PyPy 有一种很聪明的抽象,称为//对象空间//(简称objspaces)。objspace 封装了代表和操作 Python 数据类型的所有知识。例如,对两个 Python 对象执行二元操作或获取对象的一个属性,都完全由 objspace 处理。这让解释器无需知道 Python 对象的任何实现细节。字节码解释器把 Python 对象看成是黑盒子,并在需要操作它们时调用 objspace 方法。例如,下面是 ‘’BINARY_ADD’’ 机器码的一个粗糙的实现,在两个对象用 + 运算符结合的时候会调用它。请注意解释器如何不去检查运算符;所有处理都立即被委托给 objspace。

1
2
3
4
5
def BINARY_ADD(space, frame):
object1 = frame.pop() # pop left operand off stack
object2 = frame.pop() # pop right operand off stack
result = space.add(object1, object2) # perform operation
frame.push(result) # record result on stack

objspace 的抽象有许多优势。新的数据类型实现可以在不修改解释器的情况下进行交换。并且因为 objspace 是对对象进行操作的唯一途径,objspace 可以拦截、代理或者记录对对象的操作。通过使用 objspace 强大的抽象,PyPy实验了形实转换程序和污染。通过形实转换程序,结果被懒惰地,但同时又完全透明地按照需求被计算出来了。污染则是指任何对对象的操作都将触发异常(在向不被信任的代码中传递敏感数据时很有用)。然而,对于 objspace 的最为重要的应用将会在 [[pypy_wiki_1#19.3. RPython 翻译器|19.4 节]]中进行讨论。

在平常的 PyPy 解释器中使用到的 objspace 被称之为标准 objspace(简称为std objspace)。除了 objspace 系统中提供的抽象之外,标准 objspace 还提供了另一个层次的间接方法;一种数据类型可能有多种实现方式。然后,对数据类型的操作会采用多方法发送。这使得对一个给定的数据能采用最为高效的表示形式。例如,Python的 long 型(表面上是大整数数据类型)在足够小的时候可以被表示为一个标准机器字长的整数,只有在必要的时候才会使用内存和计算代价更高的任意精度 long 型实现,甚至可可以使用标记指针来实现 Python 的整形数。容器类型同样可以被特化针对特定数据类型。例如,PyPy 中有针对字符串键值的字典类型(Python 的哈希表数据类型)实现。同样的数据类型可以被不同的实现方式所表示,这对于应用级代码是完全透明的;一个为字符串类型特化的字典和普通的字典是相同的,当有非字符串类型的键值放入时将会进行退化。

PyPy 对解释器层(interp-level)与应用层(app-level)的代码进行区分。编写大部分解释器所采用的解释器层代码必须是 RPython 并且被翻译过。这层代码直接作用于 objspace 和封装的 Python 对象。应用层代码一般通过 PyPy 字节码解释器运行。与 C 和 Java 相比,PyPy 的开发者们认为在翻译器的一些部分使用纯应用层代码是最方便的,这和解释器层的 RPython 代码一样简单。因此,PyPy 支持在解释器中内嵌应用层代码。例如,用于将对象写到标准输出的 Python ‘’print’’ 语句的功能就是通过应用层 Python 实现的。内置模块同样也可以同时用两种层次的代码写成。

19.4. RPython 翻译器

RPython 翻译器是一个由几个向下阶段组成的工具链,用于将 RPython 重写至目标语言,例如 C 语言。图 19.1 描述了较高层的翻译。翻译器自身使用(不严格的)Python 编写的,并在链接到 PyPy 的 Python 解释器,具体原因将会在稍后解释。

图 19.1: 翻译步骤

翻译器所做的第一件事是将 RPython 程序加载到其进程(这一过程通过一般 Python 模块加载支持完成)。RPython 在一般动态的 Python 之上添加了一组限制。例如,函数不能在运行时创建,一个变量没有存放不匹配类型的可能性,比如一个整型数和一个对象实例。但是当程序被翻译器初次载入后,其将会运行在一个一般的 Python 解释器上,并且能够使用 Python 所有的动态特性。PyPy 的 Python 解释器是一个庞大的 RPython 程序,其大量使用这一特性用于元编程。举例来说,它为了标准的 objspace 多方法分派生成代码。唯一的要求是,程序在翻译器开始下一阶段翻译之前是合法的 RPython 代码。

翻译器通过一个名为抽象解释的过程为 RPython 程序构建流图。抽象解释重用了 PyPy 的 Python 解释器通过一个名为流 objspace 特殊的 objspace 来解释 RPython 程序。回忆一下,Python 解释器把程序中的对象看成是黑盒,通过调用 objspace 来执行所有的操作。流 objspace 与标准的 Python 对象集不同,只有两个对象:变量和常量。变量表示翻译时仍然不知道的值,而常量则表示已知的不变的值。流 objspace 有一个用于常量叠算的组件:如果需要对全是常量的参数执行操作,它将对其进行静态评估。在 RPython 中,不可变的、必须是常量的东西范围比标准 Python 中更广。例如,在 Python 中显然可变的模块在流 objspace 中是常量,并且必须被 objspace 常量叠算,因为它们在 RPython 中不存在。在 Python 解释器解释 RPython 函数字节码时,流 objspace 记录会记录它所执行的操作。它谨慎地记录条件控制流的所有分支。函数抽象翻译的最终结果是一个由连接的块组成的流图,每个块包含一个或多个操作。

下面是一个流图生成过程的示例,这是一个简单的阶乘函数:

1
2
3
4
def factorial(n):
if n == 1:
return 1
return n * factorial(n - 1)

上面函数的流图如下所示:

图19.2: 阶乘的流图

这个阶乘函数被分成了若干块,每块都包含了流空间记录的操作。每块都有输入参数和一个对变量和常量的操作表。第一个块在结尾处有一个选择,它将决定控制流接下来将进入哪一个块。退出选择一般基于一些变量的值或在块最后的操作处是否有异常出现。控制流沿着块之间线移动。

在流 objspace 生成的流图是静态单分配形式的,或者 SSA, 一种在编译器中常见的中间表示形式。SSA 的关键特性是每一个变量只会被分配一次。这一特性简化了很多编译器转化和优化的实现。

在一个函数图被生成之后,注释阶段就会开始。注释器为每一个操作的参数和结果分配一个类型。例如,前面提到的阶乘函数的接收和返回类型都被分配为整型数。

下一个阶段叫做 RTyping。RTyping 使用从注释器获得的类型信息把流图中的每一个高层次操作扩展成低层次的操作。这是目标后端所关心的翻译的第一部分。后端为 RTyper 选择一套类型系统来特化代码。目前 RTyper 拥有两套类型系统:为像 C 语言这样的后端准备的低层次类型系统,和一个有类的高层次类型系统。高层 Python 的操作和类型被转化到这个类型系统的层次。例如,含有被注释为整型数操作数的 ‘’add’’ 操作在低层次类型系统下会生成一个的 ‘’int_add’’ 操作。像哈希表查找这样更复杂的操作会生成函数调用。

在 RTyping 之后,将会进行低层次的流图的优化。它们大多是传统的编译处理,如常量折叠、存储下沉、删除无用代码等等。

Python 代码含有尤其频繁地动态存储分配。RPython 作为 Python 的衍生物继承了这种集中分配的形式。但是,在很多情况下,分配对函数来说是暂时的、局部的。//分配消除//是针对这类问题的优化。分配消除通过尽可能地把先前动态分配的对象“扁平化”为组件标量来消除这些分配。

为了了解分配消除是怎么工作的,下面是一个用迂回的方式计算平面上两点之间欧式距离的函数:

1
2
3
4
def distance(x1, y1, x2, y2):
p1 = (x1, y1)
p2 = (x2, y2)
return math.hypot(p1[0] - p2[0], p1[1] - p2[1])

在 RTpye 初始化之后,函数题包含了以下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
v60 = malloc((GcStruct tuple2))
v61 = setfield(v60, ('item0'), x1_1)
v62 = setfield(v60, ('item1'), y1_1)
v63 = malloc((GcStruct tuple2))
v64 = setfield(v63, ('item0'), x2_1)
v65 = setfield(v63, ('item1'), y2_1)
v66 = getfield(v60, ('item0'))
v67 = getfield(v63, ('item0'))
v68 = int_sub(v66, v67)
v69 = getfield(v60, ('item1'))
v70 = getfield(v63, ('item1'))
v71 = int_sub(v69, v70)
v72 = cast_int_to_float(v68)
v73 = cast_int_to_float(v71)
v74 = direct_call(math_hypot, v72, v73)

这段代码在很多方面还欠缺优化。两个从未离开函数的元组被分配内存了。此外,还有不必要的元组域的间接访问。

运行分配消除生成了下面简洁的代码:

1
2
3
4
5
v53 = int_sub(x1_0, x2_0)
v56 = int_sub(y1_0, y2_0)
v57 = cast_int_to_float(v53)
v58 = cast_int_to_float(v56)
v59 = direct_call(math_hypot, v57, v58)

元组的内存分配被彻底移除了,间接访问被扁平化了。后面,我们将介绍另一个和分配消除相似的技术在 PyPy JIT 的 Python 应用层被应用(19.5 节)。

PyPy 也会进行函数内联。像低层次语言一样,内联能提高 RPython 的性能。令人惊奇的是,这同样可以缩小最后生成的二进制文件的大小。这是因为内联后可以进行更多的常量叠算和分配消除,这总体上简化了代码。

优化后的程序和低层次流图会传递到后端生成源代码。在生成 C 代码之前,C 的后端必须进行一些额外的转化。其中之一是异常转化,异常处理将会被重写成手动栈展开。另一个则是栈深度检查的插入。这一操作使得如果运行时递归太深,会抛出一个异常。程序调用图会计算循环找到需要栈深度检查的地方。

还有一个 C 后段会执行的转化是添加垃圾回收(GC)。RPython 和 Python 一样是有自动垃圾回收的语言,但是 C 语言没有,所以垃圾回收需要被添加进去。为了达成这一目标,一个垃圾回收转换器将程序的流图转换为垃圾回收程序。PyPy 的垃圾回收转换器为翻译如何抽象一般细节做了一个很好的示范。在 CPython 中,其使用了引用计数,解释器的 C 代码必须小心翼翼地跟踪它正在处理的 Python 对象的引用。这不仅使整个代码库中的垃圾回收方案变得复杂,还容易产生微妙的人为错误。PyPy 的 垃圾回收转换器解决了这两个问题,它允许不同的垃圾回收方案进行无缝地切换。我们可以简单地调整一个翻译的配置选项来评估一个垃圾回收实现(PyPy 有很多实现)。除了转换器 bug,垃圾回收转换器同样不会出现引用错误或当一个对象不再被使用时没有通知垃圾回收器的情况。垃圾回收抽象的威力使得垃圾回收的实现实际上通过翻译器不可能很复杂。举个例子,一些 PyPy 的垃圾回收实现需要一个//写隔离//。写隔离是指一个每次垃圾回收管理的对象被放置在另一个垃圾回收管理的数组或结构中时都会进行的检查。插入写隔离的过程如果手工完成会费力而且充满错误,但是用垃圾回收转换器自动完成就很轻松。

C 后端最终会生成 C 源代码。通过低层次流图生成的 C 代码是一个有着很多 ‘’goto’’ 和晦涩命名的变量的丑陋混乱的代码。使用 C 语言的一个优势是 C 的编译器可以做大部分复杂的静态转化工作,这些工作对于最终的二进制循环优化和寄存器分配是必须的。

19.5. PyPy JIT

Python 和大多数动态语言一样,都牺牲了部分效率使其更为灵活。PyPy 的结构尤其灵活与抽象,这使得其很难进行非常快速的解释。强大的 objspace 和标准 objspace 的多方法抽象都是有代价的。因此,PyPy 解释器比 CPython 慢四倍。为了补偿这一缺陷并且挽救 Python 被称作缓慢的语言的声誉,PyPy 拥有一个即时的编译器(JIT)。JIT 在程序运行时将经常使用的代码编译成汇编语言。

PyPy JIT 利用了 19.4 节提到的 PyPy 独特的翻译架构的优势。PyPy 实际上并没有针对 Python 的 JIT;它有一个 JIT 生成器。JIT 的实现就和翻译时其他的选择一样简单。一个需要 JIT 生成的解释器只需两个特殊的函数:JIT 提示

PyPy 的 JIT 是一个跟踪型的 JIT。这表示它会检测“热的”(经常运行的)循环以此编译成汇编优化。当 JIT 决定要编译一个循环的时候,它会记录循环中一趟的操作,这一过程被称作跟踪。这些操作会被依次汇编成机器码。

如同前文提到的那样,JIT 生成器要生成一个 JIT 只需要解释器的两个提示:’’merge_point’’ 和 ‘’can_enter_jit’’。’’can_enter_jit’’ 告诉 JIT 在解释器中循环从哪里开始。在 Python 解释器中,这是 ‘’JUMP_ABSOLUTE’’ 字节码结束的地方。(’’JUMP_ABSOLUTE’’ 使得解释器跳到应用层循环的开头。)’’merge_point’’ 告诉 JIT 从哪里回到解释器是安全的。在 Python 解释器中,这是分配循环的字节码开始的地方。

JIT 生成器在翻译的 RTyping 阶段之后运行。回忆一下,这时程序的流图是有由几乎准备好生成目标代码的低层次操作构成的。JIT 生成器会定位解释器中上面提到的提示,并在运行时将它们替换为对 JIT 的调用。JIT 生成器接下来会编写每个解释器需要 JIT 化的函数的流图的序列化表示。这些序列化的流图被称为实时编译码(jitcodes)。现在整个解释器都通过低层次的 RPython 操作来描述了。实时编译码被保存在了最终的二进制文件中,在运行时会被调用。

在运行时,JIT 会为程序中运行的每个循环维护一个计数器。当一个循环的计数超过了设置的阈值时,JIT 就会被调用然后开始跟踪。跟踪中的关键对象被称作元解释器(meta-interpreter)。元解释器执行翻译过程中生成的实时编译码。它实际在解释主解释器,这就是它名字的由来。在它跟踪循环的时候,它会生成它正在执行的操作的列表,并用 JIT 的中间表示(IR)把他们记录下来。这个列表被称为循环的跟踪表。当元解释器接收到了对 JIT 化的函数(就是实时编译码存在的来源)的调用时,元解释器会进入这个函数并将它的操作记录到原始跟踪表中。因此,跟踪有扁平化函数调用栈的效果;跟踪中唯一的调用是对 JIT 所不知道的解释器函数的调用。

元解释器被强制用于特化循环迭代内容的跟踪表。举个例子,当元解释器遇见一个实时编译码编写的条件语句时,它当然要基于程序的状态选择一条路径。当它基于运行时的信息作出选择时,元解释器会记录一个被称为防护(guard)的 IR 操作。在条件分支中,条件变量上会有一个 ‘’guard_true’’ 或者 ‘’guard_false’’ 操作。大多数算数操作同样有防护,他们的作用是为了确保操作不会造成溢出。简要的说,防护在进行跟踪的时候将元解释器正在做的假设转化为代码。当汇编代码生成后,防护会确保汇编代码不会在一个未知的上下文环境中运行。当元解释器到达和开始跟踪时一样的 ‘’can_enter_jit’’ 操作时,跟踪就会结束。循环的 IR 现在就可以被传递给优化器了。

JIT 的优化器可以进行一些传统的编译器优化和许多为动态语言设计的优化,后者最重要的包括 虚拟对象可虚拟化对象

虚拟对象指的是已知的不会离开跟踪的对象,这意味着他们不会被作为参数被传递到外部的非 JIT 的函数调用。结构体和定长数组都可以是虚拟对象。虚拟对象不需要被分配内存,他们的数据可以被直接储存在寄存器或栈上。(这很像在翻译后端优化那一节中描述的静态内存分配消除阶段。)虚拟对象优化了 Python 解释器间接表示和内存分配带来的效率低下。例如,通过变成虚拟对象,被封装的 Python 整型数对象被拆分成简单的单字大小的整型数,这样就可以被直接存放在机器的寄存器中。

可虚拟化对象的行为与虚拟对象类似,但是必须在跟踪表之外(这意味着这些行为要被传递到非 JIT 的函数中)。在 Python 解释器中,框架对象保存变量的值和指令指针,它会被标记为可虚拟化的,使栈操作和栈上的其他操作可以被优化。尽管虚拟对象和可虚拟化对象很相似,然而它们在实现上却完全不同。可虚拟化对象在跟踪过程中由元解释器处理,而虚拟对象则是在跟踪优化的时候处理的,之所以会有这样的区别是因为可虚拟化对象可能会离开跟踪表,因此需要一些特殊的操作。具体来说,元解释器需要确保可能会使用到可虚拟化对象的非 JIT 函数实际上不会去尝试获取它的域。因为在 JIT 代码中,可虚拟化对象的域实际是储存在栈和寄存器中的,因此真正的可虚拟化对象相对 JIT 代码中的当前值而言可能会过期。在 JIT 生成过程中,访问了一个可虚拟化对象的代码会被重写以检查 JIT 汇编是不是在运行。如果正在运行,JIT 就会被要求从汇编的数据中更新域。此外在从外部调用回到 JIT 代码中时,程序的执行会回到解释器。

在优化之后,跟踪表已经准备好被汇编了。因为 JIT IR 已经很底层了,汇编代码的生成并不是很困难。大部分 IR 操作对应于一小部分 x86 汇编操作。寄存器的分配是一个简单的线性算法。此时,使用一个更为复杂的寄存器分配算法在后端所花费的大量时间和生成代码性能的微弱提升相比是不值得。汇编代码生成中最体现技巧的事垃圾回收集成和防护恢复。垃圾回收需要注意生成的 JIT 代码的栈的根。这通过垃圾回收中对动态根定位的特殊支持实现。

当一个防护失败时,编译出的汇编就不再合法,控制必须回到字节码解释器。这个回退事 JIT 视线中最困难的部分之一,因为需要从防护失败时的寄存器和栈状态中重建解释器的状态。对于每一个防护而言,汇编器会生成一个重建解释器状态所需要的所有变量的位置的详细描述。在防护失败时,运行跳转到一个解码这一描述的函数,并把恢复的变量传递到更高的层级用以重建。失败的防护可能是在一个复杂的操作码执行的中间,因此解释器不能只是从下一个操作码开始。PyPy 使用了黑洞解释器来解决这一问题。黑洞解释器从防护失效的点开始运行实时编译码直到下一个汇合点到达。在那里,真正的解释器恢复运行。黑洞解释器之所以叫这个名字是因为不像元解释器,它不会记录任何它所执行的操作。图 19.3 描述了防护失败的过程。

图19.3: 在防护失败时回退到解释器

就像上面所描述的一样,JIT 在经常变化条件的循环中并没有什么作用,因为防护失败会阻止汇编代码运行特别多的循环。每个防护都有一个失败计数器,当它的值超过一个特定的阈值之后,JIT 将从防护失败的点开始跟踪而不是回退到解释器。这一新的子跟踪表被称为。当跟踪到达循环结束位置时,桥就会被优化和编译,原来的循环在防护处会被打上补丁,使其跳转到新的桥而不是失败代码。通过这种方式,通过这种方式动态条件的循环实现了 JIT 化。

PyPy JIT 中所使用的技术到底有多成功呢?在这篇文章写下时,PyPy 在综合测试下几何平均运行时间比 CPython 快五倍。通过 JIT,应用层 Python 可能比解释器层的代码还要快。PyPy 开发者最近发现了一个奇特的问题,他们为了性能需要在应用层 Python 写解释层的循环。

更重要的是,JIT 并不是为了 Python 自己设计的,它可以应用在任何使用 PyPy 框架的解释器上。而且也可以不是一个语言解释器。例如,JIT 被用作 Python 的正则表达式引擎。NumPy 是一个非常强大的 Python 数组模块,常被用于数字计算和科学研究。PyPy 有一个实验性的 NumPy 重实现,利用 PyPy JIT 的能力来加速数组操作。虽然目前 NumPy 的实现才刚刚起步,但其表现让人期待接下来的发展。

19.6. 设计缺陷

虽然战胜了 C 语言,但用 RPython 写程序是一个令人沮丧的经历。它的隐含类型让人开始的时候很难适应。有些 Python 语言的特性不被支持还有一些特性会被严格限制。RPython 并不是所有的地方都被正式的决定了,翻译器所接受的对象可能会经常改变,因为 RPython 要适应 PyPy 的需求。这一章节的作者经常会为了写一个程序困在翻译器中一个半小时,只因为一个隐晦的错误。

RPython 翻译器是一个全程序分析器,这带来了很多实际问题。翻译代码中任何小修改都需要重新翻译整个解释器。这在一个现代高速的系统上大概要跑 40 分钟。这对于测试修改是如何影响 JIT 这一工作来说尤其恼人,因为测试性能需要一个翻译好的解释器。在翻译阶段整个程序要准备好意味着含有 RPython
的模块不能从核心解释器中剥离出来,单独构建载入。

PyPy 的抽象层次并不总是像理论上那样明晰。技术上 JIT 生成器仅需要上面提到的两个提示就能就应该能为语言生成一个极好的 JIT,但实际上有些代码上生成的比其他代码上生成的好。为了让 Python 解释器更加 “JIT 友好”已经做了很多的工作,包括更多的 JIT 提示和为 JIT 优化的新数据结构。

PyPy 的多层结构令追踪 bug 成为了一个艰辛的过程。Python 解释器的 bug 可能会直接存在于解释器代码或埋藏在 RPython 语义和翻译工具链的某处。尤其是当一个 bug 无法在一个没有翻译的解释器上重现时,调试会很艰难。这需要在近乎不可读的生成的 C 代码上运行 GDB。

即便是一个被严格限制的 Python 的子集,将其翻译到像 C 那样更低层次的语言也不是一件简单的事。19.4 节描述的向下传递实际上并不是独立的。函数在翻译过程中会被注释并转化为 RType,注释需要标明一些低层次的类型。因此 RPython 翻译器是一个相互依赖的错综复杂的网。翻译器可能会在几个地方执行清理工作,但这一工作即不简单也不有趣。

19.7. 过程记录

为了和其自身的复杂性作斗争,PyPy 采取了一些所谓的“敏捷开发”的方法。到目前为止其中最为重要的就是测试驱动开发。所有的新特性和 bug 修复都需要测试来确保它们的正确性。PyPy Python 解释器同样可以在 CPython 的回归测试套件下运行。PyPy 的测试驱动 py.test 已经被剥离了出来,现在在其他很多的项目中都有应用。PyPy 也有一个持续集成系统,它会运行测试套件并在很多不同的平台上翻译解释器。每天针对所有平台的二进制文件都会被生成并进行基准套件测试。所有的这些测试都是为了确保无论这个复杂的结构作出了什么改变,各个组件都能正常运行。

PyPy 项目中有强烈的实验文化。开发者被鼓励创建 Mercurial 仓库的分支。这样就可以在不影响主分支稳定的情况下尝试新的想法。分支并不总是成功的,有些被废弃了。要说的话,PyPy 的开发者都是顽强的。最为著名的是目前所使用的 PyPy JIT 实际是将 JIT 添加进 PyPy 的//第五次//尝试。

PyPy 项目以其可视化工具为傲,在 19.4 节中描述的流图是一个例子。PyPy 也有工具用来展示垃圾回收随时间变化的调用,检查正则表达式的匹配树。特别有趣的是 jitviewer,一个用来可视化地剥离 JIT 化函数的层次的程序,从 Python 字节码到 JIT IR 再到汇编。(图 19.4 中展示了 jitviewer。)可视化工具帮助开发者理解 PyPy 的层次是如何交互的。

图19.4: 展示 Python 字节码和对应 JIT IR 操作的 jitviewer

19.8. 总结

Python 解释器将 Python 对象视作黑箱,它的行为由 objspace 来处理。每个 objspace 都可以为 Python 对象提供特殊的延伸行为。这种利用 objspace 的方法也使得抽象的解释技术可以用来进行翻译工作。

RPython 翻译器的抽象让语言解释免于一些细节问题,例如垃圾回收和异常处理。同时,它通过使用不同的后端,使得在许多不同的平台上运行 PyPy 成为可能。

这种翻译结构很重要的一个应用就是 JIT 生成器。JIT 生成器的普适性让它可以被添加新的语言或者副语言(sub-language),例如正则表达式。得益于JIT 生成器,PyPy成为了最快的 Python 实现。

尽管 PyPy 的大多数的开发都集中在 Python 解释器,PyPy 可以应用于任何动态语言的实现。这些年以来,像 JavaScript, Prolog, Scheme, IO 的局部解释器都是用 PyPy 写的。

19.9. 学到的知识

最后是一些从 PyPy 项目中学到的知识:

反复重构通常是一个必要的过程。例如,最初构想时翻译器的 C 后段应该能够直接处理高层次的流图!反复几次之后才有了现在的多阶段翻译过程。

从 PyPy 中学到最重要的一点就是抽象的力量。在 PyPy 中,抽象屏蔽了实现相关的考量。例如,RPython 的自动垃圾回收允许开发者开发解释器的时候不用担心内存管理。同时,抽象会带来一些脑力的损耗。在翻译链上工作会使得翻译的不同层次一起涌入开发者的闹钟。抽象同样会掩盖 bug 所在的层次,抽象泄漏也是常见的问题,抽象泄漏指替换可互换的低层次组件打断了高层次代码。使用测试确保系统的所有部分正常运行是很重要的,这样一个系统的一个改变就不会影响另一个系统。更确切地说,因为抽象使用了很多间接的方式,所以程序的运行会变慢。

(R)Python 作为一个实现语言的灵活性使得测试新的 Python 语言特性(甚至是新的语言)变得容易。得益于其独特的架构,PyPy 将在 Python 和其他动态语言未来的实现上扮演重要角色。

翻译信息

感谢劳佳的1-3节的翻译,http://www.ituring.com.cn/article/3894 。本篇在其基础之上对其进行了修改并完成了整章的翻译。另外感谢 Zhichen Liu 对本人翻译的协助。

译者水平有限,欢迎大家提出修改意见,本人邮箱liujiyuan126@yeah.net