原文链接:How branches influence the performance of your code and what can you do about it?
原作者:Johnny’s Software Lab LLC
时间:2020-7-5
分支语句(或者是跳转语句)是最常用的指令类型之一。据统计,每五个指令就有一个是分支语句指令。分支语句可以有条件或无条件地改变程序的执行流程。一个有效的分支语句执行对于CPU的高性能来说至关重要。
在介绍分支语句是如何影响CPU性能之前,我们需要简单介绍一下CPU的内部结构。
CPU内部
许多现代的处理器(但并不是所有,特别是在嵌入式系统中使用的处理器)有部分或以下所有功能:
-
管线(Pipeline):管线允许CPU同时执行多条指令,这是因为CPU会将每一条指令的执行分成多个阶段,并且每一条指令都处在不同的执行阶段。汽车工厂也采用了同样的原理:在任何给定的时间段,工厂都有50辆汽车被同时生产,例如,一辆汽车在刷漆,另一辆在安装引擎,第三辆则在安装灯光,等等。管线可能会很短,只有几个阶段,也可能很长,包含很多阶段。
-
乱序执行(Out of order execution):从程序员的视角来看,程序会按顺序逐条地运行指令。但在CPU看来却是完全不同,CPU不需要按指令在内存中出现的顺序逐条执行它。在执行时,因为需要等待来自内存中或另一条指令的数据,所以一些指令会被阻塞在CPU中。CPU会提前考虑并执行之后那些没有被阻塞的指令。当被阻塞的指令的数据可用时,之前没有被阻塞的指令已经执行完成了。这节省了CPU的周期。
-
前瞻执行(speculative execution):即使CPU无法100%确定指令需要执行时,它也会开始执行指令。例如,处理器在执行条件分支指令时,会猜测分支的目标位置,并开始执行目标位置处的指令,尽管在此时它还没有100%确定该分支条件是否成立。如果之后CPU认为猜测是错的,它就会取消猜测指令执行得到的结果,并使所有内容看起来就像没有进行过任何推测一样。
-
分支预测(Branch prediction):现代CPU都有特殊的电路用于记录每一个分支指令之前的状态:分支条件是否成立。当同样的分支指令在下一次被执行时,CPU就会使用这个状态去猜测分支语句的目标并开始在分支目标处推测性执行指令,一旦分支预测对了性能就会得到提升。
所有现代的处理器为了更好地利用CPU的资源都有管线这一功能,并且大多数还有分支预测和前瞻执行。就乱序执行而言,由于它能耗高且对运行速度提升并不明显,所以大多数低端低功耗处理器都不具备此功能。但不要过于字面地理解这句话,因为这些信息在几年后可能会过时。
你可以通过这篇文章——Jason Robert Carey Patterson: Modern Microprocessors – a 90 minutes guide,了解更多关于现代CPU的信息。
现在让我们来看一下CPU是如何影响分支执行的。
CPU如何影响分支执行
从管线的角度来看,CPU的分支指令是一个非常耗费性能的操作。
当一条分支指令进入CPU管线中时,解码之前,其分支目标是未知的,并且需要计算其目标。跟随分支指令执行的指令可以是:1)直接跟随在分支指令后面的指令或者是2)位于分支目标处的指令。
对于具有管线功能的CPU来说这是一个问题,为了保持管线满载、避免减速,处理器需要在分支指令被解码之前知道分支目标。这取决于CPU似乎如何被设计的,它可以是以下几种情况:
-
暂停管线(术语叫做"stall the pipeline")并且停止解码指令,直到分支指令被解码且分支目标是已知的,然后它就可以将争取的指令加载到管线中。
-
加载紧跟在分支指令之后的指令。如果后来发现这是错误的选择,处理器将需要清空管线并开始从分支目标处加载正确的指令。
-
询问分支预测是否要加载紧跟在分支指令之后的指令或者分支目标处的指令,分支预测需要告诉管线分支目标的位置(否则新指令需要等到CPU处理完所有的分支目标才能被加载)
我认为第一种情况如今很少见,一般出现在低端的嵌入式处理器上。让处理器怎么都不做是一种对它的资源的浪费,所以大多数处理器会使用第二种情况代替第一种。第二种情况的处理器在低端嵌入式系统和低功耗处理器中更常见,第三种情况则在台式机与笔记本以及高性能CPU中更常见。
没有分支预测的CPU中的分支指令
在没有分支预测的CPU种,CPU会加载紧跟在分支指令之后的指令,一旦分支条件成立,CPU就会清空错误加载的指令。所以一旦分支条件不成立,分支指令执行代价就会更低。记住,通常犯错的代价并不高,因为这些处理器往往具有简单的设计和较短的管线。
有分支预测的CPU中的分支指令
如果CPU有分支预测和前瞻执行如果分支预测是正确的那么分支指令执行的代价就会很低。一旦它是错误的,分支指令的执行代价就会很高。对于管线比较长的CPU来说尤其如此,在这种情况下如果出现错误的预测, CPU将需要清空许多指令。具体的错误预测代价会有很大的变化,但是通常的规则是越昂贵的CPU,分支错误预测的代价就越高。
一些分支很容易就可以去预测而另一些却很困难。为了说明这一点,想象一个这样的算法,循环遍历一个数组并且找到它的最大的元素。对于一个具有随机元素的数组来说,它的条件 if (a[i] < max) max = a[i]在大多数情况下都是错的。现在想象第二个算法,统计数组中小于数组平均值的元素数量。在一个随机数组中,条件if (a[i] < mean) cnt++对于分支预测器来说,预测这个情况会非常困难。
简单说一下前瞻执行。前瞻执行是一个更广泛的术语,但在分支指令的上下文中,前瞻执行意味着对分支条件进行猜测。现在通常情况下,分支条件无法被评估,因为CPU需要等待数据或者等待其他指令完成。前瞻执行允许CPU至少执行一些在分支语句体中的指令,当最终评估分支条件时,这些执行的指令可能是有用的这样CPU就节省了一些周期很好;或者也可能是无用的CPU就会放弃这些指令。
p_not_null = (p != nullptr)
if_not (p_not_null) goto ELSE;
transform(p);
goto ENDIF;
ELSE:
invalid++;
ENDIF: