与返回一个父间接分支相比,直接从一个块跳转到另一个块通常是分支预测的胜利,尤其是在早于 Intel Haswell 的 CPU 上。
通过从每个块的尾部跳转,每个分支都有不同的分支预测器历史。给定块通常跳转到相同的下一个块,或者具有几个目标地址的简单模式,这可能是常见的。这通常可以很好地预测,因为每个分支都有一个更简单的模式,并且分支历史分布在多个分支上。
如果所有调度都发生在单个间接分支上,则可能只有一个 BTB(分支目标缓冲区)条目,并且该模式将过于复杂而无法很好地预测。
Intel Haswell 中的现代 TAGE 分支预测器以及后来使用最近的分支历史记录(包括间接分支目标)对 BTB 进行索引,这实际上解决了这个问题。见评论X86 64 位模式上的索引分支开销 https://stackoverflow.com/questions/47052536/indexed-branch-overhead-on-x86-64-bit-mode#comment81079172_47052536,然后在中搜索 Haswellhttps://danluu.com/branch-prediction/ https://danluu.com/branch-prediction/。一个分支的复杂模式可能会将其预测分散到许多 BTB 条目中。
具体来说,分支预测和解释器的性能 -
不要相信民间传说 https://hal.inria.fr/hal-01100647/document (2015)Rohou、Swamy 和 Seznec 在解释器基准上对 Nehalem、SandyBridge 和 Haswell 进行了比较,并测量了单个调度循环的实际错误预测率switch
陈述。他们发现 Haswell 做得更好,可能使用了 ITTAGE 预测器。
他们不测试 AMD CPU。自从 Piledriver 使用 Piledriver 以来,AMD 已经发布了一些有关其 CPU 的信息用于分支预测的感知器神经网络 https://en.wikipedia.org/wiki/Branch_predictor#Neural_branch_prediction。我不知道他们用单个间接分支处理调度循环的效果如何。 (AMD 自 Zen 2 使用 IT-TAGE 作为二级分支预测器 https://en.wikichip.org/wiki/amd/microarchitectures/zen_2#Branch_Prediction_Unit,除了他们从 Zen 1 中保留的散列感知器之外。)
达里克·米霍卡讨论这个模式 http://www.emulators.com/docs/nx25_nostradamus.htm在解释 CPU 仿真器的上下文中,它针对不同的指令(或简化的微指令)从一个处理程序块跳转到另一个处理程序块。他详细介绍了各种策略在 Core2、Pentium4 和 AMD Phenom 上的性能。 (写于2008年)。当前 CPU 上的现代分支预测器最类似于 Core2。
他最终提出了他所谓的诺查丹玛斯分配器模式,以分支预测友好的方式检查提前退出(函数返回函数指针,或“防火通道”哨兵)。如果您不需要,请参阅文章的前半部分,其中他讨论了块之间的直接跳转链接与中央分发器。
他甚至抱怨 x86 中缺少代码预取指令。对于 Pentium 4,这可能是一个更大的问题,其中填充跟踪缓存的初始解码是very与从跟踪缓存运行相比,速度较慢。 Sandybridge 系列有一个解码的 uop 缓存,但它不是跟踪缓存,并且解码器仍然足够强大,不会在 uop 缓存未命中时出现问题。锐龙也类似。
相对于堆栈指针或任何其他指针访问数据之间有区别吗?
不,你甚至可以设置rsp
跳转后,每个块都可以有自己的堆栈。如果您安装了任何信号处理程序,rsp
需要指向有效的内存。另外,如果您希望能够call
任何普通的库函数,你需要rsp
用作堆栈指针,因为他们想要ret
.
是否存在间接跳转的预取(跳转到寄存器中存储的值?)。
预取到 L2 可能很有用如果您在准备好执行间接跳转之前就知道分支目标地址。当前所有 x86 CPU 都使用分离式 L1I / L1D 缓存,因此prefetcht0
会污染L1D而没有任何增益,但是prefetcht1 http://felixcloutier.com/x86/PREFETCHh.html可能有用(提取到 L2 和 L3)。或者,如果代码在 L2 中已经很热门,那么它可能根本没有用。
也很有用:尽早计算跳转目标地址,因此乱序执行可以解决分支,同时大量工作在乱序核心中排队。这可以最大限度地减少管道中潜在的气泡。如果可能的话,保持计算独立于其他内容。
最好的情况是在寄存器中寻址许多指令之前jmp
,所以只要jmp
在执行端口上获取一个周期,它可以向前端提供正确的目的地(如果分支预测出错则重新引导)。最坏的情况是分支目标是分支之前指令的长依赖链的结果。几个独立的指令和/或内存间接跳转就可以了;一旦这些指令进入 OOO 调度程序,乱序执行就应该找到运行这些指令的周期。
还有分离的 L1iTLB 和 L1dTLB,但 L2TLB 在大多数微架构上通常是统一的。但是 IIRC,L2TLB 充当 L1 TLB 的受害者缓存。预取可能会触发页面遍历以填充 L1 数据 TLB 中的条目,但在某些微体系结构上这无助于避免 iTLB 缺失。 (至少它会将页表数据本身放入 L1D 中,或者可能是页遍历硬件中的内部页目录缓存,因此同一条目的另一个页遍历会很快。但是由于 Intel Skylake(及更高版本)以外的 CPU只有 1 个硬件页面遍历单元,如果在第一页遍历仍在发生时发生 iTLB 未命中,它可能无法立即启动,因此如果您的代码如此分散以至于出现 iTLB 未命中,实际上可能会造成伤害.)
对要 JIT 的内存块使用 2MB 大页面,以减少 TLB 未命中。可能最好将代码放置在相当紧凑的区域中,并将数据分开。 DRAM 局部效应是真实存在的。 (我认为 DRAM 页面通常大于 4kiB,但这是硬件问题,您无法选择。在已打开的页面中访问的延迟较低。)
See 阿格纳·福格的微建筑 pdf http://agner.org/optimize/,并且英特尔的优化手册。 https://software.intel.com/en-us/articles/intel-sdm#optimization。 (如果您担心 AMD CPU,还有 AMD 的手册)。查看更多链接x86 /questions/tagged/x86标签维基。
这个想法可行吗?
应该是。
如果可能,当一个块总是跳转到另一个块时,请通过使块连续来消除跳转。
数据的相对寻址很简单:x86-64 具有 RIP 相对寻址。
You can lea rdi, [rel some_label]
然后从那里建立索引,或者直接对某些静态数据使用 RIP 相对寻址。
您将对代码或其他内容进行 JIT 操作,因此只需计算从当前指令末尾到要访问的数据的有符号偏移量,这就是 RIP 相对偏移量。位置无关代码 + 静态数据在 x86-64 中很容易。
在花岗岩急流及之后, PREFETCHIT0 [rip+rel32]
将代码预取到“所有级别”的缓存中,或者prefetchit1
预取到除 L1i 之外的所有级别。
这些指令是 NOP,其寻址模式不是 RIP 相关的,或者在不支持它们的 CPU 上。 (也许他们还启动 iTLB 甚至 uop 缓存,或者至少在纸面上可以。)截至 2022 年 12 月的英特尔“未来扩展”手册中的文档建议目标地址是某些指令的开头。
仅当您足够早地进行预取时,预取才有用,并且它不能解决错误预测问题。对于解释器来说,预取字节码指令的代码可能是也可能不是胜利after当前的一个。prefetchit0
不能这样做,它只适用于 RIP 相对寻址,不适用间接寻址。也许是因为 CPU 的代码获取部分(如 L1i 和 iTLB)没有用于任意地址的 AGU,如果它通过将地址提供给这些部分来工作?因此,它对于预取运行时变量代码位置没有帮助。