PyPy

PyPy是一个对于python的实现,和一个关于动态语言实现的框架。

这一章假设读者对一些基本的编译器和解释器的知识很熟悉,比如字节码和常量合并。

19.1 一点点历史

Python是一门高级语言,也是一门动态的程序设计语言,它是在上世纪80年代被荷兰的程序员Guido van Rossum发明出来的。Guido最初实现的版本是一个用C语言写成的经典的关于字节码的解释器,一般被广泛地称之为CPython。现在有许多其它的Python解释器,在其中最出名的是Jpython,IronPython和PyPy。Jpython是用Java语言编写的,并且允许使用java的接口,ironPython是用C#语言编写,并且使用微软的.NET架构的接口,PyPy则是这一章的主题。然而,CPython仍然是使用广泛的解释器,并且现在是唯一支持Python 3(下一代Python)的解释器。这一章将详细阐述Pypy的设计思想以及它与其它的Python解释器和其它动态语言的解释器所不一样的部分。

19.2 PyPy综述

除了数量极少可以忽略不计的C存根代码以外,PyPy完全是用Python语言编写的。PyPy的源代码树包含两个部分:Python解释器和RPython翻译工具链。Python解释器是面向程序员的运行库,人们使用PyPy来调用Python实现。它其实是由Python的一个子集——限制Python(缩写为RPython)写成的。使用RPython来编写Python解释器的目的是为了让解释器能够将结果输出到PyPy的第二个重要部分,即RPython翻译工具链。RPython翻译器接收到RPython代码,并将它们翻译成给定的低级别语言(比如C语言)。这就使得PyPy成为一个自足执行的实现,意味着它是使用它需要执行的源语言来写的。就像我们即将在这一章中看到的那样,RPython翻译器同样使得PyPy成为了一个通用的动态语言实现的框架。

PyPy强大的抽象功能使它成为了最灵活的Python解释器。它具有将近200种配置选项,从选择不同的垃圾回收机制,到改变不同的翻译优化的参数。

19.3 Python解释器

由于RPython是Python的一个真子集,因此PyPy Python解释器可以不经翻译就在另一个Python实现器上执行。当然,这样运行起来非常慢,但是我们可以快速检测到解释器的变化。因此,使用普通的Python调试工具同样可以用来调试这个Python解释器。大部分关于PyPy解释器的检测都可以同时运行在未经翻译的和经过翻译的解释器上,从而允许了开发过程中的快速检测,并且保证了未经翻译和经过翻译的解释器产生的结果是一样的。

大部分情况下,PyPy Python解释器的实现细节与CPython Python解释器十分相似;在解释过程中,PyPy和CPython使用了几乎相同的字节码和数据结构。两者最主要的区别在于PyPy使用了一个更加聪明的抽象机制,叫做对象空间(简写为objspaces)。一个objspace封装了所有代表和操作Python数据类型所需要的知识。例如,对两个Python对象进行位运算或者获取对象的一个属性可以直接用objspaces进行处理。这样解放了Python解释器,它不必知道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实验了形实转换和污点。在形实转换中,结果可能是“懒惰的”,但是已经按照需要被完全计算出来了。在“污点”中,任何在对象上面的操作都将会触发异常(这一点在从不被信任的代码上传递带有敏感信息的数据时很有用)。然而,对于objspaces的最为重要的应用,将会在19.4节中进行讨论。

在平常的PyPy解释器中使用到的objspace被称之为标准objspace(简称为std objspace)。除了objspace系统中提供的抽象之外,标准objspace还提供了另一个层次的间接方法(indirection);一种数据类型可能有多种实现方式。然后,在数据类型上的操作是采用多方法进行调度,这也允许了对一个给定的数据采用最为高效的表示形式。举个例子来说,Python的长型(表面上看是一个BIGINT数据类型),在它足够小的时候可以被表示为一个标准机器字长的整数,甚至可以用标记指针来实现Python的可用整数。容器类型同样可以被专门化为某一特定的数据类型。例如,Python中的字典类型(Python的哈希表数据类型)就可以为字符串键(string keys)进行专门实现。同样的数据类型可以被不同的实现方式所表示,这个事实对于应用级的代码来说是透明的;一个为string类型进行了专门化的字典和普通的字典是相同的,当有非字符串类型的元素放入时它将退化为正常的字典。

