8086的本地寄存器分配器

2021-05-11 05:38:42

这是贪婪自下而上的本地寄存器分配器的实现,英特尔8086 CPU。它可以用作简单编译器的一部分。特别是,它可以用于C编译器,其INT为16位,CHAR为8位。

为了使事物有趣和交互式,它具有从代表具有16位整数ALU操作,常量和内存负载和存储的表达式的树木生成8086个汇编代码(可编译为DOS .com程序)的代码生成。代码生成的一些额外指令检查输出寄存器和输出存储器位置中的表达式评估结果,如果有的话。

因此,您实际上可以执行生成的代码和调整围绕何时触及更改影响代码生成。

; 7(VR4); MUL(VR5); 5(VR3);添加(VR6); 3(VR1); MUL(VR2); 2(VR0); ---- regs需要(大约):3; --------; ; VR0 MOV AX,2; VR0 MOV CX,3; VR1 MUL CX; VR2 MOV CX,5; VR3 MOV DX,7; VR4 XCHG AX,CX; VR5 MUL DX; VR5加入CX,AX; VR6; ; VR6 CMP CX,41; VR6 jne失败; VR6.

N.B.此寄存器分配器不会完全在ALLCUMSTANCE中分配寄存器。它会偶尔生成不必要的寄存器动作和交换。请记住,最佳寄存器分配是一个NP完整的问题。

英特尔8086架构具有许多具有固定inputand /或输出寄存器的指令和它' s并不完全琐碎,以连接这些说明的ins andouts。在那里有这方面的历史悠久,实现了这个和类似的架构,但它有点奇怪的是它'碎片找到这样的概念简单的算法。 "龙"书籍Andmany其他来源考虑架构,无需使用如此缀有的寄存器操作数或使用图形来解决问题,这意见的原因可能是不切实际的。

英特尔8086架构在现代Intel Pentiumcpus中幸存到这一天,旧的8086的兼容后裔,它会在有些指令中存活有关固定的/输出寄存器操作数的说明。因此,遇到的是今天的问题,虽然在今天的时候处理较新的32位和64位X86指令和寄存器的限制较少。

8086通过特殊用途注册表达式评估(请赦免COMB和装配运营商,说明和语法和ASCII ART):

+ - 可以在大多数ALU指令中自由使用,因为SRC / DST | + - 可以用作访问存储器的地址| + - 有一个单独的8位子轮辐| | | + - 可以用CBW签名| | | | + - 可以转移| | | | | + - 可以是偏移计数VVVVVV + - << | ^〜[r] 8bit cbw<<<<<<<<<<<<<<<& | ^<<<& | ^& | ^& FIX + FIX + BX + + + + + CX + + + + + + DX + + + FIX + SI + + + + + + + + + + ^ ^ ^ ^ ^ ^可以是产品 - + | | | | |可以是多平面 - + | | | |可以是股息 - + | | |可以除法 - + | |可以是商 - + |可以余下 - +

直接在ALU指令中使用立即和内存操作数(我们使用单独的负载/商店说明)

在我们到达8086的细节之前,让' s描述了anarchitecture的算法,其中areN' t指令,固定寄存器操作数。我们' ll在它的顶部构建。

让' S还考虑了一个案例,我们的表达式仅由二进制程序(一元运算符将是一个琐碎的专业化),它被认为是CPU中的3个注册指令,即2个输入寄存器和1输出寄存器,就像在MIPS架构中(例如Sub R2,R3,R4,R4 r2 = R3 - R4;)。

op - op op + | OP OP OP OP ^ * /%nm nm nm nm nm nm nm nm nm1 2 3 4 5 6 7 8

AlloChregs(节点):递归调用两个输入/子节点的Allochregs()保证()输入/子值都在寄存器中()两个输入寄存器Allocate()一个输出寄存器使用这3个寄存器操作数生成指令

对于数字叶节点,将只有最后两个步骤:allocate()向前输出寄存器和将integerconstant加载到该寄存器中的指令。

确保(节点):如果节点' s值没有位置:allocate()寄存器,如果节点' s值已在寄存器中返回regress earts:返回export every:/ *节点' s值必须溢出到堆栈* / allocate()寄存器生成弹出指令以将值从堆栈弹出到此寄存器中返回此寄存器

分配(节点):如果有n个寄存器中的空缺寄存器分配器管理:将其标记为保持节点标记为在此寄存器中的节点返回给其他寄存器:从分配器管理的N个寄存器找到注册其值Won' t是最长的,即其父/父母/使用节点是最遥远的标记,该节点将寄存器保存为溢出的节点生成推送指令,以推动节点'从寄存器中的值。在堆栈上标记寄存器作为将参数节点传递给allocate()标记为在此寄存器中的节点返回寄存器

