js引擎中的懒解析

来源:互联网 发布:mac本的压缩文件是什么 编辑:程序博客网 时间:2024/06/05 15:22

本文为翻译,原文:http://ariya.ofilabs.com/2012/07/lazy-parsing-in-javascript-engines.html

现代浏览器都可以把一个function的函数体解析延迟到真正需要的时候,那这又是如何工作的呢?

IE的团队最近发表了一篇关于使用懒解析来提高性能的文章“Advances in JavaScript Performance in IE10 and Windows 8 “。实际上稳定版的IE9已经开始使用这种方式了,只不过IE10更进一步优化了性能。据IE团队称(IE中使用的js引擎是Chakra ):

为进一步减少首次执行时间,只有当functions要执行的时候Chakra才处理和释放字节码,这种机制称之为延迟解析。
我们来看一个简单的例子,看下它是如何工作的,假设有一段js代码:

function add(x, y) { return x + y; }function mul(x, y) { return x * y; }alert(add(40, 2));

在js引擎可以执行代码之前,必须先把代码传给解析器。解析器的目的就是做语法分析,并得到一个抽象的语法树(AST).可以使用parser demo 这个demo工具来了解下语法树的大致生成结构。完整的语法书是非常复杂的,用通俗点的语言表达就是:


定义一个function,叫add,它接受x,y参数,它有一段返回申明,返回值是x,y的二进制操作+定义一个function,mul,接受x,y参数,它有一段返回申明,返回x,y的二进制操作*创建一个alert函数的调用,它的参数是add函数接受40,2参数的结果。

基于上面这个语法书,一些奇怪的事情发生了。最后,解析器质性你的代码的时候弹了个消息框。但是你会注意到,上面这个解析过程有一个浪费的步骤,浪费精力去解析mul函数,而实际上mul并没有被调用。这个例子看起来很简单,实际上(根据微软的 JSMeter research),大部分的已声明的函数却没有被调用到。

现代浏览器都开始逐渐使用lazy paring来替代这种单次完整的解析。上面的解析工作就变成这样了:

定义一个add函数,函数体是“{ return x + y; }”定义一个mul函数,函数体是“{ return x * y; }”调用alert函数,传入add函数处理40,2参数的结果作为参数
这样,解析器就不会深入到每个函数体内部,在执行过程中,过程将继续:

调用add函数,Hmm,但是它还没有解析,调用实时解析“{ return x + y; }”它接受x,y参数,返回x,y的+操作结果

函数源代码的解析就被推迟了,只有当需要执行之前才解析。懒解析同样需要解析传入的代码,因为它需要定位出整个函数的body,比如你看到了”function add(x, y) {“ ,然后你需要找到函数体的结尾”}“。这不能通过正则和任何形式的扫描来完成,解析器如果是时解析的话需要处理整份代码。幸运的是,解析不需要去找结束标记了,这样就可以达到最优化。我们根本不需要语法树,因为它不会被任何人访问。另外,代码路径也不需要从内存中划分堆出来存储。分配内存消耗了资源也拖慢了速度。

...(原文中的一个生活比喻偶就不翻译了,丫的,又累又啰嗦啊,懂了就行了。如果真的是还没理解,那只能怪我翻译水平有限了,我再翻译也是徒劳了,还是问问元芳吧)....


我们假设来解析一个while语句块来作为比较:

‘while’ ‘(‘ Expression ‘)’ Statement

真正的解析需要明白并产生一个抽象的语法树(AST)代表它的结构,在js中看来像这样:

function realParseWhileStatement(){  expect('while');  expect('(');  var expression = parseExpression();  expect(')');  var statement = parseStatement();   // node for the AST  return {    type: 'WhileStatement',    test: expression,    body: statement  };}

如果使用lazy parsing,我们并不关心结果,所以代码就更简单:

function lazyParseWhileStatement(){  expect('while');  expect('(');  parseExpression();  expect(')');  parseStatement();}

显然,还有很多其它函数语来解析各种语法规则。底线就是解析器要解析所有标记,直到函数体完成。这样一来,它就知道函数的结尾标记,匹配函数的开始标记。

在赖解析中如果我们遇到一个内嵌函数呢?这个规则同样适用,内嵌函数同样会被懒解析。是不是有点想函数盗梦空间?

实际上,lazy 解析器会更复杂一些,需要妥善处理严格模式,要考虑解析错误、堆栈溢出等其他情况。

我们来看下lazy parsing在2大流行的js引擎中的实现。

webkit和safari 中使用的JavaScriptCore (JSC),JavaScriptCore 代码定义在Source/JavaScriptCore 目录,实现lazy parsers的相关代码:

parser/Parser.hparser/Parser.cppparser/SyntaxChecker.h

正常解析和懒解析的实现JavaScriptCore都在同一个代码里,通过专门的C++模板实现。解析器本身不构建语法树,这份工作交给TreeBuilder。这里有2中builder,ASTBuilder and SyntaxChecker,后一种本质上讲不做什么工作,除非被解析器调用,解析器可以在向前构造的时候在任何点停下来。SyntaxChecker扮演的是一个语法检查的角色。

JSC解析器会调用语法检查器,当在需要解析一个function的body的时候,可以看parseFunctionBody,在parseFunctionInfo中被调用。function的body的语法检查完之后,包含函数体范围的大括号就会被保存起来,当函数调用,真正解析的时候有用。因为JSC保存整个源,保存的范围值足够大,所以没有必要复制源字符串。

Chrome,Node.js中的V8引擎也类似,懒解析的相关代码在:

src/preparser.ccsrc/preparser.hsrc/preparser-api.cc

跟JSC不同的是,v8对普通解析和懒解析有2种不同的代码(但是有类似的接口)。V8里称之为 PreParser ,preparser在v8遇到一个函数体时触发,像Parser::ParseFunctionLiteral。有趣的是,V8为一个特殊的情况做了优化。即时函数调用的情况,为了不污染全局环境,流行的写法,比如:

var foobar = (function() {    //  do something    //  return the module object})();

这种方式非常普遍,v8做了一个启发式检测,如果遇在一个function前遇到(,然后就不触发lazy parsing,使用真正普通的解析,这样v8就会为函数体产生语法树。

那firefox中使用的SpiderMonkey又会怎样呢,还没有lazy parsing,但是正在计划实现。可以看bug 678037,这一举措可能将进一步提升firefox的性能。