4. wamcc方法
上述三个建议方法的共同点表现为,同样在一个单独模块内,引发一个大的功能,C编译器编译起来很痛苦。如果这些可能,额外的模块调用比内部模块调用开销更大。因此,一段程序分解在模块的方式,不仅影响编译时间,而且影响执行时间,呈明显反比。
我们wamcc系统的第二个版本的目标是翻译aWAM分支到一个本机代码jump。由于强制分解成几个功能,这些跳转应该到达一个函数内部的代码块。为了产生直接分支,我们必须确定静态标签(在编译时),而不是动态的(执行时)。“编译器+连接器”组合很适合做函数地址。在wamcc采取的办法是插入一个标签,在进入每个功能感谢ASM(...)指令。要操纵这些标签的地址,说L,只需要愚弄一下计算机,让它相信L是一个外部函数,声明为原型函数L并使用符号L(在C函数的名称是其地址)。然后,编译器生成一个带孔指令,将依据所有插入标签(内部和外部)的知识,由连接器填补。额外模块调用的成本完全和模块内部调用相同。要完成我们最喜欢的例子,将产生如下代码:
void label_p(); /* prototypes */
void label_p1();
void label_q();
void label_r();
#define Direct_Goto(lab)
#define Indirect_Goto(p_lab)
void fct_p() /* p:- q,r. */
{
asm("label_p:");
push(CP); /* allocate */
CP=label-p1; /* call(q) */
Direct_Goto(label_q); /* : */
}
void fct_p1()
{
asm("label_p1:");
pop(CP); /* deallocate */
Direct_Goto(label_r); /* execute(r) */
}
void fct_q() /* q. */
{
asm("label_q:");
Indirect_Toto(CP); /* proceed */
}
只有两个宏直接或间接需要实现分支,他们依赖于及其的体系结构。例如对于一个RISC机器,我们有:
* Direct_Goto(lab)简单的调用标签功能。
* Direct_Goto(p_lab)调用一个函数,名称(地址)将储存在p_lab。
事实上,一个RISC机器上的一个函数调用指令,通过控制给定的地址(如一个jump),并初始化处理器的继续指针。由于RISC架构,这个指令是一个简单的快速jump。如无堆积,它可以被用来分支(事实上继续指针的更新并不重要,因为我们知道这是不是一个真正的函数调用,而仅仅是jump)。这样做可以避免需要插入jump指令的汇编代码。此外,RISC分支指令只能访问代码比较接近当前指令,函数调用指令不遵循这个限制。由于模块分解,这确实需要访问代码比较深。让我们终于注意到离开生成函数调用的C编译器,允许它来优化延迟槽指令pipeline【2】的优势。总结:
* 直接jump是为尽可能快地执行,因为它们被翻译成本机代码jump(或RISC架构的情况下以相同开销进入函数调用)。
* 额外模块调用不超过内部模块调用。
* 相比以往的方法,其中一个单独的模块的所有谓词被编译成一个单一的功能,这种方法产生了和子句体里一样多的函数(头和第一目标计数)。因此产生的代码编译速度更快(见后面)。
* 每个功能已经开始的时候只有一个直接的切入点。因此,只jump过了序言。为了允许本地变量,由定义一个数组中的一个中间函数开始计算,一些(足够大)的空间保留在C控制堆栈。这样C堆栈指针SP指向数组的末尾。局部变量将被分配在此阵列(见下文)。
* 这只是这种方法的假设。因此,序言除了递减SP什么也不做。这是一般情况,除了少数情况下机器的C编译器不通过SP引用局部变量,但通过另一个FP寄存器(帧指针)将函数入口设为SP。这旨在帮助调试器,它通常根据编译器选项,可能停用此操作。在此情况下,人们不可能总是生成一个汇编指令初始化这个FP寄存器。
* 在这些伪函数内,可能有真函数调用。特别是大部分WAM指令相关的宏,扩展到调用wamcc库。这有可能改变代码大小(编译速度),对执行速度造成的(小)损害。
现在让我们详细描述上述开始计算所需要的代码。假设第一个谓词(通常是顶层)地址p_lab:
#include<setjmp.h>
jmp_buf jumper;
void Label_Success();
void Label_Fail();
Bool Call_Prolog(WamCont p_lab)
{
Create_Choice_Point();
ALTB(B)=Label_Fail;
CP=Label_Success;
ret_val=setjmp(jumper);
if (ret_val==0)
Call_Next(p_lab);
Delete_Choice_Point();
return ret_val==2;
}
void Call_Next(WamCont p_lab)
{
int t[1024];
Indirect_Goto(p_lab);
}
void Call_Prolog_Success(void)
{
asm("Label_Success:");
longjmp(jumper,2);
}
void Call_Prolog_Fail(void)
{
asm("Label_Fail:");
longjmp(jumper,3);
}
Call_Prolog函数已执行谓词,地址是p_lab。它开始创建一个选择点,以便失败(Label失败)后记录分支地址。CP(Call_Prolog),表示谓词成功后执行哪些代码,由Label_Success初始化。最后,执行一个setjmp是为了随后能返回到指令。调用Call_Next函数在C堆栈,为可能的局部变量(对照:数组T声明)保留足够空间。控制权然后转交给谓词,它将像先前一样细致执行。成功(或失败)时,控制权转移至Label_Success(或Label_Failure)。将通过一个longjmp的第二个参数设置为value 2(或3),简单返回到Call_Prolog函数。
【2】在某些RISC处理器,指令后马上执行一个jump或函数调用(延迟槽)。因为在管道中已经准备好了。编译器试图在分支后通过移动一个相关指令使用这特性。如果这是不可能的,将生成一个nop指令。