首先贴个Javascript性能测试站点,测试并展现了数个 JavaScript 引擎的性能数据:arewefastyet
咱们看到在这个比武场上,最近 Chrome 出现了多个新条目,其中不少条目都是关于 v8 的 Ignition 新架构的组合,他们是 v8 引擎最近推出的 JS 字节码解释器。html
纵览各个 JS 引擎的实现,咱们发现基于字节码的实现是主流。例如苹果公司的 JavaScriptCore (JSC) 引擎,2008 年时他们引入了 SquirrelFish(市场名 Nitro),实现了一个字节码寄存器机(Register Machine)。再如 Mozilla 公司的 SpiderMonkey,他们使用字节码的历史更久,能够追溯到 1998 年的 Netscape 4(见 https://dxr.mozilla.org/class... ),SpiderMonkey 实现的是堆栈机(Stack Machine)。微软的 Chakra 也使用了字节码,他们实现的是寄存器机(Register Machine)。而 v8 以前的作法是比较“脱俗”的,他们跳过了字节码这一层,直接把 JS 编译成机器码。而在刚刚过去的五一假日前夕,v8 5.9 发布了,其中的 Ignition 字节码解释器将默认启动 :https://v8project.blogspot.co... 。v8 自此回到了字节码的怀抱。前端
这让笔者不由怀念起 2007 年 Ruby 1.9 的发布。当时 Ruby 1.9 也是第一次引入了字节码,名为 YARV,由笹田耕一领导主导开发完成。当时,Ruby 还在使用松本行弘的初级的解释器实现,亦即,解释器每次遍历代码的抽象语法树(AST)来进行 Ruby 代码的解释执行。而 YARV 则把抽象语法树(AST)先编译成字节码,而后再运行。引入字节码以后,Ruby 的性能获得了显著的提高。浏览器
而此次 V8 引入字节码倒是向着相反的方向后退。由于以前 v8 选择了直接将 JS 代码编译到机器代码执行,机器码的执行性能已经很是之高,而此次引入字节码则是选择编译 JS 代码到一个中间态的字节码,执行时是解释执行,性能是低于机器代码的。最终的性能测试势必会下降,而不是提升。那么 V8 为何要作这样一个退步的选择呢?为 V8 引入字节码的动机又是什么呢?笔者总结下来有三条:缓存
(主要动机)减轻机器码占用的内存空间,即牺牲时间换空间网络
提升代码的启动速度闭包
对 v8 的代码进行重构,下降 v8 的代码复杂度架构
故事得从 Chrome 的一个 bug 提及: http://crbug.com/593477 。Bug 的报告人发现,当在 Chrome 51 (canary) 浏览器下加载、退出、从新加载 facebook 屡次,并打开 about:tracing 里的各项监控开关,能够发现第一次加载时 v8.CompileScript 花费了 165 ms,再次加载加入 V8.ParseLazy 竟然依然花费了 376 ms。按说若是 Facebook 网站的 js 脚本没有变,Chrome 的缓存功能应该缓存了对 js 脚本的解析结果,不应花费这么久。这是为何呢?ide
这就是以前 v8 将 JS 代码编译成机器码所带来的问题。由于机器码占空间很大,v8 没有办法把 Facebook 的全部 js 代码编译成机器码缓存下来,由于这样不只缓存占用的内存、磁盘空间很大,并且退出 Chrome 再打开时序列化、反序列化缓存所花费的时间也很长,时间、空间成本都接受不了。函数
因此 v8 退而求其次,只编译最外层的 js 代码,也就是下图这个例子里面绿色的部分。那么内部的代码(以下图中的黄色、红色的部分)是何时编译的呢?v8 推迟到第一次被调用的时候再编译。这时间上的推移还致使另一个短板,就是代码必须被解析屡次——绿色的代码一次、黄色的代码再解析一次(当 new Person 被调用)、红色的代码再解析一次(当 doWork() 被调用)。所以,若是你的 js 代码的闭包套了 n 层,那么最终他们至少会被 v8 解析 n 次。性能
Facebook 的网站之因此收到这个设计带来的负面的性能影响,就是由于他们的前段工程流程中最后把各个独立的 module 编译成了一个单独的文件,其中用到了不少闭包,如:
如此一来 Chrome 的缓存做用就只能做用在最外层的 __d() 代码上,而内部的真正的逻辑根本没有被缓存。
刚才提到了机器码占空间大的一个坏处,就是不能一次性编译所有的代码。机器码占空间大还有另一个坏处,就是一些只运行一次的代码浪费了宝贵的内存资源。正如上面 Facebook 中的 __d() 系列函数,他们的做用可能只是注册、初始化各个模块组件,而一旦初始化完成便不会再执行。但因为机器码占空间大,这些只执行一次的代码也会在内存中长期存在、长期占用空间。正以下图所示,通常状况下大约 30% 的 V8 堆空间都用来存储未优化的机器码。
而引入字节码以后,占空间的问题就能够获得缓解。经过恰当地设计字节码的编码方式,字节码能够作到比机器码紧凑不少。V8 引入 Ignition 字节码后,代码的内存占用确实下降了,以下图所示。
经过对十大流行手机端网站的测试,能够发现他们的内存占用显著降低。
这即是 v8 引入字节码的主要动机。而这样实现以后其实顺便又带来了两个好处,笔者认为能够视做 v8 引入字节码的次要动机,亦即:更快的启动速度和更好的 v8 代码重构。
在启动速度方面,现在内存占用过大的问题消除了,就能够提早编译全部代码了。由于前端工程为了节省网络流量,其最终 JS 产品每每不会分发无用的代码,因此能够指望所有提早编译 JS 代码不会由于编译了过多代码而浪费资源。v8 对于 Facebook 这样的网站就能够选择所有提早编译 JS 代码到字节码,并把字节码缓存下来,如此 Facebook 第二次打开的时候启动速度就变快了。下图是旧的 v8 的执行时间的统计数据,其中 33% 的解析、编译 JS 脚本的时间在新架构中就能够被缩短。
v8 自身的重构方面,有了字节码,v8 能够朝着简化的架构方向发展,消除 Cranshaft 这个旧的编译器,并让新的 Turbofan 直接从字节码来优化代码,并当须要进行反优化的时候直接反优化到字节码,而不须要再考虑 JS 源代码。最终达到以下图所示的架构。
其实,Ignition + TurboFan 的组合,就是字节码解释器 + JIT 编译器的黄金组合。这一黄金组合在不少 JS 引擎中都有所使用,例如微软的 Chakra,它首先解释执行字节码,而后观察执行状况,若是发现热点代码,那么后台的 JIT 就把字节码编译成高效代码,以后便只执行高效代码而再也不解释执行字节码。苹果公司的 SquirrelFish Extreme 也引入了 JIT。SpiderMonkey 更是如此,全部 JS 代码最初都是被解释器解释执行的,解释器同时收集执行信息,当它发现代码变热了以后,JaegerMonkey、IonMonkey 等 JIT 便登场,来编译生成高效的机器码。
回顾历史,不少 JS 引擎都是采用了字节码这一脚本语言实现技术的,而 v8 一枝独秀,走“纯机器码”路线,其实过于激进了:虽然执行性能上能够登峰造极,但却带来了内存占用过大的问题。此次引入字节码实则是作了工程上的恰当取舍,将损失掉的内存找回来,更加符合现在移动和嵌入式设备为主的应用场景;以时间换空间,让 v8 能更好的服务于低内存的设备。现在 V8 也回到了字节码的怀抱,不由使人感叹 JS 引擎与字节码真是有着不解之缘!