Hello World
Spiga

浅谈Jscex编译结果的优化

2011-05-05 18:45 by 老赵, 2670 visits

Jscex的核心是一个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类库方面的话题,感兴趣的朋友不妨同去同去。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

4 条回复

  1. 程序设计的艺术
    122.112.144.*
    链接

    程序设计的艺术 2011-05-05 23:24:37

    老赵有空能不能讲讲js性能优化的方案?谢谢

  2. 老赵
    admin
    链接

    老赵 2011-05-06 11:38:08

    @程序设计的艺术

    这东西没意思,都是些hack,trick,workaround,不喜欢……

  3. libinqq
    27.36.34.*
    链接

    libinqq 2011-05-06 22:26:29

    老赵能说下 node.js 未来么,最近我看园子里很多人都在学,但是我觉得node.js 还不成熟,官方没有数据库支持,异步模式是特点(但仅限于聊天室,在线访谈等),还有没有好的IDE, 企业级数据库效率开发不如C#, 你觉得node.js 以后发展会怎样,我觉得拿php做毕竟nodejs 还差的很远。

  4. 老赵
    admin
    链接

    老赵 2011-05-07 10:40:29

    @libinqq

    它又不是给你做企业数据库开发的,它是给你写通信服务器,node.js的IDE就是JavaScript的IDE,php就是用来做HTTP的,如果你觉得它不成熟,现在不学自然也行,本来就是还在发展的东西嘛。

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我