PyPy将解释器级别(interp-level)的代码和应用级(app-level)的代码进行区分。解释器级(interp-level)的代码,包含了解释器中的大部分代码,必定在RPython中并且已经被翻译过。它对objspace直接进行处理,并且封装Python的对象。应用级(app-level)代码通常是通过PyPy字节码解释器来运行的。就和解释器级(interp-level) RPython代码一样简单的是,相比于C或者Java,PyPy开发者发现在解释器的某些部分中使用纯应用级代码是最简单的。所以,PyPy支持对嵌入式的应用程序级代码的解释。例如,Python中的’’print’’功能(用来把对象写到标准输出),是在应用级的Python被实现的。内建模块同样可以一部分是解释器级代码,一部分是应用级代码。

19.4 RPython翻译器

RPython翻译器是由几个对语言层次进行降低的阶段构成的工具链。这些降低阶段负责将RPython重写成一个目标语言,最典型的是C语言。高层次阶段的翻译过程在图19.1中呈现。翻译器本身由(不受限制的)Python编写,并且与PyPy Python解释器紧密相连。

图19.1 翻译步骤

翻译器做的第一件事,就是把RPython程序装载到它自己的进程中。(这是通过普通Python模块的装载支持来完成的。)RPython在标准的、动态的Python基础上加了一系列的限制。举个例子,函数不能在运行时被创建,并且一个变量不能存储与它不兼容的类型,比如一个整型数和一个对象实例。但是,当一个程序被翻译器首先装载过之后,它就是在一个普通的Python解释器上运行,并且可以使用Python的所有动态特性。作为一个巨大的RPython程序,PyPy Python解释器大量使用这种特性用于元编程。例如,它为objspace的多方法分派来生成代码。唯一的要求是,在翻译器开始下一个阶段的翻译时,这个程序是一个有效的RPython程序。

翻译器通过一种叫做“抽象解释”(abstract interpretation)的过程生成了RPython程序的流图。抽象解释重用了PyPy Python解释器,通过一个叫做流objspace的特殊objspace来解释RPython程序。回想我们之前说的,Python解释器把程序中的对象看成是黑盒,通过调用objspace来执行所有的操作。流objspace,它不是Python对象的某一个标准的子集,它只含有两个对象:变量和常量。变量表示的是在翻译过程中所不知道的数值,所以常量表示的就是翻译过程中已经被知道的并且不会被改变的数值。流objspace有着对于常量折叠的最基本的操作:如果它要去执行一个所有参数都是常量的操作,它将对其进行静态评估。在RPython中,一成不变的和必须被定义为常量的东西比标准Python多。例如,模块,这在Python中被着重强调为可变,在流objspace中是常量,因为它在RPython中不存在,并且必须要被流objspace进行常量折叠。因为Python解释器对RPython有关函数和功能的字节码进行了解释,流objspace记录下了它需要执行的操作。它负责记录条件控制流结构的所有分支。对于一个函数(function)进行抽象解释的最终结果是一个包含有很多互相连接的块的流图,每一个块包含一个或者多个操作。

一个流图生成的典型例子就是顺序执行。考虑如下的一个简单的阶乘函数:

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语言这样的后端准备的低层次类型系统,和一个高层次的、含有类结构的类型系统。例如,一个含有被注释为整型数的操作数的’’add’’操作会生成一个具有低级别类型系统的’’int_add’’操作。像哈希表查找这样更复杂的操作会生成函数调用。

在RTyping之后,在低级别的流图上将会执行一些优化。它们大多是传统的编译处理,如常量折叠、存储下沉(store sinking)、删除死代码等等。

Python代码非常典型地含有频繁地动态存储分配。RPython,Python的衍生物,继承了这种分配并且使它更加集中(intensive pattern)。然而,在很多例子中,分配对函数来说是暂时的、局部的。去除分配(Malloc removal)是对解决这些问题的优化。去除分配是通过在可能的情况下把先前动态分配的对象“扁平化”成组件的标量来删除这些分配。

想要知道去除分配是怎么工作的,考虑如下的用一种迂回的方式来计算平面两点之间欧氏距离的函数:

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])

当它首先被RType之后,函数的主体部分含有如下的操作:

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)

在某些方面这个代码还不是最优的。从来没有逃脱出这个函数(escape the function)的两个元组也被分配了空间。此外,还有对元组字段的不必要的间接访问。

