浅谈Jscex编译结果的优化
2011-05-05 18:45 by 老赵, 2714 visitsJscex的核心是一个JavaScript语言到Monadic形式的编译器。从理论上说,这种编译规则十分简单,要写一个能够“正常运行”的编译器很容易。但是“正常运行”不代表足够优化。优化不当,会导致生成的结果中产生太多函数及闭包,对性能产生负面影响。在Jscex的早期原型中,从AST生成最终代码的逻辑比较简单,只做了一些基础优化。后来重构了编译器,减少了不必要的代码。而上周我提交了更新,实现了更复杂而有效的优化策略。如今的Jscex编译器部分应该已经足够稳定,剩下的便是类库方便的发展了。
基础优化
Jscex的基础编译策略十分精简,这里仅列出其中最简单一条来说明问题。比如有如下代码:
statement1; statement2;
按照Jscex的转化标准,应该由Combine及Delay组合而成:
builder.Combine( builder.Delay(function () { statement1; }, builder.Delay(function () { statement2; }));
在Jscex中,无论是Combine和Delay返回的都是协议相同的中间对象。出现Delay,是因为JavaScript一门Strict Evaluation的语言,需要有个机制确保“延迟执行”。Combine的作用是连接两个中间对象,并返回一个新的中间对象,该对象的“执行语义”表示“按序”执行两者。如果有三条语句则会继续使用Combine嵌套起来。
那么如果有10条语句连续执行呢?这便会产生大量的Combine,Delay调用,以及一大堆函数、闭包等等。运行结果自然没有问题(就像Delay套Delay那样),但很显然会产生不必要的性能开销。因此,从最起初的Jscex原型开始,便对这样的连续语句做出了优化。例如:
// before function () { var a = 0; b = a + 1; a++; return a + b; } // after (function () { var $$_builder_$$_0 = Jscex.builders["async"]; return $$_builder_$$_0.Start(this, $$_builder_$$_0.Delay(function () { var a = 0; b = a + 1; a++; return $$_builder_$$_0.Return(a + b); }) ); })
无谓的Combine和Delay消失了,程序主体依旧是顺序执行的普通代码。
高级优化
连续的语句容易优化,所以在最早的Jscex原型中就已经有所体现。但是,如try/catch或是while循环这样的语言特性,又会变成怎么样的代码呢?例如,我们得到一个URL数组,然后使用个普通的for循环来遍历并请求每个URL里的内容:
// before function (urls) { for (var i = 0; i < urls.length; i++) { $await(requestAsync(urls[i])); } } // after (function (urls) { var $$_builder_$$_0 = Jscex.builders["async"]; return $$_builder_$$_0.Start(this, $$_builder_$$_0.Delay(function () { var i = 0; return $$_builder_$$_0.Loop( function () { return i < urls.length; }, function () { i++; }, $$_builder_$$_0.Delay(function () { return $$_builder_$$_0.Bind(requestAsync(urls[i]), function () { return $$_builder_$$_0.Normal(); }); }), false ); }) ); })
无论是for,while还是do循环,编译之后都会成为了Loop调用。以上的代码已经足够优化,但如果我们换种写法,将“顺序”请求变为“并发”请求:
// before function (urls) { var requests = []; for (var i = 0; i < urls.length; i++) { requests.push(requestAsync(urls[i])); } $await(Jscex.Async.parallel(requests)); }
在相当一段时间内,Jscex编译器输出的代码是这样的:
// after (function (urls) { var $$_builder_$$_0 = Jscex.builders["async"]; return $$_builder_$$_0.Start(this, $$_builder_$$_0.Delay(function () { var requests = []; return $$_builder_$$_0.Combine( $$_builder_$$_0.Delay(function() { var i = 0; return $$_builder_$$_0.Loop( function() { return i < urls.length; }, function() { i++; }, $$_builder_$$_0.Delay(function() { requests.push(requestAsync(urls[i])); return $$_builder_$$_0.Normal(); }) ); }), $$_builder_$$_0.Delay(function() { return $async.Bind(Jscex.Async.parallel(requests), function() { return $async.Normal(); }); }) ); }); ) })
其实也很容易理解:一个标准的for循环嘛。但实际上,编译器完全没有必要生成那么复杂的代码。因为这个for循环内部没有涉及到绑定操作(例如异步类库下的$await),它完全只是段普通的代码,应该全部保留,就像这样:
// after (function (urls) { var $$_builder_$$_0 = Jscex.builders["async"]; return $$_builder_$$_0.Start(this, $$_builder_$$_0.Delay(function () { var requests = []; for (var i = 0; i < urls.length; i++) { requests.push(requestAsync(urls[i])); } return $$_builder_$$_0.Bind(Jscex.Async.parallel(requests), function () { return $$_builder_$$_0.Normal(); }); }) ); })
以上便是目前编译器会生成的代码。从表面上看来,要实现这点并不困难,只要检查AST中的for节点内部有没有绑定操作,如果没有,则直接输出。我一开始也是这么认为的,但是写了一大堆代码以后发现事情并没有想象中那么简单,于是回滚代码,重新开始。试看这样一段简单的代码:
// before function () { try { throw "Error"; } catch (ex) { throw ex; } }
您可以猜测一下,这段代码编译后的结果是怎么样的呢?应该是这样的:
// after (function () { var $$_builder_$$_0 = Jscex.builders["async"]; return $$_builder_$$_0.Start(this, $$_builder_$$_0.Delay(function () { try { throw "Error"; } catch (ex) { return $$_builder_$$_0.Throw(ex); } return $$_builder_$$_0.Normal(); }) ); })
请注意,try内部的throw在目标代码里还是throw,而catch里的throw则是把异常抛向外部,因此则编译为Throw方法。如果再嵌套一个try/catch则会看的更加明显:
// before function () { try { try { throw "Error"; } catch (ex1) { throw ex1; } } catch (ex2) { throw ex2; } } // after (function () { var $$_builder_$$_0 = Jscex.builders["async"]; return $$_builder_$$_0.Start(this, $$_builder_$$_0.Delay(function () { try { try { throw "Error"; } catch (ex1) { throw ex1; } } catch (ex2) { return $$_builder_$$_0.Throw(ex2); } return $$_builder_$$_0.Normal(); }) ); })
在这段代码里,内层catch里的throw会被外层的catch捕获到,因此它还是普通的throw。与F#不同的是,JavaScript的某些语言特性(如throw,break,continue)会中断代码正常的执行流。因此,即便是一个普通的if分支里的的break,我们也必须判断这是个普通的break语句还是一个Break输出。不过实现这样的编译器也并不困难,例如我只是在生成代码时记录当前位置而已。例如一个普通try内部的throw便是普通的throw,如果这个try变成了Try结构,则必然是Throw输出了。
优化到这步之后,应该说编译器的输出结果已经定型,剩下的就是类库方面的事情了。顺便一提,Jscex中文站上的说明也已经同步更新,并补充了更多内容。此外,下周六我将会在杭州CNodeJS聚会上和大家讨论Jscex类库方面的话题,感兴趣的朋友不妨同去同去。
老赵有空能不能讲讲js性能优化的方案?谢谢