间接跳转可能是指令解码的最佳选择。
在较旧的机器上,比如 1997 年的 Intel P6,间接跳转可能会导致分支预测错误。
在现代机器上,比如 Intel 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 // or byte or word
load reg2 := instruction_table[reg]
jmp [reg]
label_instruction_00h_ADD: ...
load reg := next_instruction_bits // or byte or word
load reg2 := instruction_table[reg]
jmp [reg]
label_instruction_01h_SUB: ...
load reg := next_instruction_bits // or byte or word
load reg2 := instruction_table[reg]
jmp [reg]
...
即用每个地方循环顶部的代码替换到指令获取/解码/执行循环顶部的跳转。
事实证明,即使在没有间接预测器的情况下,这也有更好的分支预测。更准确地说,一个有条件的、单一目标的、PC 索引的 BTB 在后一种线程代码中将比在只有一个间接跳转副本的原始代码中好得多。
大多数指令集都有特殊的模式——例如在 Intel x86 上,一条比较指令几乎总是跟在一个分支之后。
祝好运并玩得开心点!
(如果您关心的话,工业中指令集模拟器使用的指令解码器几乎总是执行 N 向跳转树,或数据驱动的对偶,导航 N 向表树,树中的每个条目都指向到其他节点,或到要评估的函数。
哦,也许我应该提一下:这些表、这些 switch 语句或数据结构是由专用工具生成的。
一棵N路跳转树,因为当跳转表中的case数量变得非常大时会出现问题-在我在1980年代编写的工具mkIrecog(make指令识别器)中,我通常会跳转表达到64K条目的大小,即在 16 位上跳跃。当跳转表的大小超过 16M(24 位)时,当时的编译器就崩溃了。
数据驱动,即指向其他节点的节点树,因为 (a) 在较旧的机器上可能无法很好地预测间接跳转,并且 (b) 事实证明,指令之间的大部分时间都有公共代码 - 而不是具有跳转到每条指令的案例时的分支预测错误,然后执行公共代码,然后再次切换,并获得第二个错误预测,您执行公共代码,参数略有不同(例如,您消耗了多少指令流的位,以及下一组要分支的位是 (are)。
我在 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 路、分支或表查找。)