运行去除分配产生如下简洁的代码:

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)

对元组的分配已经被完全去除,间接访问也已经变平(flatten out)。稍后,我们将会看到在PyPy JIT中一个类似于去除分配的技术是如何运用到应用级Python中的(19.5节)。

PyPy同样进行对函数的内联操作。就像在低层次语言中一样,内联改善了RPython的性能。更令人惊讶的是,它同样减少了最终二进制代码的长度。这是因为它允许了更多可以用来减少代码长度的常量折叠和去除分配的实现。

现在在优化的,低层流量图程序,被传递到后端以产生源。在产生C代码之前,C语言后端还需要做一些额外的改变。其中的一个是异常转换,使用手动堆栈展开重写了异常处理过程。另一个是栈深度的嵌入式检查。如果递归的深度过于深,在运行时会抛出一个异常。在程序的调用图中,栈深度检查所需要检查的部分在计算周期中被发现。

C语言后端做出的另一个改变是加入了垃圾收集机制(GC)。RPython和Python一样,都是一个进行垃圾收集的语言,但是C语言不是,因此需要加入垃圾收集机制。为了实现这个机制,垃圾收集转化器把程序中的流图转变成一个垃圾收集程序。PyPy的垃圾收集转化器提供了一个极好的示范,关于翻译时如何抽象那些平凡的细节。在CPython中,使用的是引用计数,因此解释器C代码必须小心地对它所操纵的Python对象进行追踪。这不仅要对整个代码库的垃圾回收计划进行硬编码,还更容易产生一些人为错误。PyPy的GC转化器把两个问题同时解决了。它允许不同的垃圾回收机制,并且可以进行无缝进出交换。仅仅通过在翻译时调整一个配置选项来评价一个垃圾收集器的实现,这是微不足道的(PyPy的垃圾收集器的实现方式有多种)。GC转化器也没有出现模变换错误或者引用错误,以及当一个对象不再使用的时候忘记通知GC这样的错误。GC的抽象能力允许GC实现那些解释器硬编码所几乎不可能实现的内容。例如,几种PyPy的GC实现需要写屏障。写屏障是一种检查,每次当一个被GC管理的对象被插入一个被GC管理的数组或者结构体中时,写屏障都会被执行。插入写屏障的过程是费力且充满错误的,但是如果使用GC转化器来完成的话,这些费力和错误都将是微不足道的。

C语言后端可以最终产生C代码。从低级别流图生成的C代码,是一个包含了很多个’’goto’’和隐晦命名变量的非常丑陋的“烂摊子”。不过,写C语言代码的一个好处就是C语言编译器可以做很多非常复杂的数据转换的工作,这些工作需要对最终的二进制串进行优化,并且对寄存器进行分配。

19.5 PyPy JIT

如同大部分的动态语言一样,Python也牺牲了部分效率来保证高灵活度。PyPy的架构在灵活性和抽象方面尤其复杂,因此很难进行非常快速的翻译。强大的objspace以及标准objspace中的多种方法抽象的产生都是需要代价的。因此,一个PyPy解释器的翻译速度比CPython慢4倍。不仅要对这一缺陷进行补救,还要挽救Python作为一门“缓慢语言”的声誉,PyPy拥有一个即时编译器(通常被写作JIT)。在程序运行时,JIT将经常使用的代码翻译成汇编语言。

PyPy JIT使用了19.4节中描述的Python独特的翻译结构。PyPy实际上并没有针对Python的JIT,它拥有的是JIT生成器。JIT的实现就和翻译其它的部分一样简单。一个需要JIT生成的翻译只需要两个特殊的函数调用,JIT和暗示

PyPy的JIT是一个追踪JIT。这意味着它发现“热”(频繁运行)的循环,并且通过转化成汇编代码来优化它们。当JIT决定对一个循环进行编译,它记录下一趟循环中的操作,这一过程叫做追踪。这些操作最后都会被编译成机器代码。

就像上文中提到的那样,JIT生成器生成一个JIT的时候只需要来自翻译器的两个提示:’’merge_point’’和’’can_enter_JIT’’.’’can_enter_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_git’’操作相同的操作时,跟踪就停止了。循环IR可以被传递给优化器。