免费(硬件_reg):标记该寄存器的节点保存为没有位置标记为寄存器,因为没有节点

使用此算法我们将在此代码上为上面的树生成(Let' s在此处复制树):

op - op op + | OP OP OP OP ^ * /%NM NM NM NM NM NM NM NM1 2 3 4 5 6 7 8MOV HR0,1MOV HR1,2xor HR0,HR0,HR1MOV HR1,3MoV HR2,4MUL HR1,HR1,HR2ADD HR0,HR0,HR1MOV HR1,5MoV HR2,6IDIV HR1,HR1,HR2MOV HR2,7MoV HR3,8犯8核HR2,HR2,HR30R1,HR1,HR2SUB HR0,HR0,HR1

如果我们限制分配器可以管理到3的寄存器数量,我们' ll更新(让' s再次复制树):

op - op op + | OP OP OP OP ^ * /%NM NM NM NM NM NM NM NM1 2 3 4 5 6 7 8MOV HR0,1MOV HR1,2xor HR0,HR0,HR1MOV HR1,3MoV HR2,4MUL HR1,HR1,HR2ADD HR0,HR0,HR1MOV HR1,5MoV HR2,6IDIV HR1,HR1,HR2MOV HR2,7Push HR0Mov HR0,8核人HR0,HR2,HR0OR HR0,HR1,HR0POP HR1SUB HR0,HR1,HR0

请注意上面代码和寄存器HR3的PERISAPPEARANCE中的推送和流行指令的外观。

分配器一旦将7加载到HR2并且即将加载到另一个寄存器中,分配器运行就会运行。此时,有3个部分展开扫描所有3寄存器:

我们溢出HR0,结果+,到堆栈上,使HR0可用于加载。我们选择HR0以溢出,因为它的父母 - ,是最遥远的(|和%,/和7的父母更接近) 。当我们最终需要+的结果时,我们将其从堆栈中弹出。

父节点的相对距离可以通过将唯一数字的简单恢复为所有节点来获得:

op - op op + | OP OP OP ^ * /%nm nm nm nm nm nm nm nm nm1 2 3 4 5 6 7 80 2 11 6 3 5 4 14 7 9 8 13 10 12 11 < - 节点编号/虚拟reg

然后每个节点都将具有此唯一编号(我们可以将其称为VirtualRegister),我们可以遵循节点&#39; s父链接以查找父级&#39; sunique号码,或者我们可以简单地存储父母&#39; S shardnode中的唯一数字而不是在那里存储链接。

因此,在我们的这个例子中,14是三个数字中最大的(14,13,12),这就是我们如何决定将HR0重新踢出到堆栈上。

泄漏具有最遥远的使用的寄存器,保证了unspillingwill以确切的相反顺序(Lifo),这让我们使用堆栈用堆栈和流行的指令。具体来说,运营商处于大多数二进制(不是三元),我们&#39; LL永远不会泄漏两个兄弟节点,即我们&#39; llnever必须区分同等的遥远节点。

当实现此算法时,我们&#39; ll需要一个全局的n指针到表达树节点,其中n是注册放置器管理的寄存器的数量。 (此数组在我们的代码中称为Nodefromhreg []。)通过申请此阵列的内容我们&#39;如果特定的硬字体是由某个节点的结果占用的。节点本身将不会将结果的位置(即,它可以是其中一个Handwareregisters或堆栈或无处)。我和这样的设置我们&#39; ll总是beable通过检查阵列或节点和we&#39; ll对阵列和节点进行适当地反映进程的当前分配来讲述何处。

当评估二进制运算符(例如A + B)时,我们应该首先评估使用更多寄存器的Chechild / subExpresspects,然后使用更少的寄存器。这导致更有效的寄存器使用和更少的溢出。逻辑ISSIMPLE:在评估二进制运算符之前,您需要在评估另一个孩子的同时在寄存器中保留其中一个孩子。所以,如果孩子需要,让&#39; s说,1和2每个寄存器为了评估,第一个第一次将需要3个寄存器,而评估它们的相反顺序需要2寄存器。

当分配唯一节点号(虚拟寄存器号)如前一节所述,我们可以按此顺序分配它们,然后我们可以通过查看和比较其唯一数字来按此顺序传递节点。

