[英]Why is it good to avoid instruction branching where possible?
我经常读到,从一个perf的角度看,在汇编指令级别的分支是不好的。 但我还没有真正理解为什么会这样。 所以为什么?
大多数现代处理器预取指令,甚至在代码流到达该指令之前推测性地执行它们。 有一个分支意味着突然有两个不同的指令可能是下一条指令。 至少有三种可能的方式可以与预取相互作用:
根据处理器和特定代码,与没有分支的等效代码相比,分支可能会或可能不会产生显着的性能影响。 如果执行代码的处理器使用分支预测(大多数情况下)并且大部分正确猜测特定代码段,则可能不会对性能产生显着影响。 另一方面,如果它大多猜错了,它可能会给你一个巨大的减速。
对于特定的代码段,很难预测删除分支是否会显着加快代码速度。 当微观优化时,最好测量两种方法的性能而不是猜测。
这很糟糕,因为它干扰了指令预取 。 现代处理器可以开始加载下一个命令的字节,同时仍然处理第一个字节以便更快地运行。 当分支发生时,必须丢弃预取的“下一个命令”,这浪费时间。 在紧密的循环或类似内部,那些错过的预取可以加起来。
因为处理器不知道它应该预取执行的指令,如果你给它的可能性。 如果分支以不同于预期的方式运行,则必须刷新指令管道,因为那些加载的指令现在是错误的,这使得它慢了几个循环......
除了预取问题,如果你跳,你没有做其他工作......
如果您考虑一下汽车装配线,您会听到一天内X号汽车上线的事情。 这并不意味着原材料在生产线的开始处开始,X数字在一天内完成整个运行。 谁知道它可能不会但每辆车开始结束需要几天,这就是装配线的重点。 想象一下,如果由于某种原因你有一个制造业的变化,你基本上不得不冲洗生产线上的所有汽车并将其废弃或抢救他们的零件,以便在其他时间放在另一辆车上。 需要一段时间才能填满装配线并每天回到X号车。
处理器中的指令管道工作方式完全相同,管道中有数百个步骤,但概念是相同的,以保持每个时钟周期执行率(每天X个车辆数)的一个或多个指令保持该管道顺利运行 所以你预取,会烧掉一个内存周期,这通常很慢,但缓存层有帮助。 解码,需要另一个时钟,执行,可以在像x86这样的CISC上占用很多时钟。 当你执行分支时,在大多数处理器上,你必须丢弃执行和预取中的指令,如果你考虑一般的简化管道,基本上是管道的2/3。 然后你必须等待那些时钟进行获取,并在你恢复顺利执行之前进行解码。 最重要的是,fetch,有点定义,不是下一条指令,有些百分比的时间超过了一个高速缓存行,有一定百分比的时间意味着从内存中获取或更高层的高速缓存,这是更多的时钟循环比你线性执行。 另一种常见的解决方案是,一些处理器声明无论在分支指令之后是什么指令,或者有时总是执行分支指令之后的两条指令。 这种方式在刷新管道时执行,一个好的编译器会安排指令,以便在每个分支之后有一个其他的nop。 懒惰的方式是在每个分支之后放一个或一个nop,创造另一个性能命中,但对于该平台,大多数人将使用它。 第三种方式是ARM做的,有条件执行。 简而言之,前向分支,这不是那么罕见,而不是说条件如果条件,你标记你试图分支的几个指令与执行,如果没有条件,他们进入解码并执行和执行作为nops和管道继续前进 ARM依赖于传统的刷新和重新填充更长或更后的分支。
较旧的x86(8088/86)手册以及其他处理器的其他同样旧的处理器手册以及微控制器手册(新旧)仍将发布每条指令执行的时钟周期。 对于分支指令,如果分支发生,则会添加x个时钟。 你的现代x86,甚至ARM和其他旨在运行Windows或Linux或其他(庞大而缓慢)操作系统的处理器都不会打扰,他们经常只是说它每个时钟运行一条指令或谈论mips到兆赫或类似的东西而不是必然每条指令都有一个时钟表。 你只假设一个,记住这就像每天一辆车,它的最后一个执行时钟而不是其他时钟到达那里。 特别是微控制器人员每条指令不处理一个时钟,并且必须比普通桌面应用程序更了解执行时间。 看看其中一些Microchip PIC(不是PIC32,即mips),msp430,绝对是8051的规格,尽管这些是由许多不同公司制造的,但它们的时序规格变化很大。
最重要的是,对于桌面应用程序甚至操作系统上的内核驱动程序,编译器效率不高,操作系统增加了更多的开销,您几乎不会注意到时钟节省。 切换到微控制器并放入太多分支,您的代码速度最慢可达2或3倍。 即使使用编译器而不是汇编程序。 使用编译器(不是用汇编语言编写)可以/将使你的代码慢2到3倍,你必须平衡开发,维护和可移植性与性能。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.