JIT优化器可以实现一些经典的编译器优化和许多专门为动态语言设计的优化。在经典编译器优化中最重要的是虚拟对象可虚拟化对象

虚拟对象是一些不会从追踪中逃脱出的对象,这意味着它们不会作为参数传递给外部的、没有被JIT化的函数调用。结构体和定长数组可以作为虚拟对象。虚拟对象不需要分配空间,它们的数据可以被直接保存在寄存器和栈中。(这很像在翻译后端优化中所提到的静态分配去除阶段。)虚拟对象的优化除去了python解释器中间接表示和内存分配所带来的效率低下问题。例如,通过变为虚拟对象,封装好的python整型对象被解封为基本的字类型的整型数,并且能被直接保存在机器的寄存器中。

可虚拟化对象和虚拟对象很相似,但是可以从追踪中逃脱(可以被传输给没有JIT化的函数)。在python解释器中,包含了变量值和指令指针的框架对象被标记为可虚拟化对象。这使得栈操作和栈上的其它操作可以被优化。尽管虚拟对象和可虚拟化对象时相似的,它们在实现方面没有任何的共同点。可虚拟化对象是在元解释器的追踪中被处理,而虚拟对象是在追踪优化中被处理。这是因为需要对可虚拟化对象进行特殊的处理,由于它们可能会逃脱出追踪。特别地说,元解释器需要确认那些可能会使用可虚拟化对象的、没有被JIT化的函数实际上不会去尝试获取它的域。这是因为在JIT代码中,可虚拟化对象的域被存储在栈和寄存器中,所以真正的可虚拟化对象相对于它在JIT代码中的当前值而言可能会过时。在JIT生成过程中,那些获取和使用可虚拟化对象的代码被重写,从而检查出JIT汇编代码是否在运行。如果在运行,JIT会从汇编语言的数据中更新这些域。此外,从外部的调用回到JIT代码中时,程序的执行会回到解释器。

在优化之后,跟踪表已经做好了汇编的准备。因为JIT IR已经很底层了,汇编代码的生成并不是很困难。大部分的IR操作仅仅和一小部分x86汇编操作相对应,并且寄存器的分配是一个简单的线性算法。在此时,将时间花费在使用更复杂的寄存器分配代码从而产生一个稍微好一点的代码上并不是一个合理的选择。在汇编代码生成中最为棘手的部分就是垃圾回收集成和防护恢复。垃圾回收需要注意生成的JIT代码的栈根,这通过垃圾回收中对动态根定位的特殊支持来实现。

当防护失败时,编译出的汇编代码就不再是合法的了,控制必须回退到字节码解释器。这个回退过程是JIT实现中最为困难的部分之一,因为在防护失败时解释器的状态必须由寄存器和栈的状态来重新构造。对于每一个防护,汇编生成器会生成一个包含所有重建解释器时所需要的状态的详细位置信息。当防护失败时,程序跳转到解码这个信息的函数,并将恢复信息传递给更高层次以便进行重建。防护失败可能是在一个复杂的操作符执行的中间,因此解释器没法立刻开始下一个操作码的执行。PyPy使用了黑洞解释器来解决这个问题。黑洞解释器从防护失败点开始执行JIT代码,直到达到了下一个归并点。在那里,真正的解释器将恢复运行。和元解释器不同,黑洞解释器不对任何它执行的操作进行记录,故因此而得名。图19.3描述了防护失败的过程。

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

就像上文描述的那样,JIT在经常变化的循环中并没有什么用,因为防护失败会阻止汇编代码运行非常多次的迭代过程。每个防护都有一个失败计数器,当它的值超过某个阈值时,JIT就开始对防护失败点进行追踪,而不是回退到解释器中。这个新的子追踪过程被叫做桥梁。当追踪到达循环结束的位置时,桥就会被优化和编译,原来的循环会在防护处跳转到新的桥而不是失败代码。通过这种方式,动态条件循环实现了JIT化。

PyPy中使用的JIT技术究竟有多成功呢?在这篇文章写作的时候,PyPy在综合条件下的测试速度比CPython快5倍。通过JIT,应用级Python可能比解释器级的代码还要快。PyPy的开发者最近遇到了一个奇特的问题,为了性能他们需要在应用层Python写解释器层的代码。

