w3ctech

Firefox 的生成器函数速度提升了 22 倍

原文

Generators in firefox now twenty-two times faster

很荣幸我能宣布这个消息。多亏了 Mozilla 的 Jan de Mooij,新的 ES6 生成器函数(Generator Function)现在在 Firefox 里速度提升了 22 倍!

给不了解情况的同学介绍一下背景。Javascript 的一个新版本—— ECMAScript 6 (ES6) 即将来临。在 ES6 的新特性中提供了生成器函数:一种可以暂停的函数。Firefox 的 Javascript 引擎 SpiderMonkey 在很久之前就已经支持生成器了,领先其他 js 引擎多年。对亏了彭博社的赞助,这项支持在去年升级到了新的 ES6 标准,并在 Firefox 26 中提供给用户。

Firefox 26 里实现了非常基本的生成器。正如你可能知道的那样,现代 Javascript 的实现有若干层引擎。在 SpiderMonkey 引擎的例子中,一共有三层:语法解释器、基线(baseline)编译器和优化编译器。代码在解释器中开始执行,这也是最先启动的引擎。如果有一段代码耗时过长(被称为“热代码”),引擎将会将其“升级”到下一层中执行,对代码执行分析,可能进行优化,最后编译成机器码。

不幸的是,SpiderMonkey 中的生成器函数一直局限在最低层——解释器层。这是因为 SpiderMonkey 对生成器实现的策略选择。生成器被实现为“浮动解释器栈帧(floating interpreter stack frames)”:在这个解释器的实现中,堆上分配的对象与栈帧上的结构一模一样。这种方案实现简单,在开发的初期具有成本低廉的优势,但最终会导致无法从即时编译(JIT)技术中获益,因为 JIT 代码在特有的栈上运行,具有不同的内存排布。之前的策略还依赖于“弹簧床机制”,通过一个 C++ 编写的帮助程序来继续执行生成器函数,这让优化无从下手。

解决方案是将暂停中的生成器对象表示为栈帧状态的多个快照,而不是表示为栈帧自身。为了让其更高效,去年我们做了一系列块作用域优化的实验,来减少一个生成器帧需要存储的状态总数。在今年三月份左右,我们终于可以重构解释器,在普通栈上实现生成器了,并使用标准的解释器字节码,使得 JIT 技术可以应用在这些字节码上。

在我可以发布补丁集之前我花光了时间;尽管这些补丁按照我们的思路去实现了,事实上它们却让生成器执行起来更慢,因此在 Bugzilla 上被“雪藏”了好几个月。真是一个悲伤的故事。个把月前我很高兴地看到 SpiderMonkey 的 JIT 引擎维护者 Jan de Mooij 对合并这些补丁感兴趣。之后他一直在修改代码直到将我之前的补丁改的像样,最后将其全部应用到引擎中。

他做了更多的事,通过优化栈帧使其不为“具有别名的”本地对象(关联到作用域链上的本地变量)预留空间,提升了基线解释器中创建字面量(literal object)的速度,并最终为生成器实现了基线 JIT 编译。

那么,用上所有的这些性能技巧后,结果如何?22 倍的性能提升!基准测试代码如下:


function *g(n) {
    for (var i=0; i<n; i++)
        yield i;
 }
function f() {
    var t = new Date();
    var it = g(1000000);
    for (var i=0; i<1000000; i++)
        it.next();
    print(new Date() - t);
 }
 f();

之前,这段代码在 Jan 的计算机上,使用 SpiderMonkey 需要执行 980 毫秒。优化后呢?减少到仅 43 毫秒!这还比 V8 快上一点点,后者在测试中跑了 45 毫秒。无论如何,有竞争总有好处,而且作为同时给两个项目提交代码的贡献者,笔者对两者都能使用较好的实现感到满意。

与 V8 引擎一样,在 SpiderMonkey 下生成器并没有达到优化的最高层。笔者还对生成器是否需要像你期望的那样,如此频繁地暂停感到些许怀疑。据说在生成器中的 yield 从编译器优化的角度来说与函数调用无异,他们都会导致存储所有的本地变量来保存执行状态。区别在于,本地变量可能代表了一个非当前作用域的值,所以我们可能需要在保存生成器状态时将这些值“装箱”,并在恢复状态时取出。

感谢彭博社为最初的工作提供资金支持,并强烈感谢 Mozilla 的 Jan de Mooij 从我们落下的地方继续。祝大家能用生成器开心地写代码!

w3ctech微信

扫码关注w3ctech微信公众号

共收到0条回复