几年前,我曾经参与过 Pyston的开发。Pyston 是 Dropbox 对 Python JIT 的又一次尝试。
和其他一些 Python JIT 尝试一样,Pyston 的第一次尝试也失败了。
不过现在 Pyston v2 将尝试一种新的方式。
以前的 JIT 方式
在介绍新的方式之前,先回顾一下已有的 JIT。具体来说,之前的 Python JIT都遵循着一种模式:把 Python 源码翻译成一种中间模式(字节码)
用栈式虚拟机或寄存器式虚拟机执行这些字节码
当一段代码执行很多次的时候,将这些代码编译成机器码,替换掉之前的字节码。从而节省速度
因此,JIT 对 Python 代码的提升,主要来自与用机器码替换掉字节码而节省的时间。
CPython 为什么慢?
CPython 慢已经是众所周知的事了,但 CPython 速度慢并仅仅是因为这一点。
CPython 速度慢还有几点来源:
漫长的分派过程:
如果剖析过 CPython 代码,不难发现在 CPython 中,哪怕一个最基本的加减法。在 CPython 中都要在主循环 PyEval_EvalFrameEx 中花费大量的时间。检查这个加减法的操作数能不能做运算,能不能原地处理,不能原地处理就要创建新的临时变量。
简单来说,就是不停的检测操作数的类型,然后看对应的类型中是否有对应的执行函数。如果A操作数中所有执行函数都检查完了,但还是没有可用的,那么再检测第二个操作数。如果两个操作数都有可用的执行函数,但一个是另一个的子类,那么应该先调用子类的函数。
总之,许多时间浪费在这种寻找真正的执行函数的路上了。
成员属性访问:
有些语言访问成员属性,a.b,本质上就是一个偏移量内存地址的访问。而 CPython 访问成员属性,本质上是在查询一个字典。同时由于 CPython 可以随意改变同一个成员名称的变量类型,比如可以先执行 a.b =foo,然后再执行 a.b() 。这时候这个成员属性已经不是之前的那个了,因此给查找带来了很大困难。
这一点可以通过内联缓存解决(inline caches)。
出路
之前的许多次 Python JIT 尝试,都在 JIT (即将部分代码转换成机器代码)和成员属性访问上取得了一些成功。但由于 Python 的动态性过强,已有的 Python JIT,要么在分派上节省的时间并不多,要么由于大规模重写运行时,带来了比较严重的兼容性问题。
Pyston v2 的思路是, 将 CPython 编译成 LLVM,在运行的时候,根据调用函数中参数的类型,比对被调用函数的参数类型,如果一致的话,就动态“指导”CPython 跳过某些分派,直接抵达对应的执行函数。
由于是在 CPython 的基础上动态修改 CPython 的运行时,所以并没有显著的兼容性问题。
目前这个计划刚刚重启,具体细节还在讨论中,现在的设想是直接以 Python 3.9 起步。如果能成功,那么将会是一个值得关注的新尝试。
这次我继续打算参与,如果其中的 JIT 涉及到与国产 CPU 的施配,我也会尝试支持。