现在的位置: 首页 > 综合 > 正文

wamcc:将Prolog编译成C (No.7-3)

2013年10月13日 ⁄ 综合 ⁄ 共 3426字 ⁄ 字号 评论关闭

三、现有的逻辑编程译者

我们将在本节详细介绍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
但是出现了问题因为间接分支不具备的标准(ANSIC(因此必须模拟,也因为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

在Erlang也被翻译成一个C函数谓词。然而,为避免函数调用和返回的开销,Erlang的优势是GNU C编译器(GCC)提供的新的可能性。事实上,GCC认为(jump指令)标签作为第一类对象,并使得它可以存储在一个指针变量的标签并在随后的执行值的间接跳转指出这样的变量。因此,我们的想法是翻译的WAM分支到简介跳转内部C函数的调用,以避免额外的费用。然后需要存储一个全局表格的所有地址,必须把每个函数的第一次调用初始化。回来说我们不可避免的例子,这将产生:

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时,这将导致一个问题,并会尝试使用未初始化的寄存器。

抱歉!评论已关闭.