这个 shift/jump 能比 switch...case 语句快吗?
Could this shift/jump be faster than switch...case statement?
我正在尝试最大程度地优化一个分支(类似 switch...case 的分支)以在 x86 CPU 上模拟 X CPU。我想到了:在内存中,我将加载固定长度为 0x100 字节的 x86 操作码块,如下所示:
first block
0
...[my code, jump at 0x10000, nop nop nop until 0x9F...]
0x9F
second block
0x100
...
019F
third block
0x200
...
0x29F
...
etc, etc
...
0x10000
这将是有限的,从内存 $0(+ 可能有一些偏移量)开始并以 $0xFFFF 结束(比如 "rom" 的 0x10000 大小)。现在,每次获取和模拟 X CPU opCode 时,我都会这样做:将其左移 8 位并跳转到该位置。执行这个,然后正常继续我的程序流程。我的问题是:1)这些操作码块是否有可能如此紧密? 2) 那是过去的普遍做法吗?
如果您通过开关块跨越 256 个操作码进行分支,您将进行 CPU 无法很好预测的间接跳转,这将使您在每个操作码上都出现流水线中断。
如果模拟操作码的工作规模适中,那么这个流水线中断可能无关紧要。我怀疑是这样; "load register" 操作码基本上只是通过内存读取来模拟,这不是很多工作。
您可能会购买一些可见的改进,通过在 switch 块之前添加特殊测试,检查两个或三个最常见的操作码(可能是 LOAD、CMP、JMP 条件)[如果 opcode = JMP 那么... ] 这些测试 CPU can 通常可以很好地预测。如果你这样做,测量,测量,测量。
一个更简单的技巧是,如果可以的话,将流水线中断的成本分摊到多条指令中。如果机器有很多单字节操作码,您可以考虑在接下来的两个操作码字节中进行 65536 路分支。 (现在你必须编写很多 switch cases 的代码,但是其中有很多
本质上是一样的。想知道您的编译器是否可以处理它?)在摘要中,这将流水线中断成本降低了两倍。对于具有非常规则的指令集的特定机器,这可能不会给你带来很多好处。
然而,您可能没有很多单字节操作码,但您可能需要为每条指令解码一个或多个字节。 x86 是这样的(前缀、操作码、MODRM、SIB、偏移...)。大开关盒应该能很好地解决这个问题。
最好在高速缓存行边界上对齐每个开关盒。如果指令模拟很简单,则代码很可能适合缓存行,因此内存只会看到该缓存行的一次提取。如果您不这样做,您的指令仿真将有更高的机会跨越高速缓存行边界,从而将内存获取成本提高到两倍。
这对于频繁执行的指令可能无关紧要,但很少执行的指令的代码可能会从缓存中掉出。当您实际遇到其中之一时,这将有所帮助。
最后一点建议:衡量、衡量、衡量。
我正在尝试最大程度地优化一个分支(类似 switch...case 的分支)以在 x86 CPU 上模拟 X CPU。我想到了:在内存中,我将加载固定长度为 0x100 字节的 x86 操作码块,如下所示:
first block
0
...[my code, jump at 0x10000, nop nop nop until 0x9F...]
0x9F
second block
0x100
...
019F
third block
0x200
...
0x29F
...
etc, etc
...
0x10000
这将是有限的,从内存 $0(+ 可能有一些偏移量)开始并以 $0xFFFF 结束(比如 "rom" 的 0x10000 大小)。现在,每次获取和模拟 X CPU opCode 时,我都会这样做:将其左移 8 位并跳转到该位置。执行这个,然后正常继续我的程序流程。我的问题是:1)这些操作码块是否有可能如此紧密? 2) 那是过去的普遍做法吗?
如果您通过开关块跨越 256 个操作码进行分支,您将进行 CPU 无法很好预测的间接跳转,这将使您在每个操作码上都出现流水线中断。
如果模拟操作码的工作规模适中,那么这个流水线中断可能无关紧要。我怀疑是这样; "load register" 操作码基本上只是通过内存读取来模拟,这不是很多工作。
您可能会购买一些可见的改进,通过在 switch 块之前添加特殊测试,检查两个或三个最常见的操作码(可能是 LOAD、CMP、JMP 条件)[如果 opcode = JMP 那么... ] 这些测试 CPU can 通常可以很好地预测。如果你这样做,测量,测量,测量。
一个更简单的技巧是,如果可以的话,将流水线中断的成本分摊到多条指令中。如果机器有很多单字节操作码,您可以考虑在接下来的两个操作码字节中进行 65536 路分支。 (现在你必须编写很多 switch cases 的代码,但是其中有很多 本质上是一样的。想知道您的编译器是否可以处理它?)在摘要中,这将流水线中断成本降低了两倍。对于具有非常规则的指令集的特定机器,这可能不会给你带来很多好处。
然而,您可能没有很多单字节操作码,但您可能需要为每条指令解码一个或多个字节。 x86 是这样的(前缀、操作码、MODRM、SIB、偏移...)。大开关盒应该能很好地解决这个问题。
最好在高速缓存行边界上对齐每个开关盒。如果指令模拟很简单,则代码很可能适合缓存行,因此内存只会看到该缓存行的一次提取。如果您不这样做,您的指令仿真将有更高的机会跨越高速缓存行边界,从而将内存获取成本提高到两倍。 这对于频繁执行的指令可能无关紧要,但很少执行的指令的代码可能会从缓存中掉出。当您实际遇到其中之一时,这将有所帮助。
最后一点建议:衡量、衡量、衡量。