近来我在JavaScript领域花费了大量的时间。这没有完全出乎意料-当我第一次向Mozilla申请工作的时候,我被只是半开玩笑的警告:”它们雇用所有最好的Python开发人员,然后强迫它们编写JavaScript“。做为一门语言,我并不深爱或者憎恨它,但是做为一个平台,JavaScript却深深吸引着我,做为一种任何地方都可运行且廉价的通用命名环境,它逐渐地被塑造并迫使其成为一个相当完美的通用运行时环境。然而如果“互联网络是平台”,那么一个落伍的Python支持者做什么呢?
当然是移植Python到JavaScript!
以前就有许多方法可以做到这些。Skulpt和Brython就是在JavaScript上对Python的重新实现,其中包括为了制作一个非常引人注目的演示而开发的交互式控制台。Pyjamas可以让你把Python应用转换为JavaScript,这样它们就可以在浏览器里运行了。还有更多这样不同程度成功并且技术非常完善的例子。
我不想弱化像这样的项目背后所付出的非凡的努力。不过就我个人而言,我有点担心沉湎于重新实现这种单调的工作所带来的风险。我非常愿意充分利用为制作非常出色的Python解释器所做的工作和制作非常出色的JavaScript运行时环境所做的工作,然后在把两者结合在一起的时候,尽可能少的做重复实现的工作。
最终,沿着这条思路我踏出了试探性的第一步:混合两个令人着迷的开放源代码项目:PyPy和Emscripten。
PyPy
PyPy宣称自己是“ 另一个快速的、兼容性的Python语言实现“,而且还有一个花哨的 速度测试网站来支持它的这个声明。当然,速度确实快多了,不过真正吸引我的是它的实现细节。在构建一个新的Python解释器的过程里,PyPy团队创建了用于构造动态语言解释器的强大的通用工具套件,因而PyPy项目可以分成很大程度上相互独立的两个部分。
第一部分就是PyPy解释器本身,它完全用Python编写。说的更具体一点,它是用一个称为Rpython的Python语言的受限制子集编写的,它保留了完整的Python语言的许多优秀的特性,并且启用了高效的预编译选项。这就使得与直接用C实现解释器,也就是实现python.org的标准解释器相比开发工作将非常容易和灵活。
第二部分就是RPython转换工具链,它提供了大量可以把RPython代码转换为可执行包的令人目炫的各种方法和选项。它可以把RPython代码转换为可以直接编译的低级别的C语言代码,或者转换为可在Java和.NET虚拟机上运行的高级别的字节码。它还可以插入几种不同的内存管理模式,线程实现的任何一种,以及定制最终可执行包的所用的大量的其他选项。
RPython工具链还包含了PyPy’s速度的秘密:通常自动地为RPython程序的热循环生成及时编译器的能力。这就是深层次元级别的魅力,而且这恰恰就是运行在互联网上的Pyhton解释器获得非常好的性能所必须的东西。
因此理论上,只要我们为RPython转换工具链实现生成JavaScript的后端,那么我们就能讯速地且具有很好的兼容性的移植Python到JavaScript。
Emscripten
Emscripten是“JavaScript编译器的低级虚拟机(LLVM)“,它用来把C或者C++程序编译为JavaScript。通常它用来把大多数已经存在的C++应用转换后放到互联网上,而且它还是运行在浏览器里的Epic Citadel的最新演示背后的编译器。这是相当优美的骇客技术,而且由于近来火热的浏览器JavaScript性能竞争,所以生成的代码可以提供完全可接受的性能。
Emscripten用来把C编程模型映射到JavaScript的技术最近已经在一个名叫asm.js的规范里正式化了。asm.js是允许高效的预编译选项的JavaScirpt受限子集。在可识别asm.js代码的JavaScript引擎里,Emscripten编译的程序可以使用比本地可执行包使用的资源少两倍的情况下都可以运行。
这两种技术混合的可能性在理论上是很明显的:RPython工具链把Python代码编译成C代码;Emscripten编译C代码为JavaScript;在你的浏览器使用Python把它们聚在一起。
Emscripten以前确实用来把标准的基于C的解释器编译为JavaScript;正是这个才使repl.it上的Python shell更强大。不过解密PyPy非常速度的想法却十分引人注目,同时RPython构建链的灵活性也开启了其他可能性。那么会怎样呢?
RPython的JavaScript后台
对PyPy的伟大的工作人员和Emscripten的开发人员来说,现实中混合这两种技术几乎同理论上听起来那样容易。PyPy的RPython工具链有一个可以让你很容易地插入定制的编译器或者甚至插入一个完整的新工具链的扩展的地方。我的github分支就包含把它和Emscripten挂接所必须的逻辑:
Emscripten努力做到像标准的Posix构建链那样运行,这样只要求你用”emcc”替换通常所用的”gcc”调用。我确实需要做一点调整使它更像Posix运行环境,因此你需要用到下面的分支,直到它们与上面的分支合并为止:
https://github.com/rpk/emscripten
为了把RPython代码编译为常见的可执行包,你要调用”rpython”转换程序。下面是一个从PyPy源代码仓库中提取简单的“你好,世界”的例子,它可以直接运行:
1 2 3 4 5 6 |
$> python ./rpython/bin/rpython ./rpython/translator/goal/targetnopstandalone.py [...lots and lots of compiler output...] $> $> ./targetnopstandalone-c debug: hello world $> |
然而为了把RPython代码编译成JavaScript,你只需要指定选项”–backend=js”。生成的JavaScript文件可以使用如nodejs这样的JavaScript shell的命令行来执行:
1 2 3 4 5 6 |
$> python ./rpython/bin/rpython --backend=js ./rpython/translator/goal/targetnopstandalone.py [...lots and lots of compiler output...] $> $> node ./targetnopstandalone-js debug: hello world $> |
这就是所有要做的。如果你还有多余时间的话,那么你可以执行下面命令把整个PyPy解释器转换为JavaScript:
1 2 3 4 |
> python ./rpython/bin/rpython --backend=js --opt=2 ./pypy/goal/targetpypystandalone.py [...seriously, this will take forever...] ^C $> |
或者你只想获取最终的结果:
pypy.js
未压缩的情况下生成的JavaScript文件为139M.它包括完整的Python语言解释器,几个非常重要的内置模块以及附加的Python标准库中所有.py文件的列表。如果你手边有一个JavaScrip shell的话,你可以像下面命令行这样传递这些参数JavaScript shell来运行Python命令:
1 2 3 4 5 6 7 8 |
$> node pypy.js -c 'print "HELLO WORLD"' debug: WARNING: Library path not found, using compiled-in sys.path. debug: WARNING: 'sys.prefix' will not be set. debug: WARNING: Make sure the pypy binary is kept inside its tree of files. debug: WARNING: It is ok to create a symlink to it from somewhere else. 'import site' failed HELLO WORLD $> |
正如你所料,第一个版本有非常多的警告:
- 没有即时编译器(JIT)。在上面,我通过传递”–opt=2″选项显式地禁止了省城即时编译器。生成即时编译器需要一些平台相关的代码的支持,实际上我仍然没有弄清楚它应该看起来像什么。
- 没有文件系统访问权限,这使得在启动的时候就打印出调试的告警信息。这还需要做些对Emscripten扩展可插拔虚拟文件系统的工作,在将来的某个时刻可启用本地文件的访问权限。
- 然而,为了提供Python标准库,它使用了绑定文件系统的快照。这使得启动非常非常地慢,因为在进入解释器的主循环之前需要把整个快照解包到内存。
- 没有交互式控制台。输出运行正常,不过输入却并不是这样的。我仍然不想深挖细节,不过让一些基本的东西运行应该不是太难的。
- 丢失了许多内置模块,因为这些内置模块需要其他C级别的依赖。比如,”hashlib”模块依赖OpenSSL。我将一个接着一个地添加这些内置模块。
- 我肯定不会像repl.it那样在它的上面放一个基于浏览器的华丽的用户界面(UI)。
因此即便没有这些,你也不可能立刻在浏览器里运行这个。不过它是真正的Python解释器,而且它还可以执行真正的Python命令。对我来说,以些许连接代码的代价获得所有这些就非常了不起。
性能
当然大的问题时它是怎样执行呢?为了分析这个,我求援于Python社团的最流行的并且不科学的基准:pystone。这是一个没有意义的小程序,它用来测试Python解释器执行循环的次数,并以“每秒执行pystone的个数“这样的结果来显示速度。下面是我在我的机器上对各种Python解释器测试的结果;数值越大性能越好:
解释器 | Pystones/秒 |
---|---|
pypy.js, on node | 877 |
pypy.js, on spidermonkey | 7427 |
native pypy, no JIT | 53418 |
native cPython | 128205 |
native pypy, with JIT | 781250 |
到目前为止,最慢的是运行在碰巧安装的稳定版本的nodejs上的编译了的pypy.js。实质上这是JavaScript的基本性能,因为这个版本的node没有对有Emscripten生成的asm.js风格的代码做任何特别特殊的处理。如果我构建目前的开发版本,那么它可能运行地会更快些。
下一个最慢的就是在SpiderMonkey JavaScript shell的每晚构建下运行的编译了的pypy.js。这是强化Firefox的JavaScript引擎,而且它能够识别和优化Emscripten生成的asm.js语法。果真,这个外加的优化实质上提高了速度。
下一个最慢的是禁止了即时编译功能(JIT)的PyPy的本地构建。把这个版本与pypy.js相比就能对在JavaScript里运行和本地代码运行所花费的资源开销有所了解,我们可以看到快了大约7倍。这甚至与在其他asm.js编译的代码上所呈现的只是慢两倍的结果相差很远。不过再说一遍,我没有做过任何研究或者调整性能的工作。我怀疑可能有一些相对容易实现的东西可以帮助缩短这个差距。
我的系统中较快的是本地Python解释器CPython 2.7.4。有时可能忘记的重要的一点是:没有即时编译器(JIT) ,PyPy解释器通常比标准的CPython解释器慢一些。这是它为实现灵活性目前必须付出的代价。然而任何事情需要的不是停留- PyPy的开发者一直在寻找甚至在缺少即时编译器(JIT的情况下加速PyPy解释器。
毋庸置疑,这儿的速度之王是启用即时编译功能的PyPy本地构建。
比较pypy.js和启用了即时编译(JIT)的本地PyPy是很容易的,结论是两者根本就没有可比性。现在它们的速度差异是在两个数量级上!不过这只是第一次尝试,而且没有PyPy那样的特别的速度JavaScript版本依然正常运行。如果我们能够成功地把PyPy的即时编译(JIT)功能转换为JavaScript,那么我们就能够弥补回大量这样的性能差距。的确这是一个相当大的“假设”,不过是一个有趣的可选项。
要想提前看看什么可能发生,请考虑一下PyPy仓库里pystone的单独的RPython版本。如果我们把它从RPython变为为本地代码,那么它将给出机器能力的大概上限。然而,如果我们把她从RPython编译为JavaScript,那么它将给出启用即时编译的PyPy的JavaScript选项可能的大概上限:
解释器 | Pystones/秒 |
---|---|
native rpystone | 38461538 |
rpystone.js, on spidermonkey | 13531802 |
比较上面的pystone运行后的结果,数字高的惊人。这么高以致于我怀疑这些数值是否完全精确,并且由于pystone的RPython版本和标准版本的某些不同而出错。然而实际上根本就没有不同之处。
这儿最有趣的事情是比较这两个版本的性能。JavaScript版本比本地的编译版本慢3倍多,而与使用全功能的解释器相比则是更小的差距。是否启用即时编译的pypy.js运行流行的循环仅仅比本地解释器满3倍吗?这是一个有趣的发现。
能不能即时编译?
眼下留给我一个急需解决的问题是:JavaScript平台是否强大到足以支持PyPy的即时编译功能?坦率地讲,我不知道!不过更加深入地探究RPython即时生成器的细节并且弄清楚由Emscripten和asm.js构建的JavaScript后台看起来像什么是我正在进行的工作。
从JavaScript角度来看,有非常积极一点:asm.js规范里明确地呼吁在代码运行的任何阶段都可以生成和连接新的asm.js模块。由于JavaScript具有动态特性,所以即时编译完全得到了支持,并且完全按照规范所期望那样运行。
然而,以asm.js方式运行的代码禁止为自身创建新的函数。如果pypy.js解释器需要即时编译某些代码,那么它将不得不通过调用外部的JavaScript函数来跳出asm.js的快速运行通道。实际上运行生成的代码同样需要外部跳板以允许解释器跳出自身的asm.js模块去调用新的代码,即时编译的代码也需要类似的跳板以回调主解释器。
这种asm.js内部模块连接是Emscripten路线图的一个试探性的内容,而且不清楚它将需要多少资源开销。如果前后跳跃所需要的所有开销太高,那么它就容易地受困于即时编译代码可能带来的性能好处里。
在PyPy方面还有一些潜在的障碍。PyPy开发者多次试图在低级虚拟机(LLVM)上构建自己的即时编译系统,然而重复多次后发现他们的需求受到了太多限制。提出主要的原因之一是没有能力动态地为生成的机器码打补丁,不能通过JavaScript即时编译(JIT)后台实现共享。
对我来说,如何限制仍然不清楚。如果牺牲一些效率,比如向生成的代码里加入其它检查和标志变量,就能够找到限制运行的地方,那么我们也许就可以从即时编译器知道问题所在。然而如果对代码动态地打补丁是即时编译操作的基础,那么我们也许纯粹是运气不好了。
最后,有人只需要试试,然后看看结果。假若我能找到这样的时间的话,我也计划这么做。
常常有这样的报道:带有即时编译的PyPy在某些基准测试上比CPython要快6倍或者更多。而且我们已经看到了asm.js代码比本地代码运行要慢不到三倍。结合这两个数据,今年剩余的时间我的崇高的、疯狂的、良好冬季但可能徒劳的目标如下:
让运行在spidermonkey shell里的pypy.js获得比本地CPython解释器更快的以每秒pystone数计量的速度。
可能吗? 我不知道。不过找到了将是很有趣的一件事!
读者中任何敢赌的人都可以
直接向Brendan Eich询问。