我们将在本节详细介绍Janus,KL1,Erlang和wamcc如何处理控制流。此演示文稿的灵感来自[5],它采用了堆叠模型的目标。但是,我们不遵循类似于实际执行的抽象。这种选择的后果,明确描述了C代码与WAM指令的相关性。由于篇幅所限,我们只在这里讨论控制问题。首先是出于这样的事实,wamcc使用的WAM是传统而没有优化的。从而现在为其他指令写的代码变得众所周知了[1]。第二,有效控制的关键在于翻译成C,因为WAM代码是平的并且通过分支来执行转换。这是更适合高层次的控制结构,例如功能,并且对于低级别控制不提供更多。因此,主要问题将为WAM分支的翻译找到一个合适的解决方案。我们的演示将基于下面的例子中,只有一个子句和一个事实:
p: allocate /* p:- q, r. */
call(q)
deallocate
execute(r)
q: proceed /* q. */
然而,这个简单的例子显示了Prolog的控制在确定性的情况下使用的指令。翻译的方式调用和执行将尤其突出,如何管理直接分枝(即当目标地址是一个已知的标签),而进行指令翻译解决问题间接分枝(即当目标地址是某些变量的内容,在这种情况下注册CP)。但是出现了问题,因为间接分支是不具备的标准(ANSI)C(因此必须模拟),也因为GOTO指令只能处理在同一功能的代码。该解决方案,因此导致一个C程序组成的一个独特的功能一个开关指令来模拟间接GOTO语句。这种方法之后,我们前面的例子将被转换为:
fct_switch()
{
label_switch:
switch(PC){
case p: /* p:- q,r . */
label_p:
push(CP); /* allocate */
CP=p1; /* call(q) */
goto label_q; /* " */
case p1:
pop(CP); /* deallocate */
goto label_r; /* execute(r) */
case q: /* q. */
label_q:
PC=CP; /* proceed */
goto label_switch; /* : */
.
.
.
}
}
这种方法在RISC机器花费大,因为switch语句的成本约为10个机器指令(包括边界检查)。然而,这种方法的主要的缺点:是一个程序上升到一个单一的功能。因此,除玩具的例子,它会产生一个巨大的功能,C编译器是无法在合理的时间内处理。如果这样设置,应对模块化是不容易的。它要求每个谓词调用一个咨询动态表,以便通过本模块,控制开关功能。此外,为支持一个完整的Prolog,也在上下文变化情况下时关注正确处理回溯。因此,支持模块化是惩罚,并且一个额外的模块调用将比一个模块内调用花费大得多。
3.1 Janus
Janus实现是基于一个简单的思想,通过一个C分支翻译成WAM分支,即一个goto指令。类似的方法,如[11]中描述的在Prolog编译器中使用。但是出现了问题,因为简介分支是标准C(ANSI C)不具备的(因此必须模拟),也因为goto指令只能处理在同一代码功能。该解决方案导致一个C程序组成的一个独特的功能一个开关指令来模拟间接GOTO语句。这种方法之后,我们前面的例子将被转换为:
fct_switch()
{
label_switch:
switch(PC){
case p: /* p:- q,r */
label_p:
push(CP); /* allocate */
CP=p1; /* call(q) */
goto label_q; /* : */
case p1:
pop(CP); /* deallocate */
goto label_r; /* execute(r) */
case q: /* q. */
label_q:
PC=CP; /* proceed */
goto label_switch; /* : */
.
.
.
}
}
这种方法在RISC机器上花费昂贵,因为switch语句的成本约为10个机器指令(包括边界检查)。然而,主要的缺点这种方法是一个程序,上升到一个单一的功能。因此,除玩具的例子,它会产生一个巨大的功能,C编译器是无法在合理的时间处理。在此设置下,应对模块化是不容易的,它要求每个谓词调用一个咨询动态表,以便通过本模块,控制开关功能。此外,为支持一个完整的Prolog,也照顾环境的变化的情况下,正确处理回溯。因此,支持模块化是惩罚性的,并且一个额外的模块调用将比一个模块内调用昂贵得多。
3.2 KL1
作为一个C函数的汇编是不现实的,代表C程序WAM代码切片成几个功能。每一个Prolog谓词看起来那么自然地翻译成一个C函数。WAM分枝会给上升到函数调用。这样一个函数在返回之前调用另一个嵌套函数(分支),依此类推;如此,它永远不会返回之前结束程序。因此,在C控制栈中积累的数据无用,可导致内存溢出。解决的办法是执行一个分支之前从任何一个函数得到返回,并有一个过程监督器使分支得到足够的延续。这导致了下面我们继续的代码示例:
fct_supervisor()
{
while(PC)
(*PC)();
}
void fct_p() /* p:- q,r, */
{
push(CP); /* allocate */
CP=fct_p1; /* call(q) */
PC=fct_q; /* : */
}
void fct_p1
{
pop(CP); /* deallocate */
PC=fct_r; /* execute(r) */
}
void fct_q() /* q. */
{
PC=CP; /* proceed */
}
描绘上面的代码可以抑制PC寄存器优化,可以返回其信息的功能。因此,当传递控制和需要分支时,每个功能实现行计算和终端地址返回。这种方法的分析表明一个WAM分支在一个函数调用后,由一个返回实施到监管者。这花费显然明显高于简单jump指令而将产生一个本地代码编译器。然而,额外的模块调用现在可能无需支付额外费用。首次实施的wamcc使用这种技术,并且比仿真Sicstus慢两倍左右。KL1为了减少函数调用和返回,进行了权衡:同一模块内德所以谓词被翻译成一个单一的功能。因此,当只有一个模块,KL1行为像Janus。监管功能只需要对额外模块调用上下文切换,因此成本超过内部模块调用。
最后,让我们陈述这种方法(无论是否改善KL1建议)是最适合为100%的ANSI C的解决方案。
3.3 Erlang
void fct_p() /* p:- q,r. */
{
jmp_tbl[p]=&&label_p; /* (initialization) */
jmp-tbl[p1]=&&labe_p1;
return;
label_p:
push(CP); /* allocate */
CP=&&label_p1; /* call(q) */
goto *jum-tbl[q]; /* : */
label_p1:
pop(CP); /* deallocate */
goto *jmp-tbl[r]; /* execute(r) */
}
void fct_q() /* q. */
{
jmp_tbl[q]=&&label_1; /* (initialization) */
return;
label_q:
goto *CP; /* proceed */
}
所有的分支都通过一个全局地址表间接goto。为了消除间接代替直接跳转的开销,Erlang像KL1或Janus,一个给定模块的所有谓词编译成一个单一功能。因此,只有额外的模块调用需要全局地址表的咨询,并会就此内部模块调用更加昂贵。
观察分支直接在函数内部,避免了序言使得它无法使用局部变量(在C堆栈无保留空间)从而意味着只使用局部变量。还要注意的是任何指令必须不移动前项标签,这是相当难以保证。让我们考虑到全局表中的元素的访问。这编译成一个地址表的负载,其次是用于访问给定元素的指令表的地址。编译器可以随意地优化表的访问,并在函数的一开始放置加载表的地址,它假定它将始终被执行。当在函数内jump时,这将导致一个问题,并会尝试使用未初始化的寄存器。