来自http://www.boost.org/community/implementation_variations.html
“...编码差异,例如将类从虚拟成员更改为非虚拟成员或删除间接级别不太可能产生任何可衡量的差异,除非内部循环深入。甚至在内部循环中,现代CPU经常在相同数量的时钟周期内执行这些竞争代码序列!“
我试图理解“甚至在内环”部分。具体来说,CPU在相同数量的时钟周期内执行哪些机制来执行两个代码(虚拟与非虚拟或额外的间接级别)?我知道指令流水线和缓存,但是如何在与非虚拟调用相同的时钟周期内执行虚拟调用?间接是如何“丢失”的?
答案 0 :(得分:4)
缓存(例如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
让我们假设一个绝对完美的情况:*这个和整个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已经可能满意的模式。
虚拟等高级语言功能通常是表达和控制之间的权衡。老实说,我只是想通过提高你对虚拟实际操作的认识(不要害怕不时阅读反汇编视图,并且绝对偷看你的CPU架构手册),你会倾向于使用它什么时候有意义而不是什么时候,如果需要的话,探查者可以覆盖其余部分。
关于“不使用虚拟”或“虚拟使用不太可能产生可衡量的差异”的一刀切的说法让我感到不安。现实情况通常更复杂,要么你将处于一种你足够关心或避免它的情况,或者你处于其他95%的地方,除了可能的教育内容之外,它可能不值得关心。
答案 1 :(得分:3)
流水线是主要方式。
加载指令,解码,执行操作和加载间接内存引用可能需要20个时钟周期。但由于pipleline,处理器可以在流水线的不同阶段同时执行19个其他指令的部分,从而在每个时钟周期提供1个指令的总吞吐量,而不管通过流水线提供该指令实际需要多长时间。
答案 2 :(得分:1)
发生了什么,我认为处理器有一个特殊的缓存,可以保存分支和间接跳转的位置和目标。如果在$ 12345678遇到间接跳转,并且在上次遇到它时它转到地址$ 12348765,处理器甚至可以在解析分支地址之前开始在地址$ 12348765处推测执行指令。在许多情况下,在函数的内部循环中,特定的间接跳转将始终在循环的整个持续时间内跳转到相同的地址。因此,间接跳转缓存可以避免分支惩罚。
答案 3 :(得分:1)
现代CPU使用自适应分支预测技术,该技术可以预测许多间接跳转,例如虚拟函数的vtable实现。见http://en.wikipedia.org/wiki/Branch_prediction#Prediction_of_indirect_jumps
答案 4 :(得分:0)
如果CPU已经在缓存中有内存地址,那么执行加载指令是微不足道的,如果是这样的话。