数据结构 - 分支预测,为什么使用 if/else 语句会降低程序效率
if 语句与运行效率
说起if语句导致程序运行效率下降,就不得不提到CPU的流水线结构,效率降低主要是由于多级流水线结构造成的。
现代的大部分CPU在执行代码时并不是读取一条指令,然后执行一条,而是使用了一种叫做流水线技术的方式,同时去执行多个操作。
流水线的影响
比如三级流水线就是指,CPU在执行一条指令时,同时会读取后面的指令,并对进行译码。(读取、译码、执行)
这样处理的优势很明显,使用流水线技术可以大大的提高执行效率。但是它并不是所有时刻有效的,在程序中执行跳转代码时,CPU 会丢弃流水线现有的结果,因为不执行后面的代码了,提前读取也没有用。
所以在这个时候if语句相对于顺序执行的指令,会有几个时钟周期的差距。但这不是if语句说特有的,所有带跳转结构的语句都会这样(if、switch、for)。
分支预测的含义
条件分支指令通常具有两路后续执行分支。即不采取(not taken)跳转,顺序执行后面紧挨JMP的指令,以及采取(taken)跳转到另一块程序内存去执行那里的指令。是否需要跳转,只有到真正执行时才能确定。如果没有分支预测器,处理器将会等待分支指令通过了指令流水线的执行阶段,才把下一条指令送入流水线的第一个阶段—取指令阶段(fetch stage),这种技术叫做 流水线停顿。分支预测器就是猜测条件判断会走哪一路,如果猜对,就避免流水线停顿造成的时间浪费。如果猜错,那么流水线中推测执行的那些中间结果全部放弃,重新获取正确的分支路线上的指令开始执行,这导致了程序执行的延迟。分支预测简单说就是猜测后面的程序会执行那一段代码,并提前将它读取。
分支预测的影响
多级流水线在遇到跳转时,会有几个时钟的周期的影响,但这并不是它被指控运行效率低的主要原因。而是在因为它分支预测部分,它有可能有10-20个时钟周期的影响,在大量使用 if 的地方这种影响将被放大。多级流水在遇到跳转指令时会清空当前流水线,CPU的设计者在设计引入了一种叫做分支预测的技术来进行处理这个问题。
例如:一辆火车,在有很多岔道的路上前进,为了不让每次都在岔道停下等待(清空流水线),于是想出了一个办法。现在猜测火车需要前进的方向,如果猜中了火车就可以不用停下等待,而提高效率。但是如果猜错了,则需要倒车回到岔路口重新选择。这样的错误代价就比较高了,而大家所说的效率降低主要源于此。
if-else 对程序结构的影响
在大部分情况下,是不需要考虑if语句对代码执行效率的影响,甚至感觉不到它的存在。
因为大部分情况下,CPU的性能是足够的(性能优化时除外)。但if-else对程序结构的影响却是不容忽视的,开发者可直观的感受到它的存在(可读性),而且对开发和维护有极大的影响。
if-else 对程序效率的影响
简单示例:
package main
import (
"fmt"
"time"
)
func testPredictableIfElse() {
start := time.Now()
for i := 0; i < 1000000; i++ {
if i < 500000 {
// 执行一些简单操作
} else {
// 执行一些简单操作
}
}
elapsed := time.Since(start)
fmt.Printf("Predictable if/else elapsed time: %v\n", elapsed)
}
func testUnpredictableIfElse() {
start := time.Now()
for i := 0; i < 1000000; i++ {
if i%2 == 0 {
// 执行一些简单操作
} else {
// 执行一些简单操作
}
}
elapsed := time.Since(start)
fmt.Printf("Unpredictable if/else elapsed time: %v\n", elapsed)
}
func main() {
testPredictableIfElse()
testUnpredictableIfElse()
}
运行结果:
分别测试了分支走向可预测(例如 i < 500000)和不可预测(例如 i%2 == 0)的两种情况。通过比较它们的执行时间,可以观察到不可预测的分支走向可能导致执行时间更长,从而体现出对程序效率的影响。