PyPy 为什么会比 CPython 还要快?

  • Post author:
  • Post category:python


你本来有个 python 代码:

def add(x, y):
    return x + y

然后 CPython 执行起来大概是这样(伪代码):

if instance_has_method(x, '__add__') {
    return call(x, '__add__', y) // x.__add__ 里面又有一大堆针对不同类型的 y 的判断
} else if isinstance_has_method(super_class(x), '__add__' {
    return call(super_class, '__add__', y)
} else if isinstance(x, str) and isinstance(y, str) {
    return concat_str(x, y)
} else if isinstance(x, float) and isinstance(y, float) {
    return add_float(x, y)
} else if isinstance(x, int) and isinstance(y, int) {
    return add_int(x, y) 
} else ...

这下能看出来因为 Python 的动态类型,一个简单的函数里面要有这么多判断才能正确执行。然后这还没完,你以为里面把两个整数相加的函数,就是 C 语言里面的 x + y 么?naive。

实际上 Python 里面一个 int 大概是个这样的结构体(也是伪代码,真实情况要比这个复杂):

struct {
    prev_gc_obj *obj
    next_gc_obj *obj
    type int
    value int
    ... other fields
}

然后每个 int 都是这样的结构体,还是动态分配出来放在 heap 上的,里面的 value 还不能变,也就是说你算 1000 这个结构体加 2000 这个结构体,得出来 3000 这个结构体,还要去 heap 上 malloc 一个结构体来。

CPython 每次就这么老老实实的执行这个过程,就算你每次调用 add 函数都是只传两个整数。

然后 pypy 执行的时候,发现执行了一百遍 add 函数,发现你 TM 每次都只传两个整数进来,那我何苦每次还给你做这么多计算,于是当场生成了一个类似 C 的函数:

int add_int_int(int x, int y) {
  return x + y;
}

然后当场编译成机器码,然后你下次每次调用 add(1, 2) 的时候,直接就调用这个 “Native” 的函数,你说你 Pypy 快不快?

上面这个过程就叫做 Just In Time 编译,也就是 JIT,肯定比 CPython 的执行速度要快了。当然 JIT 也有很多问题,比如编译本身也很花时间,如果这段代码本来就只执行一次,需要1s,但是你把它编译出来需要10s,那 JIT 就得不偿失了。所以很多 JIT 实现都会先解释执行,然后确定了一段代码经常被执行之后,再进行编译。并且分多层 JIT,比较初级的对编译出来的机器码不做比较复杂的优化什么的。

Pypy 和 Unladen Swallow 对比的话,最大的不同点就是前者基本完工了(可用),后者做一半坑了。另外的区别的话,Unladen Swallow 立项的目的是完全兼容 CPython,于是直接再 CPython 的 codebase 上改,想要把之前 CPython 解释执行的部分改成 JIT 执行,然后内存模型不改,于是就能兼容 Cpython(包括原生扩展)。Pypy 的话基本都是自己重写的,(原生扩展)兼容性没有放在第一位了。