最后一次,我们了解了MIPS分支延迟插槽。今天,我们会看一些可以使用分支延迟插槽的一些技巧。
第一个技巧:跳入分支延迟插槽是合法的。当然,当你这样做时,它不是分支延迟插槽。这使您可以编写一些古怪的代码:
当拍摄无条件分支时,在分支目标继续执行之前,V0寄存器将设置为零。
同时,如果有人跳到标签,则执行在标签上继续执行,该标签将v0设置为零,然后继续使用其他内容。
标签的指令作为前提条分支的分支延迟插槽,但如果有人直接跳入其中,它也是基本块中的第一个指令。
我已经看到这类“挤出单一指令”优化出现的机会,但Microsoft编译器不会利用它。这可能是一件好事。 (对于一件事而言,二进制转换工具使程序变得更加困难,以将程序分解为基本块并以不同方式重新组合它们。)
另一个愚蠢的分支延迟插槽技巧是作为跳转的一部分编辑返回地址。
BAL指令将RA寄存器设置为指向分支延迟插槽后的指令,这在我们的情况下是第一个NOP。但在分支延迟插槽中,我们修改了RA寄存器,以便在执行达到被叫过程的开始时,它获得了人工返回地址。
我被告知某种诀窍由某些编译器使用,将呼叫和无条件跳入呼叫与虚假返回地址。例如,在此代码片段中
如果(...){...功能1(...); } else {...} //恢复x = 0;
对Function1的调用可能之后是无条件跳转以跳过其他分支。
BAL功能1 NOP;垃圾在分支延迟插槽B恢复或v0,零,零;设置x = 0 ... else-branch代码在这里......或v0,零,零;设置x = 0resume:...
BAL功能1 ADDIU RA,RA,RESUME - Nominal_Return;调整返回addrationnominal_return:... else-branch代码进入这里...恢复:或v0,零,零;设置x = 0 ...
在分支延迟插槽中,我们编辑返回地址,以便在函数1返回时,它恢复在恢复而不是nominal_return时执行,从而避免必须执行另一个分支指令。 (我们还能够删除已挂起的重复或v0,零,零指令,该指令已挂起到无条件分支的分支延迟插槽。)请注意,只有您在分支延迟插槽中有一个垃圾nop,只能获得此节省。如果那里有一个有用的指令,那么转换就会这样:
//原始代码BAL功能1移动A0,R0;函数B恢复或v0的参数,零,零;设置x = 0 // Sneaky代码移动A0,R0;设置函数Bal功能的参数1 Addu Ra,RA,......;调整返回地址:或v0,零,零;设置x = 0
BAL指令的分支延迟插槽中的指令必须进入其他地方,因此您没有保存任何时间(尽管您仍然通过避免重复或v0,零,零)保存一个空间指令。
但正如我们早些时候所看到的那样,这个技巧击败了返回地址预测器,¹所以这可能是一个坏主意。
好的,下次,我们将更加密切地查看呼叫大会。
奖金Chatter:另一个额外的偷偷摸摸的技巧正在重用返回地址。假设您的解释器循环如下:
void解释器_loop(译员_state * state){for(;;){uint32_t opcode = * state-> pc;国家 - 和gt; PC ++; jump_table [操作码](状态,操作码,状态 - > pc); }}
解释器循环将永远调度到下一个操作码。据推测,您将用Longjmp或其他一些非函数转移突破这个循环。
处理程序函数被赋予当前的解释器状态(所以它可以更新它),并且作为礼貌,它还可以根据便利性获取当前操作码和指向下一个解单字节的指针。
译员_Loop:...移动S0,A0; S0指向解释器状态LA S1,Jump_Table La Ra,Next_opcode;脚注²next_opcode:LW V1,80(S0);获取下一个操作码字节Addu A2,V1,1的地址;移动到下一个操作码字节(处理程序的参数)lbu a1,0(v1);加载当前操作码字节(处理程序的参数)SW A2,80(S0);将指针保存到下一个操作码字节SLL T0,A1,2;多个乘4到索引跳转表Addu T0,T0,S1;计算跳转表LW V0,0(T0)中的条目;加载跳转目标JR V0;跳转到处理程序 - 将返回到Next_opcode Move A0,S0;处理程序的论据
当我们调用第一个处理程序时,RA设置为等于Next_opcode。该处理程序将完成其工作,然后通过将返回地址恢复到RA寄存器并执行JR RA来返回调用者。
这意味着当控制返回到next_opcode时,您知道RA等于Next_opcode!由于这是您想要在该寄存器中的价值,无论如何,当您跳转到下一个处理程序时,您就可以将其留在那里,从而节省您必须明确地分支到Next_opcode的麻烦。
这似乎是一个非常聪明的技巧,但它在实践中可能并不是那么返回地址预测的东西。
¹另一方面,MIPS R4000没有单独的操作码用于“跳转到注册”,并且“跳转间接以获得返回的目的”;它使用两种情况的JR指令。
无法区分跳跃指令是语义上的返回指令是MIPS架构的原始实现中的非问题。它只有一个两级管道,所以单个分支延迟槽就足以避免需要预测任何分支。
MIPS R4000有一个四阶段管道,并且分支错误规定将遭受2个周期的摊位。 MIPS设计人员编码了现有的实践并追溯宣布,如果JR指令中的寄存器操作数是RA,则它将其预测为子程序返回; 否则它预测计算跳跃。 ²为额外的窃款(并保存指令),³循环准备代码可以写成 LA S1,Jump_Table Bal Next_Opcode Move S0,A0; S0指向Interpreter StateNext_opcode: 此版本允许处理器通过执行bal来计算Next_opcode的地址。 这将返回地址设置为分支延迟插槽后的指令,这是next_opcode,然后跳转到... next_opcode,这就是指令已经消失的地方。