我最近看了这里的问题为什么快处理比排序的数组排序的数组? 并找到了答案,绝对引人入胜,并与基于数据的分支处理时,它已经完全改变了我的人生观编程。
我现在有一个很基本的,但功能完备的解释英特尔8080仿真程序用C语言编写,运行的心脏是处理每个操作码256长的switch-case表。 我最初的想法是这显然是因为操作码的编码工作的最快的方法是不相符整个8080指令集和解码将增加大量的复杂,矛盾和一次性的案件。 一种开关情况下表的全预处理器宏是一个非常整洁的和易于维护。
不幸的是,阅读上述职位,它发生在我,但绝对没有办法之后在我的电脑分支预测器可以预测的开关情况的跳跃。 因此,每一个开关情况进行导航的管道将有时间来彻底消灭,导致否则什么应该是一个令人难以置信的快速程序的几个周期的延时(还有的甚至没有这么多,因为乘法在我的代码)。
我敢肯定,你们中的大多数都在想:“哦,这里的解决方法很简单,转会动态重新编译”。 是的,这似乎像它会切出大部分的switch-case,并增加速度相当。 不幸的是我的主要兴趣是模仿旧的8位和16位时代的主机(英特尔8080这里仅仅是一个例子,因为它是我的模拟代码最简单的片),其中的周期和定时保持的确切指令是视频和声音的重要必须基于这些精确的定时进行处理。
当处理这种级别的精度性能就成了一个问题,甚至对老年人控制台(请看bSnes为例)。 是否有任何追索或者是这只是与长管道的处理器打交道时,一个就事论事的事实呢?
与此相反, switch
的说法可能被转换为跳表 ,这意味着它们可能执行几个if
s(对于范围检查),和一个单一的跳跃。 的if
不是应引起分支预测的问题,因为它是不太可能你将有一个不好的操作码。 跳跃是不与管道太热情了,但最后,它只是一个为整个switch
语句..
我不相信你可以转换一个长switch
的操作代码语句转换成任何其他形式会导致更好的性能。 这是当然的,如果你的编译器是足够聪明,将其转换为一个跳转表。 如果没有,你可以手动。
如果有疑问,执行等方法和措施的表现。
编辑
首先,确保你不要混淆的分支预测和分支目标预测 。
分支预测仅适用于分支语句。 它决定分支条件是否会成功或失败。 他们什么都没有做的跳转语句。
在另一方面分支目标预测也试图猜测那里跳将结束。
所以,你的说法“没有办法,分支预测器可以预测跳”应该是“没有办法的分支目标预测可以预测跳”。
在您的特定情况下,我不认为你可以真正避免这种情况。 如果你有一个非常小的一组操作,或许你能想出覆盖所有的操作,就像那些在逻辑电路做了一个公式。 但是,随着指令集大如CPU的,即使有风险,即计算的成本比单跳的罚款高得多。
因为是密密麻麻您256路开关语句分支,编译器将实现这个作为一个跳转表,所以你在正确的每次经过这个代码时间(你会触发一个分支错误预测的间接跳转不会显示任何一种可预测的行为)。 与此相关的惩罚将是,也许在缺乏一个微操作的缓存旧的微体系架构长达25现代CPU(Sandy Bridge的)上大约15个时钟周期。 对于这样的事情一个很好的参考是agner.org“软件优化资源”。 第43页的“使用C优化软件++”是一个良好的开端。
http://www.agner.org/optimize/?e=0,34
你能避免这个惩罚的唯一方法是通过确保同一指令的执行,无论操作码的值。 这通常可以通过使用条件移动(其中添加数据依赖关系,比预测的分支慢)或以其他方式寻找你的代码路径的对称性来完成。 考虑到你正在试图做这什么可能不会是可能的,如果是那么它几乎肯定会增加开销比对错误预测的15-25个时钟周期更大。
总之,在现代建筑有没有什么可以做,那将是比开关/箱效率更高,mispredicting分支的成本并不像你所期望的。
间接跳可能是用于指令解码做的最好的事情。
在老机器,像1997年说,英特尔P6,间接跳可能会得到一个分支预测错误。
在现代机器,好比说英特尔酷睿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路一棵树跳跃,因为有问题时,跳转表中的案件数量变得非常大 - 在工具,mkIrecog(使指令识别),我在80年代写的,我平时做跳转表到64K在大小条目,即跳跃16位。 时的编译器爆发时跳转表超过了尺寸(24位)16M。
数据驱动的,即指向其他节点的节点树,因为(a)在老机器间接跳转可能无法预测的好,和(b)事实证明,多的时候有指令之间共同的代码 - 而不是有跳转到每条指令的情况下,然后执行通用代码,然后再切换,并获得了第二次错误预测的分支预测错误,你做的通用代码,与稍微不同的参数(如,如何在指令流中有多少位你消费,下一个组比特分支上是(是)。
我是在mkIrecog非常积极,就像我说的最多允许在交换机中使用的32位,虽然实际的限制几乎总是阻止了我在16-24位。 记得我经常看见所述第一解码为16或18位开关(64K-256K的条目),并且所有其它解码小得多,不超过10位大。
嗯:我张贴mkIrecog入USENET回大约1990 ftp://ftp.lf.net/pub/unix/programming/misc/mkIrecog.tar.gz您可以看到使用的表,如果你的关心。 (善待:我年轻的时候,然后我不记得,如果这是帕斯卡或C.因为我已经重写了很多次 - 虽然我还没有完全重写,使用C ++位向量)
最让我知道其他人谁做这样的事情的同时做的事情字节 - 即8位,256路,分支或查找表)。
我想,既然没有人提到它,我想补充一下。
诚然,间接跳转很可能是最好的选择。
但是,你应该去与N-比较的方式,有一些,我想起两件事情:
首先,而不是做ñ平等比较,你可以做日志(N)的不平等比较的基础上,他们通过二分法数值码测试您的指令(或测试由比特数位如果该值空间已接近满)。这是一个像一个哈希表位,你实现一个静态树找到最终的元素。
其次,你可以运行你想要执行的二进制代码进行分析。 你甚至可以做到这一点每个二进制文件,执行之前,且运行时间修补你的模拟器。 这种分析将建立代表指令的频率直方图,然后让最频繁的指令得到正确预测你将组织你的测试。
但我不能看到这是不是媒体15次点球更快,除非你有MOV的99%,你把在其他测试之前,MOV操作码的平等。
文章来源: How to deal with branch prediction when using a switch case in CPU emulation