缓存(例如branch target caching)、并行加载单元(流水线的一部分,还有诸如“命中未命中”之类不会使流水线停滞的东西)和out-of-order execution 可能有助于转换load -load-branch 变成更接近固定branch 的东西。流水线的解码或分支预测阶段的指令折叠/消除(正确的术语是什么?)也可能有所贡献。
所有这些都依赖于很多不同的东西,但是:有多少不同的分支目标(例如,你可能触发多少不同的虚拟重载),你循环了多少东西(是分支目标缓存“温暖”? icache/dcache 怎么样?),虚拟表或间接表如何在内存中布局(它们是缓存友好的,还是每个新的 vtable 加载都可能驱逐旧的 vtable?),缓存是否失效由于多核乒乓等原因反复...
(免责声明:我绝对不是这里的专家,我的很多知识都来自研究有序嵌入式处理器,所以其中一些是外推。如果您有更正,请随时发表评论!)
确定特定程序是否会出现问题的正确方法当然是分析。如果可以,请在硬件计数器的帮助下这样做——它们可以告诉您很多关于管道各个阶段的情况。
编辑:
正如 Hans Passant 在上述评论 Modern CPU Inner Loop Indirection Optimizations 中指出的那样,让这两件事花费相同时间的关键是能够在每个周期有效地“退出”多条指令。指令消除可以对此有所帮助,但superscalar design 可能更重要(未命中命中是一个非常小且具体的示例,完全冗余的负载单元可能更好)。
让我们假设一个理想的情况,假设直接分支只是一条指令:
branch dest
...间接分支是三个(也许你可以得到两个,但它大于一个):
load vtable from this
load dest from vtable
branch dest
让我们假设一个绝对完美的情况:*this 和整个 vtable 都在 L1 缓存中,L1 缓存的速度足以支持两次加载的每条指令成本分摊一个周期。 (您甚至可以假设处理器对负载重新排序并将它们与先前的指令混合在一起,以便它们有时间在分支之前完成;对于这个示例来说无关紧要。)还假设分支目标缓存是热的,并且没有管道分支的刷新成本,并且分支指令归结为单个周期(摊销)。
因此,第一个示例的 理论最短时间是 1 个周期(已摊销)。
第二个示例的理论最小值是 3 个周期(有 3 条指令),不存在指令消除或冗余功能单元或允许每个周期停用超过一条指令的东西!
间接加载总是会比较慢,因为有更多的指令,直到你进入像超标量设计这样的东西,它允许每个周期引退多条指令。
一旦你有了这个,这两个示例的最小值再次变为 0 到 1 个周期之间,前提是其他一切都是理想的。可以说,与第一个示例相比,第二个示例需要更理想的环境才能实际达到理论最小值,但现在有可能。
在您关心的某些情况下,您可能不会达到这两个示例的最低要求。要么分支目标缓存变冷,要么 vtable 不在数据缓存中,要么机器无法重新排序指令以充分利用冗余功能单元。
...这就是分析的用武之地,无论如何这通常是个好主意。
您可以首先支持对虚拟的轻微偏执。请参阅Noel Llopis's article on data oriented design、优秀的Pitfalls of Object-Oriented Programming slides 和Mike Acton's grumpy-yet-educational presentations。现在,如果您正在处理大量数据,您突然进入了 CPU 可能已经满意的模式。
像虚拟这样的高级语言特性通常是表达性和控制力之间的权衡。不过,老实说,我认为,只要提高你对 virtual 实际在做什么的认识(不要害怕不时阅读反汇编视图,并且一定要看看你的 CPU 的架构手册),你就会倾向于使用它什么时候有意义,什么时候没有意义,如果需要,分析器可以覆盖其余部分。
关于“不使用虚拟”或“虚拟使用不太可能产生可衡量的差异”的千篇一律的陈述让我不高兴。现实通常更复杂,要么你将处于足够关心或避免它的情况,要么你处于另外 95% 的情况下,除了可能的教育内容外,它可能不值得关心。