间接跳转可能是指令解码的最佳选择。
在旧机器上,比如1997年的英特尔P6,间接跳转可能会出现分支预测错误。
在现代机器上,比如英特尔Core i7,有一个间接跳转预测器,可以相当好地避免分支预测错误。
但即使在没有间接跳转预测器的旧机器上,你也可以使用一个技巧。这个技巧(曾经)在很久以前的英特尔P6日子里,在《英特尔代码优化指南》中有记录:
不要生成像下面这样的东西
loop:
load reg := next_instruction_bits // or byte or word
load reg2 := instruction_table[reg]
jmp [reg]
label_instruction_00h_ADD: ...
jmp loop
label_instruction_01h_SUB: ...
jmp loop
...
生成代码如下
loop:
load reg := next_instruction_bits
load reg2 := instruction_table[reg]
jmp [reg]
label_instruction_00h_ADD: ...
load reg := next_instruction_bits
load reg2 := instruction_table[reg]
jmp [reg]
label_instruction_01h_SUB: ...
load reg := next_instruction_bits
load reg2 := instruction_table[reg]
jmp [reg]
...
即在每个位置用循环顶部的代码替换跳转到指令获取/解码/执行循环的跳转。事实证明,即使没有间接预测器,在这种后续的线程代码中,这样做的分支预测要好得多。更准确地说,在只有单个间接跳转副本的原始代码上,条件、单目标、PC索引BTB将比较适用于此后续线程代码。
大多数指令集都有特殊的模式——例如,在Intel x86上,比较指令几乎总是跟随着一个分支。
祝你好运并玩得开心!
(如果你在意的话,工业中指令集模拟器使用的指令解码器几乎总是执行N路跳转树,或者数据驱动的双重导航N路表树,每个树节点中的条目指向其他节点或函数进行评估。)
哦,也许我应该提一下:这些表格、开关语句或数据结构是由专用工具生成的。
一种N路跳转的树,因为当跳转表中的案例数量变得非常大时会出现问题 - 在我在20世纪80年代编写的工具mkIrecog(制作指令识别器)中,我通常会制作大小为64K条目的跳转表,即使用16位进行跳转。当跳转表的大小超过16M(24位)时,当时的编译器会崩溃。
数据驱动,即指向其他节点的节点树,因为(a)在旧机器上无法很好地预测间接跳转,(b)事实证明,许多时候指令之间存在共同代码 - 而不是在跳转到每个指令的情况下出现分支误判,然后执行公共代码,再次切换,并获得第二个错误预测,您可以执行公共代码,稍微调整参数(例如,您消耗指令流的位数以及下一组要跳转的位在哪里)。
在mkIrecog中,我非常激进,允许使用32位开关,尽管实际限制几乎总是将我限制在16-24位。我记得我经常将第一个解码视为16或18位开关(64K-256K条目),而所有其他解码都要小得多,不超过10位。
嗯:我大约在1990年发布了mkIrecog到Usenet。 ftp://ftp.lf.net/pub/unix/programming/misc/mkIrecog.tar.gz
如果您感兴趣,可以看到使用的表格。
(请友善一点:当时我还很年轻。我不记得这是Pascal还是C语言。我已经多次重写它 - 尽管我尚未重写为使用C++位向量。)
我认识的大多数做这种事情的人都是以字节为单位进行操作 - 即采用8位、256路分支或表查找。
switch
语句。同时,还可以参考X86预取优化:“computed goto”线程代码以及有关解释器的此问答,这些内容对于解释CPU仿真器与R或Python同样适用,而不是JIT仿真器。 - Peter Cordes