如何展开(编译)解释器循环?

时间:2009-02-08 20:51:05

标签: c optimization compiler-construction programming-languages interpreter

我听说有些语言是通过“展开解释器循环”从解释到编译的。

假设我有一个伪树的伪c代码解释器。

int interpret(node)
{
    switch(node) {
        case PLUS:
             return interpret(child(0))+interpret(child(1));
        case MINUS:
             return interpret(child(0))-interpret(child(1));       
    }
}

如何展开此循环以创建已编译的程序?

我看到你们所有人都在低调,因为我不知道我在说什么,但这是维基百科的一句话,说明了我所描述的内容。

“因素最初只是解释,但现在已完全编译(非优化编译器基本上展开解释器循环”

8 个答案:

答案 0 :(得分:14)

“展开循环”通常意味着用一系列动作替换重复。循环:

for (int i = 0; i < 4; ++i) {
    a[i] = b[i] + c[i];
}

会展开到等效内容:

a[0] = b[0] + c[0];
a[1] = b[1] + c[1];
a[2] = b[2] + c[2];
a[3] = b[3] + c[3];

在我看来,维基百科引用的任何人都在某种隐喻意义上使用了这个短语。所以,从那个意义上说......

您的示例通常会在一个遍历AST节点树的解释器中调用,这可能看起来像这样:

 ASSIGN
    |
 +--+---+
 |      |
REF   MINUS
 |      |
 x   +--+---+
     |      |
    VAR    PLUS
     |      |
     a   +--+--+
         |     |
        VAR  CONST
         |     |
         b     3

并且interpret函数会有其他选项:

int interpret(node) {
    switch(node) {
        case PLUS:
             return interpret(child(0))+interpret(child(1));
        case MINUS:
             return interpret(child(0))-interpret(child(1));       
        case ASSIGN:
             return set(child(0), interpret(child(1));
        case VAR:
             return fetch(child(0));
        case CONST:
             return value(child(0));
        ...
    }
}

如果你使用interpet函数(实际执行操作)来执行AST,那么你就是在解释。但是,如果函数记录要执行的操作,而不是执行它们,那么您正在编译。在伪代码中(实际上,伪代码两次,因为我假设一个假设的堆栈机器作为编译目标):

string compile(node) {
    switch(node) {
        case PLUS:
             return(compile(child(0))) + compile(child(1)) + ADD);
        case MINUS:
             return(compile(child(0))) + compile(child(1)) + SUB);
        case ASSIGN:
             return(PUSHA(child(0))) + compile(child(1)) + STORE);
        case REF:
             return(PUSHA(child(0)));
        case VAR:
             return(PUSHA(child(0)) + FETCH);
        case CONST:
             return(PUSHLIT + value(child(0)));
        ...
    }
}

在该AST上调用compile(忽略任何伪代码错误;-)会吐出类似的内容:

PUSHA x
PUSHA a
FETCH
PUSHA b
FETCH
PUSHLIT 3
ADD 
SUB
STORE

FWIW,我倾向于将其视为展开AST,而不是展开翻译,但如果不在上下文中阅读,就不会批评别人的比喻。

答案 1 :(得分:3)

我有点困惑。我不认为'展开循环'是正确的术语。即使你重构代码没有任何递归调用,你仍然会使用解释器。

您可以使用GCC编译此程序。然后你将有一个编译的程序,虽然编译的程序将解释AST。

将此转换为编译器的一种方法是,而不是执行return interpret(child(0))+interpret(child(1));,而是生成汇编指令,而不是将其添加到文件中。

答案 2 :(得分:2)

由于并非interpret的所有调用都是尾调用,因此您实际上没有循环。

最接近你的编译器,假设堆栈模型......

int compile(node)
{
    switch(node) {
        case PLUS:
             return compile(child(0))&&compile(child(1))&&compile_op(op_plus);
        case MINUS:
             return compile(child(0))&&interpret(child(1))&&compile_op(op_minus);       
    }
}

但我认为在此上下文中展开更适用于字节码解释器而不是AST解释器。字节码指令通常在循环中解释。然后“展开”技术是发出对应于每个字节码指令的代码。

因子类似于FORTH。通常,FORTH有一个外部解释器,用于生成线程代码。线程代码可以设想一个函数指针数组(有几种变体,直接线程,间接线程,子程序线程等)。线程代码由内部解释器执行。在这种情况下展开解释器是指内部解释器,并且是连接线程代码的问题。

答案 3 :(得分:2)

Factory是基于堆栈的语言,而不是基于AST的解释器。

我已经将基于堆栈的语言用于演员解释器,所以这就是我的工作方式,这可能与Factor不完全不同。

每个函数都是作为一个函数实现的,它接受一个堆栈,并返回一个堆栈(在我的例子中是同一堆栈的变异版本,我不确定因果是否正常或变异)。在我的解释器中,每个函数还将函数的延续放在堆栈的顶部,因此解释器知道下一步该做什么:

所以解释器调用堆栈上的下一个函数就像:

for (;;)
    stack = (stack[0].function_pointer)(stack);

考虑函数foo:

def foo (x,y):
   print( add(x, y) )

add可以定义为:

pop a
pop b
stack[ return_offset ] = a + b
return stack 

和foo as:

pop x
pop y
push _
push &print
push y
push x
push &add

并且用于调用foo(5,6)的堆栈将在循环的每个步骤中演变为这样:

>> foo(5,6)
[&foo, 5, 6]
[&add, 5, 6, &print, _]
[&print, 11]
=> 11
[]

一个简单的编译器可以为foo函数展开循环,生成等效的线程代码:

compiled_foo (stack): 
    stack = begin_foo(stack) // arranges stack for call to add
    stack = add(stack)
    stack = print(stack)
    return stack

答案 4 :(得分:2)

这可能没有关系,但也请查看第二个Futamura投影

http://en.wikipedia.org/wiki/Futamura_projection

表示编译器只是一个具有部分评估/常数折叠的解释器(理论上很好但不是实践)。

答案 5 :(得分:1)

this article中,我通过一个自动将解释器转换为编译器的示例(虽然编译为Scheme而不是机器代码)。这与其他人在此处给出的想法相同,但您可能会发现将其自动化是有帮助的。

答案 6 :(得分:0)

解释器在运行时扫描每个字节码(或AST节点)并调度到函数调用(通常使用无限循环中的switch语句)。

编译器基本上做同样的事情,但是在编译时。编译器在编译时扫描每个字节码(或AST节点)并发出代码(机器代码或某些更高级的中间语言,如C),以便在运行时调用相应的函数。

答案 7 :(得分:0)

我认为这意味着不是循环遍历语句并执行它们,而是遍历语句并输出本已执行的解释器代码。

基本上,正在发生的事情是将在解释器循环中执行的代码内联到新函数中。循环得到“展开”,因为当代码执行时,它不再在解释器循环中,它只是生成函数的线性路径。