并非所有CPU架构提供3-Register Alu指令,如MIPS。X86架构&#39; al Alu指令是2枚寄存器。也就是说,outputoverswrites其中一个输入。

这会影响先前描述的AlloChregs()函数一点。它可以通过Alloc()函数分配特定输出寄存器,使得Alloc()将与TheInputs之一分配相同的寄存器。因此,例如,如果AX和CX对添加指令的输入,如果我们生成添加AX,CX(或其他方式,则输出将在AX中,如果我们生成添加,则输出将在AX中。如果我们生成添加,则输出将在CX中。 CX,AX;保持不是所有二进制运算符都是对称的。

当节点的指令需要其在特定寄存器中的输入时,它的子节点在所需寄存器中产生其输出(这是通过熟悉的ALLoChregs()函数递归地完成)。然而,它们可能无法满足由于所需的请求他们的所有阶段(即,他们永远无法在elestheonatrally生产的输出),或者所需的寄存器已经保持有用的东西。所以孩子节点做了他们可以做的事情并将其留给他们的父母,当他们可以&#39; t满足请求。

值得庆幸的是,它&#39;总是可以在一些寄存器中交换Xchg指令中的一些寄存器,以满足输入侧的需求,而且常常需要竞争。

因此,先前引入的AlloChregs(),保证()和分配()函数以接收其他参数,所需的寄存器。使用此参数应该是可以请求特定寄存器的,从所有可分配寄存器的特定子集寄存器寄存器。

枚举hreg:int {//特定regs begin hreg0,hregax = hreg0,hreg1,hregcx = hreg1,hreg2,hregdx = hreg2,hreg3,hregbx = hreg3,hreg4,hregsi = hreg4,hreg5,hregdi = hreg5,//特定的regs结束hRegcnt,//代表多项选择所需regs的常量:hregany = hregcnt,//未指定,根本是OK HRegnotcx,//优先于CX以外的Regs(转移)hRegnotdxnotax,//更喜欢dx以外的regs和AX(对于(i)div)hRegnotdxnotcxnotax,//优先于dx,cx和ax hregbyte以外的regs,//更喜欢具有单个字节组件的regs:ax,cx,dx,bx hregbytcx,//除了除以单独的字节组件cx:ax,dx,bx hregaddr,//喜欢那些可以是内存操作数的人:bx,si,di};

Allocate()函数将尝试分配所请求/所需的registerFirst,但如果它未能,则它&#39; ll返回不同的一个,并且呼叫者将处理它(例如,使用Xchg指令)。

8086班次指令(SHL,SHR,SAR)如果计数ISN&#39; T. TheValue可以在其他地方的其他地方,从固定寄存器CL(CX的下半部分)取得换档计数。

因此,转换指令的Allochregs()的实现应将自己的所需寄存器参数(可能被修改为排除CX),以Theft Child&#39; sAllochregs()并确保()将HRegcx作为右侧的寄存器传递给右侧的寄存器。 39; s allochregs()并确保()。顺便说一句,当孩子们需要相同数量的寄存器来计算我们应该首先要掌握右/计数的孩子,以掌握调配cx的核心。

所需的寄存器可以通过修改到左子vshift dst,cl | | | V V期望REG = CX所需REG排除CX

确保()可能无法为合适的子组分配和返回HREGCX。碰巧时,我们要么将右子节点移动到CX(如果CX空缺)或者我们在该节点和当前驻留在CX中的寄存器之间的寄存器(当CX ISN&#39; t空置时)。对于此,我们将Thonode(s)和全局指针数组更新为节点(nodefromhreg [])并获得mov或xchg指令。我们还应该记住,确保()可以为左子组分配CX,在这种情况下,我们需要Toproperly更新左寄存器。

^ | Shift DST,Cl ^ ^ | |案例1:?x cx没有文档组2:cx?x左右左右和右类3:?x?x交换右和cx(或向右移动到cx)

(i)MUL指令乘以另一个输入的AX,并将产品输出到一对寄存器,DX:AX(最有效位:最不可知的比特)。也就是说,该产品是多样性的两倍。无论我们的目的都是针对我们的产品ISN&#39的16位乘法物的宽度。需要超过16位,我们可以将DX视为堵塞寄存器。

(i)因此,因此呼叫者和#39; S所需寄存器无视,因为(i)覆盖物输出到斧头。 (i)MUL,对称,也询问ITSCHILD /输入节点在AX中产生它们的结果,希望至少在斧头中最终。

所需的reg被忽略,因为输出位于轴中| V(i)Mul Ax,SRC | | | V V期望REG = AX所需REG = AX

如果(i)mul&#39中的任何一种,它的输入最终在dx中,它&#39;它保持在那里,因为它可以方便地堵塞。但是,如果没有输入DX,则明确保留DX Mayneed(如果DX保存其他一些部分结果,则为Won&#39; t想要(i)mul to clobber)。

保留DX,我们在DX中的一个输入节点和节点之间交换寄存器。 IOW,我们将其降低到具有DX中的一个输入,这是安全的克波伯。

因此,通过最多2个注册交换机,如果需要,我们可以在斧头和其他内部进行一个输入,如果需要,在DX中。

N.B.两个16位整数的乘积的16个最低有效位是签名和无符号乘法的CHESA名,因此在X86上,如果我们&#39;重新对产品的16位感兴趣,我们可以互换使用MUL和IMUL。

该指令划分一对寄存器,DX:AX(最高有效位:最低有效位),即32位整数,通过16位输入和TheOutputs处于DX(剩余时间)和AX(“商”中)。当我们需要Todide 16位整数时,我们将Zero-or and and and angivend从16位扩展到32bits,填充dx。 DX是股息的一部分自然需要我们使用DX进行除数。

但首先,我们需要AX中的股息,我们将HRegax传递为左侧输入节点的AlloChregs()并确保()。泛腾德克诺(含义任何寄存器但dx或ax)作为右输入节点的常见处理。

所需的REG被忽略,因为输出位于AX或DX | v(i)div dx:斧头,src | | | V V期望REG排除DX和AX所需REG = AX

然后通过大多数Xchg,我们可以确保股息确实是斧头。

如果我们&#39;重新执行和保证()(或xchcg)给我们dx for diviSoror dx包含其他一些部分结果,我们allocate()另一个寄存器(并立即免费()它,我们只找到可用的寄存器,溢出溢出)并移动到它的任何节点位于DX中,从而释放DX的功能。

最后,当我们开始分配(i)div的输出寄存器时,如果我们&#39;重新对商品感兴趣或者我们分配DX,我们分配斧头如果我们&#39;

N.B.当计算有多少寄存器(i)基于DIV的划分/仍然需要时,我们在DX周围的上述限制中的因素。这raisesthe最小从2到3寄存器,但否则计算仍然是例如e.g的名称。添加。评估顺序和Noder odermentlier的节点应考虑到此。

要从内存中加载值,我们需要3个行政机器人之一中的内存地址:BX,SI,DI。因此,提供地址的子/输入节点不会替换其Allochregs()并确保()用HRegAddr称为所致法的。如果我们&#39;重新追随地址在其中一个寄存器中,我们会像往常一样充值Xchg,将地址带入例如时。 BX。

可以将16位值加载到任何16位寄存器中,但是将8位值加载到单个8位子轮辐中,例如,当WE&#39时,可以将Al,BL,CL,DL(所需的寄存器设置为HRegbyte,如Hite8位子轮辐。此外,如果我们打算使用CBW指令将8位值从Memory签名为16位,我们需要将值加载到AL中。

如果我们可以直接处理,我们希望将值加载到所需的寄存器中。所以,在我们调用allocate()之前分配输出registerwe&#39;重新检查所需的寄存器值,看看它是否可用&#39;同样,如果allocate()没有给我们一个带有8-bitsubregisters的寄存器,我们可能会使用xchg来获取我们的ax / al for theoutput寄存器。

期望的reg可能被忽略并从具有8位子标尺的regs中选择VMOV R,[R] | v所需的reg可以解决内存

商店比加载更简单,但它们具有相同的基础知识器地址,其中存储器地址是在3个合适的寄存器(Bx,Si,di)之一中,并且8位值是4个合适的寄存器中的一个(ax / AL,BX / BL,CX / CL,DX / DL)和我们可能需要移动或XCHGTO满足这些要求。 可能会忽略所需的reg | VMOV [R],R | | | V V期望的REG可能需要具有8位子标记所需的REG可以解决内存 N.B. 商店节点&#39; s结果(除了写入记忆的直接效果之外)可以是与我们可以写入的值和#39; s类似于我们如何编写a = b = c;在c / c ++中使用结果 一个分配(b = c)作为另一分配(a = ...)的值。 工程一个编译器2。 通过Keith D. Cooper和Linda Torczon.In,第13.3.2节&#34;自下而上的本地寄存器分配&#34; 编译器原则,技术和工具(AKA&#34; Dragon&#34;书)Byalfred V. Aho,Ravi Sethi和Jeffrey D. Ullman。