代码之家  ›  专栏  ›  技术社区  ›  Gabriel Ščerbák

字节码如何用于优化动态语言的执行时间?

  •  4
  • Gabriel Ščerbák  · 技术社区  · 14 年前

    我对一些优化方法或通用字节码设计很感兴趣,与AST的解释相比,这可能有助于使用VM加速执行。

    2 回复  |  直到 10 年前
        1
  •  8
  •   olliej    14 年前

    AST口译与字节码的主要优势在于操作调度成本,对于高度优化的口译员来说,这开始成为一个真正的问题。”调度”是用来描述开始执行操作(如算术、属性访问等)所需的开销的术语。

    一个相当普通的基于AST的解释器应该是这样的:

    class ASTNode {
        virtual double execute() = 0;
    }
    
    class NumberNode {
        virtual double execute() { return m_value; }
        double m_value;
    }
    
    class AddNode {
        virtual double execute() { return left->execute() + right->execute(); }
    }
    

    所以执行代码是为了 1+1 需要3个虚拟呼叫。虚拟电话是一个非常昂贵的(在事物的大计划中),因为多个间接地打电话,而且打电话的一般成本在第一位。

    在字节码解释器中,您有一个不同的调度模型——而不是虚拟调用,您有一个执行循环,类似于:

    while (1) {
        switch (op.type) {
            case op_add:
                // Efficient interpreters use "registers" rather than
                // a stack these days, but the example code would be more
                // complicated
                push(pop() + pop());
                continue;
            case op_end:
                return pop();
        }
    }
    

    与本机代码相比,这仍然具有相当昂贵的调度成本,但比虚拟调度快得多。您可以使用一个名为“computed goto”的GCC扩展进一步提高性能,该扩展允许您删除交换机调度,从而将总调度成本降低到基本上单个间接分支。

    除了提高调度成本之外,基于字节码的解释器比AST解释器有许多额外的优势,主要是由于字节码能够像真正的机器一样“直接”跳到其他位置,例如,想象一段这样的代码:

    while (1) {
        ...statements...
        if (a)
            break;
        else
            continue;
    }
    

    为了在每次执行一条语句时正确地实现这一点,您需要指出执行是要保持在循环中还是停止中,因此执行循环就变得类似于:

    while (condition->execute() == true) {
        for (i = 0; i < statements->length(); i++) {
            result = statements[i]->execute();
            if (result.type == BREAK)
                break;
            if (result.type == CONTINUE)
                i = 0;
        }
    }
    

    当你增加更多形式的流量控制时,这种信号变得越来越昂贵。一旦您添加了异常(例如在任何地方都可能发生的流控制),您就开始需要在甚至基本算术的中间检查这些东西,从而导致开销不断增加。如果你想在现实世界中看到这一点,我鼓励你看看ecmascript规范,在那里它们用一个ast解释器来描述执行模型。

    在字节码解释器中,这些问题基本上消失了,因为字节码能够直接表示控制流,而不是通过信号间接地表示。 continue 只是简单地转换成一个跳转指令,只有当它真正被击中时,你才能得到这个代价。

    最后,根据定义,一个AST解释器是递归的,因此必须防止溢出系统堆栈,这对代码中可以循环的量有很大的限制,比如:

    1+(1+(1+(1+(1+(1+(1+(1+1)))))))
    

    在解释器中有8个递归级别(至少),这可能是一个非常大的成本;旧版本的Safari(pre-Squirrelfish)使用了一个AST解释器,因此JS在现代浏览器中只允许几百个递归级别,而在1000个递归级别中是允许的。

        2
  •  1
  •   Gian    14 年前

    也许你可以看看各种方法 llvm “opt”工具提供。这些是字节码到字节码的优化,工具本身将提供对应用特定优化的好处的分析。