速度与激情2:JavaScript编译器如何工作
当我们谈论JavaScript引擎的时候,通常是指它的编译器,一个把人类可读的源代码(本文中指JavaScript代码)翻译成机器可读的指令的程序。如果你还没考虑过你的代码在运行时会发生什么,那么这听起来可能相当神奇,但编译本质上只是一个翻译练习,让代码运行的快才是神奇的。
简单编译器是怎么工作的
JavaScript被认为是高级语言,这意味着它是人类可读的并且具有高度的灵活性。编译器的工作是把高级语言转换成计算机本地指令。
一个简单的编译程序有四个处理过程:词法分析器、解析器、翻译器、解释器。
- 1. 词法分析器(或者说是扫描器,分词器),扫描源码并把它转换为原子单位,称为记号。最常见的实现是使用正则表达式进行模式匹配。
- 2. 被标记化之后的代码被传入解析器,解析器对代码结构和作用范围进行识别和编码,生成语法树。
- 3. 这种类似图的结构之后被传入翻译器翻译成字节码。其中最简单的实现是把一个庞大的switch语句标记映射成等价的字节码。
- 4. 然后字节码被传入字节码解释器,被转换为本机代码。
这是经典的编译器设计,已经存在了很多年。但是桌面程序和浏览器的要求有很大不同。这种经典的结构在多个方面都有缺陷。解决这些问题的创新方式,是浏览器的速度竞赛故事。
快速、轻量、正确
JavaScript语言是非常灵活和具有兼容性的程序结构。那么你怎么写这种后期绑定、弱类型、动态语言的编译器呢?在你使它变快之前,必须先使它变精确,或者像Brendan Eich说的,
“快速、轻量、正确。任意选择两个,只要(结果)是正确的”
一种创新的测试编译器正确性的方式是“模糊测试”。Mozilla的Jesse Ruderman创建的jsfunfuzz正是这个目的。Brendan称它为“JavaScript 嘲弄产生器”,因为它的目的是创造怪异但是语法有效的结构,然后看编译器能否处理。这种工具在验证编译错误和边界问题上非常有帮助。
JIT 编译器
经典结构的原则性问题是运行时的字节码翻译非常慢。在编译过程中,将字节码翻译成机器代码时增加一个步骤可以带来性能提升。不幸的是停留几分钟在网页上等待它完全编译是不会让你的浏览器流行的。
解决方案是由JIT提出的“懒编译”,或者叫实时编译。顾名思义,它只将你用到的这部分代码实时编译成机器代码。JIT编译器有多种多样,各自有各自的优化策略。比如正则表达式编译器致力于优化单个任务,而其它的编译器可能优化像循环或函数这些常见操作。现代化的JavaScript引擎会用到多种编译器,分工合作,从而你代码的性能得到提升。
JavaScript JIT 编译器
第一个JavaScript JIT编译器是Mozilla的TraceMonkey。这是一个“跟踪JIT”,因为它的跟踪路径是从你的代码中寻找常见的可执行代码段。然后这些“常见代码段”被编译成机器代码。和以前的引擎相比,Mozilla的这种优化可以带来20%-40%的性能提升。
在TraceMonkey推出后不久,谷歌就发布了拥有全新V8引擎的Chrome浏览器。V8引擎是为速度而生。一个关键的设计是它完全跳过了字节码生成,取而代之的是由翻译器产生本地机器代码。V8团队在一年之内已经实现了寄存器分配、改善高速缓存、重写正则引擎,使其比原来快了10倍。他们 JavaScript整体执行速度被提高了150%。速度竞赛才刚刚开始。
最近浏览器厂商都纷纷推出了含有一个附加步骤的优化编译器。在定向流图(DFG)或语法树生成之后,编译器可以使用这方面知识,在机器代码产生之前进一步优化性能。Mozilla的IonMonkey和Google的Crankshaft就是DFG编译器的例子。
所有这些别具匠心的设计,其宏伟的目标就是使Javascript代码运行的和本地C代码一样快。这个目标在几年前听起来好像是在搞笑,现在已经越来越近。在第三部分,我们将看到编译器的设计者使用多种策略,开发速度更快的Javascript编译器。