分支语句如何影响代码的性能?对于这些你可以做些什么?
原文链接:How branches influence the performance of your code and what can you do about it?
原作者:Johnny’s Software Lab LLC
时间:2020-7-5
汇编的角度
在C和C++中分支语句由需要被判断的条件和一些列满足条件时要执行的语句组成。在汇编层面,条件判断和分支语句通常是两条指令。下面是一个C语言中简单的例子:
if (p != nullptr) {
transform(p);
} else {
invalid++;
}
汇编仅仅有两种类型的指令:比较指令和根据比较结果执行的跳指令。所以以上C++的例子大致对应以下伪汇编代码:
p_not_null = (p != nullptr)
if_not (p_not_null) goto ELSE;
transform(p);
goto ENDIF;
ELSE:
invalid++;
ENDIF:
原始的C代码中会对条件 (p != nullptr)进行判断,如果它为假,则运行与 else 分支对应的指令。否则,我们会继续往下执行执行与 if 分支主体相对应的指令。
我们也可以用不同的方法来实现。我们可以执行与else代码块对应的指令然后跳转到与if块对应的指令,就像这样:
p_not_null = (p != nullptr)
if (p_not_null) goto IF:
invalid++;
goto ENDIF;
IF:
transform(p);
ENDIF:
虽然大多数时候编译器会为原始C++代码生成第一种汇编代码,但是开发者可以通过使用内置的GCC来影响这一过程。稍后我们会讨论然后告诉编译器生成什么类型的代码。
或许你可能会问为什么我们要提到汇编代码?在一些处理器中继续往下执行比跳转更高效,也就是说告诉编译器如何去精心安排、组织代码可能会带来更好的性能。
分支(Branches)和向量化(Vectorization)
分支对你的代码性能的影响方式比你想象的还要多。首先让我们来了解一下向量化-(你可以在这里了解更多关于向量化和分支的信息)。大多数现代CPU都有可以处理多个同类型数据的特殊向量指令。例如有一个指令从内存中可以加载四张图片,另一个指令则可以做四次加法还有另一个则会存储四个结果到内存中。
向量化的代码可以比其标量(单一值)对应的代码快上数倍。编译器知道这一点并且在一个向量自动化过程中通常会自动生成向量指令。但是向量自动化也有限制,这个限制来自于分支指令。思考一下以下代码
for (int i = 0; i < n; i++) {
if (a[i] > 0) {
a[i]++;
} else {
a[i]--;
}
}
对于编译器来说这个循环向量化非常困难,因为处理的类型取决于数据本身:如果a[i]是正数,我们就执行自增;反之,执行自减。没有一条可以在正数上执行自增并且在负数上执行自减的指令。编译器处理的类型取决于数据值,所以这段代码向量化很难。
最重要的是:内部热循环的分支语句使编译器非常难以实现向量化,甚至完全阻止向量化。如果编译器成功地将循环向量化,那么努力消除内部热循环的分支语句可以带来很大的速度提升。