更重要的是,JIT不是专门为Python设计的,这意味着它可以在任何使用PyPy框架的解释器中被使用,甚至可以不是一个语言解释器。例如,Python的正则表达式引擎中也使用了JIT。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的多层结构会将错误追踪变为一个实验室级的过程。Python解释器的错误可能会直接存在于Python解释器的程序中,或者埋藏在RPython的语义和翻译链的某处。尤其是一个错误无法在没有翻译的解释器上重现的时候,调试过程会很艰难。这需要在生成的近乎于不可读的C代码上运行GDB。

将Python的一个限制子集翻译成一个像C语言一样的更低层次的语言也不是一件容易的事情。19.4节中描述的向低层次的传递并不是真的独立存在。函数在翻译过程中会被注释并转化为Rtype,并且注释中含有低层次类型的信息,因此RPython翻译器是交叉依赖、错综复杂的。翻译器可以在几个地方进行清理工作,但是这个工作并不是简单有趣的。

19.7 过程记录

在降低其自身复杂性方面(见19.6节),PyPy使用了几种“敏捷”开发方法,在其中最重要的是测试驱动开发。所有的新功能和错误修复都需要测试来证明它们的正确性。PyPy Python解释器同样可以在CPython的回归测试套件中运行。PyPy的测试驱动py.test已经被剥离出来并且在很多其它的项目中被应用。PyPy也有一个持续集成系统,它可以运行测试套件,并且在多个平台上翻译解释器。针对所有平台的二进制文件每天都被生成并进行基准套件运行。所有的测试确保了无论复杂架构中出现什么变化,这些部件都会运行。

PyPy的项目中有着强烈的实验文化。它鼓励开发者创建Mercurial仓库分支,这样就能在不影响主分支的情况下实验自己的想法。这些分支不总是成功的,有些可能会被丢弃。如果要说的话,PyPy的开发者都是顽强的。最著名的例子就是,现在的PyPy JIT是把JIT加到PyPy上的第五次尝试!

PyPy项目同样以它的可视化工具为傲。在19.4节中描述的流图就是一个例子。PyPy同样有工具来显示随着时间的推移垃圾收集器的调用情况,并查看正则表达式的解析树。最特别的是jitviewer,一个用来可视化地JIT函数层次的程序,从python字节码到JIT IR再到汇编。可视化工具帮助开发者了解PyPy的各层是如何与其它层进行交互的。

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

19.8 总结

python解释器把python对象看做黑盒,并把所有行为处理交给objspace。每一个objspace都可以为python对象提供特殊的眼神行为。使用objspace方法的技术也使抽象的解释技术可以用来进行翻译工作。

Rpython翻译器允许像垃圾收集和异常处理这样的细节从语言翻译器中抽象出来。它同时也使得使用不同的后端在不同的运行平台上运行PyPy成为现实。

最重要的翻译架构的使用之一就是JIT生成器。JIT生成器的普适性允许JIT添加新的语言或者子语言,如正则表达式。因为有JIT生成器,PyPy是目前最快的python实现。

虽然大部分的PyPy开发工作都是关于python解释器的,但是PyPy可以被使用在任何动态语言的实现上。这几年以来,javascript、prolog和IO的部分解释器都是用PyPy写的。

19.9 学到的经验

最后,从PyPy项目中学到了如下的知识:

反复重构往往是一个必要的过程。例如,最初的设想时翻译器的C语言后端可以直接处理高层次的流图。现在的多阶段翻译过程是在反复了几次之后才最终产生的。

PyPy中最重要的就是抽象的力量。在PyPy中,抽象屏蔽了相关实现细节。例如,Rpython的自动垃圾收集器允许开发者开发解释器时不需要关心内存管理。同时,抽象带来了脑力上的损耗。翻译链的工作意味着翻译的不同层次全部涌入翻译者的脑中。同时,错误的层次被抽象掩盖会更加难以确定;抽象泄漏,在其中交换来本来应该是高层次代码中断时内部交换的代码,是一个长期存在的问题。使用测试来确保系统的所有部分都正常运行是很重要的,这样一个系统中的改变就不会对另一个系统造成破坏。更确切地说,抽象汇因为创建了过多的重定位而拖慢程序的速度。

作为一个实现语言,(R)python的灵活性使得在python的新语言特性(甚至于一个新的语言)上进行实验变得容易。因为它独特的架构,PyPy将来会在python和动态语言的事项上扮演更重要的作用。