PyPy 为什么会比 CPython 还要快?

软件工程师、主攻高级编程语言虚拟机的设计与实现

640 👍 / 26 💬

问题描述

关于PyPy的性能网上有很多资料,如[1];在oolps2009会议的论文[2]里也有对性能的说明(我没有完全看懂),大意是讲:原生的解释器无法获得程序的一些信息,无从优化,而PyPy就可以。我的问题是PyPy为什么比CPython还要快?


谢谢,江疆的回答,但我仍然有疑问。

CPython也有几个带JIT的分支,如Unladen Swallow[4],那么Unladen Swallow或者类似的方案和PyPy相比呢?


有个名词在现有的回答下面都没人提到——partial evaluation。

这是PyPy的实现机制中的一个核心思想。

Truffle/Graal和PyPy是应用了partial evaluation的现代编译器/运行时项目的代表作,不过它们的具体做法有许多有趣的差异,by design。

这里面参与的部分是:

  1. 用户写的Python应用代码
  2. 用RPython写的Python解释器
  3. 用RPython写的一个实现了partial evaluation机制的tracing JIT编译器

(3)会在运行时对(2)做编译,并且把运行时发现的(1)当作(2)的输入来作特化,最终达到把(1)编译成机器码的结果。

相对其它一些常见的tracing JIT编译器(例如TraceMonkey、LuaJIT2等),PyPy里的(3)最特别的地方就是它不只需要trace一层(用RPython写的解释器)并特化,而必须trace两层(再加上用户的Python代码)并高度特化。这种trace两层的做法叫做meta-tracing

这种做法的好处是,当有人想写一个新的编程语言的实现时,只要在PyPy框架下用RPython编写一个对应上面(2)的语言解释器,就可以借助作为meta-compiler的(3)的部分,得到一个能支持把(1)JIT编译到机器码的高性能实现。

重要的事情说三遍:

写解释器,得到JIT编译器。

写解释器,得到JIT编译器。

写解释器,得到JIT编译器。

例如说,基于PyPy框架实现的Ruby语言项目——Topaz,一上来就比当时最快的Ruby实现——JRuby的默认模式要快得多了。

JRuby的性能依赖于它底下运行的JVM的性能。首先JRuby要把Ruby代码“JIT”编译到Java字节码,然后依赖JVM里的JIT编译器把字节码再编译到机器码。这样JRuby就不用自己实现完整的JIT编译器也能得到不错的性能了。

而实现Topaz通用并不需要自己实现JIT编译器,只要按照PyPy框架的指引,用RPython实现一个带有足够annotation的解释器,就自动得到了高性能的带JIT编译器的实现。何乐而不为。

(关于Ruby实现之间的性能比较,请参考这个benchmark页面:

jruby.org/bench9000/

。如果想看清楚一点的话请把最后一项(Graal那个)去掉,因为它实在快太多导致别的实现的竖线都被压矮了…)

而CPython则是写解释器,得到解释器。在执行机制上PyPy就已经有潜在优势了。

===========================================

不过要应题的话,其实PyPy除了因为有JIT编译器而比纯解释执行的CPython快之外,其实更重要的是PyPy在实现Python的时候采用了更多runtime方面的优化,例如说更优化的对象布局、更优化的虚方法查找等。

这些其实才是更重要的。而那些试图基于CPython runtime加上JIT的项目(例如Unladen Swallow和Pyjion)之所以效果都一般,也正是因为被CPython糟糕的runtime设计而绑住了手脚,无可奈何。

===========================================

下面开些额外的废话,对实现细节不关心的同学可以忽略。

评论区里

@bhuztez

大大提醒说PyPy用的是meta-tracing而不是partial evaluation。嗯这个要严谨地说的话是对的。

但其实meta-tracing的本质思路和目标跟原本Futamura第一映射是完全吻合的,要说meta-tracing是广义上的partial evaluation实现我觉得是OK的,所以本回答开头会一开始就提到“partial evaluation”。

要知道trace-based compilation跟profiling+partial compilation / regional compilation其实最终效果可以非常非常的相似,只是中间所走的路线略微不同而已。随着trace-based compilation技术的发展,大家渐渐意识到了两者的相似之处,所以两者比较新的设计也渐渐变得相似。

例如说trace-based compiler中常见的IR,经历了这样的发展:

发展到trace-graph之后,其实就跟一个带profiling的传统JIT编译器,在profile一段时间后选择只编译“热”的部分,从结果上看是非常非常非常相似的。开始收集trace的过程就权当跟传统系统中的profiler的作用一样,只不过可以看作是把profile嵌入在一小块可执行的代码里而已。

关于PyPy的meta-tracing与Truffle/Graal所实现的partial evaluation的联系与差异,请参考一篇于OOPSLA 2015发表的考察论文:

Tracing vs. Partial Evaluation

之前也有一些比较论文或文章,例如: