GIL,即全局解释器锁(Global Interpreter Lock),是计算机程序设计语言解释器用于同步线程的工具,使得任何时刻仅有一个线程在执行。常见例子有CPython(JPython不使用GIL)与Ruby MRI
为何 PyPy 是趋势?
PyPy为优化和进一步的语言开发提供了更好的架构。对于大部分Python已有的问题,PyPy已经提供了解决方案:
先进的runtime和设计,在此文中作了介绍: The Architecture of Open Source Applications.
速度 - PyPy内置的JIT很棒,有时(其实很少)甚至可以与C相提并论。
GIL问题 - PyPy引入了一个很棒的STM实现,在 Armins Rigo的 文章中对此作了介绍。
粘合代码 - 使用cffi可以简单的处理C库,甚至比CPython的ctypes还要快!
异步编程。这方面,PyPy内置的 greenlet 比CPython的C扩展更适合一些。实际上,非堆栈式的概念(也即greenlet)在PyPy中还在继续发展(参看https://ep2012.europython.eu/conference/talks/the-story-of-stackless-python)
沙盒技术
应用在web和移动中。这里有Dusty的一些文章:Pushing Python Past the Present
PyPy已经支持多平台 (x86, 64_x86, ARM)
PyPy同时还包含了一个优秀的现代的架构,在 Jim Huang 的演讲 中做了介绍,演讲的要点是:
解释性语言的框架
用于研究和产品的组件组合 (不同的数据模型,垃圾回收 - 这些可以在具体的应用场景进行改变)
构建在基于组件链的功能架构之上(翻译工具链)。每一个步骤都会延续/转换程序模型、引入特征、各种后端(JVM, JavaScript, LLVM, GCC IR等等)。来看一下翻译链的例子:python 代码 -> 字节码 -> 函数对象 -> 类型推断 -> 垃圾收集器 -> JIT
包含大量在架构的不同层次开发的现代的优化技术 (这个任务可以简化)
相信让所有软件支持PyPy需要付出艰巨的努力 - 需要在现有的库上做很多工作。不过使用新的工具,编写支持PyPy和CPython的软件会比采用C扩展的方式更简单一些(在我们能做什么一节有介绍)。
我们已经有了一个使用C写的Python实现,一个用Java写的,一个用C#写的。接下来就是:用Python写的Python实现(有心人可能会注意这句话有点问题,是个死循环,^_^)
接下来我们看下什么地方容易搞混淆。首先,我们讨论下即时编译器JIT
JIT: 为什么会有这个?它的原理是什么?
大家都知道本地机器码的速度比字节码的速度快很多。那么,如果我们能将一些字节码直接编译成本地机器码再去运行它会怎样呢?我们必须花费一些代价(比如时间)在编译字节码到本地机器码上,如果最终的运行时间更快,那么这个代价就是值得的。这就是JIT编译器的动机,一种混合了解释器和编译器好处的技术。简单来讲,JIT就是想通过编译技术提升脚本解释器系统的速度。
例如, 被JIT(及时编译)采用的通用方法:
标识被经常执行的字节码。
把其编译成本地的机器码。
缓存该结果。
当同样的的字节码再次被执行的时候,会取预编译的机器码,得到好处(例如速度提升)。
这是关于PyPy的用处: 把JIT代入Python语言 (参看前面成果的附录).当然也有其他目的: PyPy 目标是成为一个跨平台,轻内存,支持stackless(译注:stackless为python提供微线程扩展,具有并发特性)。 但是及时编译才是它真正的卖点。 基于一系列时间测试的平均, 据说性能上能提高6.27倍. 停一下, 看看下面这个由PyPy Speed Center提供的图表:
PyPy具有巨大的潜力,在这一点上,它与CPython高度兼容所以它能运行Flask,Django等等)。
但关于PyPy有许多困惑 (例如,荒谬的建议创造一种PyPyPy…语言). 按我的观点,那主要是因为PyPy实际上是两种东西:
一种用RPython (非Python (我之前撒谎了))编写的Python解释器。 RPython是Python的子集,具有静态类型。在Python里,最难严格推论类型 (为什么这么困难,考虑下下面的事实:
Python
x =random.choice([1, "foo"])
将是合法的Python代码 (归功于 Ademan). x的类型是什么? 我们怎么推出变量的类型,当类型还没有被严格实施?)通过RPython,你牺牲了一些灵活性, 但使得内存管理和优化大大的容易。
一个编译RPython代码为了各种目标和加入及时编译的编译器。 默认平台是C,也就是从RPython到C编译器,但你也可以瞄准JVM或者其他。
只为清晰,我将引用这些PyPy(1)和PyPy(2)。
为什么你在同一层面下同时需要这两者? 你可以这样想一下:PyPy(1)是一个用RPython写的解释器,因此它能加载用户的Python代码并将它编译成字节码。但是这个用RPython写的解释器本身要能运行,就必须要被另外一个Python实现去解释,对不?
我们可以直接用CPython去运行这个解释器。但是这个还不够快
取而代之,我们使用了PyPy(2)(参考 RPython的工具链)去编译这个PyPy的解释器,生成其他平台(比如C, JVM或CLI)代码在我们的机器上运行,并且还加入了JIT特性。这个很神奇:PyPy动态的将JIT加入一个解释器,生成它自己编译器!(这就是核心原理:我们在编译一个解释器,并同时加入了另外一个单独的编译器到里面去)。
最终结果就是一个融合了JIT优化特性的单独的可执行文件,用来解释执行我们的Python源代码。这就是我们之前想要达到的效果。这么讲可能比较拗口,下面这张图可能会解释的比较清楚点:
再次重申下,PyPy真正可贵之处在于我们可以利用RPython实现各种不同的Python解释器,不用去关心JIT(除了一些小的提示外)。PyPy到时候会利用RPython工具链/PyPy(2)为我们自动实现JIT
事实上,我们还可以更抽象一点,我们理论上可以写一个适用于任何语言的解释器,然后将它扔给PyPy,最后获得那种语言的JIT。原因是PyPy仅仅关心的是优化解释器,而不会去关心这个解释器到底解释的是什么语言。
理论上你自己可以写一个适用于任何语言的解释器,然后将这个解释器传给PyPy,最后你得到这个语言的一个JIT。一个简单的题外话,我这里想提一下,JIT本事是相当棒的。它使用了一种叫做跟踪的技术,按照下面的步骤执行:
执行解释器并解释执行所有代码(还没有加入JIT特性)
对被解释过的代码做一些记录
确认你已经执行过的操作
将确认过的这些代码编译成本地机器码
想获取更多信息,可以参考这篇文章,易于理解,并且非常有趣
最后收尾:我们使用PyPy的RPython-to-C(或者其他目标平台)编译器去编译PyPy的基于RPython实现的解释器。
为什么它如此的伟大?为什么这个疯狂的想法值得我们去追求?我想Alex Gaynor已经在他的博客上面做了很好的解释了:“[PyPy就是未来] 因为[它]提供了更快的速度,更大的灵活性,并且对于Python的成长也提供了一个更好的平台”
总之:
它很快,因为它将源代码编译成了本地机器码(使用了JIT)
它很灵活,因为除了极少数的额外工作需要做外,它就能将JIT加入你的解释器中
它还是很灵活,因为你能使用RPython实现你的解释器,这个比其他的(比如C语言)更易扩展。事实上,它是如此的简单,这里有一篇教程教你如何实现你自己的解释器。
补充:
为什么Python程序慢?
Python的慢可是众所周知的。
来一个小程序试试看:
Python:
NUM = 111181111 def is_prime(n): i = 2 while i < n: if n % i == 0: return False i += 1 return True print is_prime(NUM)
等效的C程序
#include <stdio.h> int NUM = 111181111; int is_prime(int n) { int i; for(i = 2; i < n; i++) { if (n % i == 0) { return 0; } } return 1; } int main() { int result; result = is_prime(NUM); printf("%d\n", result); return 0; }
python2 isprime.py 15.60s user 0.01s system 100% cpu 15.609 total jython isprime.py 8.45s user 0.09s system 117% cpu 7.272 total pypy isprime.py 1.43s user 0.03s system 99% cpu 1.461 total ./isprime-o0 0.62s user 0.00s system 99% cpu 0.624 total ./isprime-o3 0.60s user 0.00s system 99% cpu 0.605 total
其实这个问题已经有人解答了。推荐来自IBM研究中心的Jose Castanos等人的论文:《On the Benefits and Pitfalls of Extending a Statically Typed Language JIT Compiler for Dynamic Scripting Languages》。文章分析了一个试图将Python运行在一个高性能虚拟机上的事例。他们提到了一些失败的例子和成功的例子,并阐述了成功的关键。
但是,对于Python来说,a+b这样的简单二元运算,可就真的很麻烦了。Python是动态语言,变量只是对象的引用,变量a和b本身都没有类型,而它们的值有类型。所以,在相“加”之前,必须先判断类型。
不仅如此,如果一次循环中i是整数,循环中i只是自加了1,那么下一次循环它还是整数。知道这些知识,优化器就可以大幅度地优化上述代码。
目前,PyPy是一个很活跃的项目。但是,毕竟是一个研究型的项目,PyPy也有自己的不足。如和官方Python并不完全兼容;PyPy本身的可执行文件很大;并不是运行所有的程序都快――PyPy虽然JIT Compiler很快,但它的解释器速度不如官方的Python,对于无法通过优化加速的程序来说,PyPy就不快了。