一、GCC各级优化简述
一般默认情况下就是-O0,没有任何优化。
O1到O3,优化级别逐渐升高,且级别高的包含前面级别低的所有优化
。
只要开了优化,无论-O1, -O2, -O3代码执行顺序都会被打乱,对于调试不太好。
每个优化选项里面还包括很多细分的优化选项,有时候也可以打开其中某一个子项,在gcc编译命令里,具体的可以看这篇博客
另外,一般线上项目都会打开O2优化,不优化的时候是本地调试用的,实际工程运行肯定开优化更好。而且,常说的STL各容器效率不如自己数组实现,那是在没开O2优化的时候(打竞赛可能不能开启优化),STL各容器在在O2开启的前提下效率还是很高的,STL各容器设计已经最小化各自开销了。
-O1优化:
这个级别开始就会对指令进行重排
了(编译器级别的,不是CPU级别)、会简单的删除一些多余的条件分支、减少不必要的栈帧开辟
-O2优化:
这个级别并不会做循环展开!!ChatGPT会说O2有循环展开,这是错误的;
-O2会打开所有-O1的优化,它的额外优化比如:
1)强制将内存上的数据拷贝到寄存器上再执行该数据相关的指令,所以如果多个指令都要用到该数据,那就节省了再次将数据从内存拷贝到寄存器的时间(比如a = 2,然后有很多变量都要+a,那先把a拷贝到寄存器,然后执行所有+a的指令,比每次遇到+a指令,再将a的值从内存中拷贝到寄存器更高效)
2)还可以多个函数调用合并为一个函数调用(这个在编译选项-funit-at-a-time
中做到,该选项可以使编译器在生成机器指令前,先分析整个的汇编语言代码,从而执行一些优化),比如下列例子:
int add(int a, int b) {
return a + b;
}
int mul(int a, int b) {
return a * b;
}
int add_and_mul(int a, int b, int c) {
int sum = add(a, b);
return mul(sum, c);
}
在上面的代码中,add_and_mul
函数调用了 add
和 mul
两个函数,可以将它们合并为一个函数调用,如下所示:
int add_and_mul_opt(int a, int b, int c) {
return a * b + c;
}
-O3优化:
这个级别才有循环展开
、函数内联
下面介绍循环展开。
二、循环展开
(循环展开是减少循环次数,不是完全不要循环
)
循环展开优化是一种优化技术,它通过减少循环迭代的次数来提高程序的性能。这种技术通常用于减少循环的开销,提高CPU的利用率
和减少分支预测失败的次数
。
(1)提高CPU利用率
在一个循环里,各个语句可以并发执行,一个语句最终是多个指令,那么语句越多,就有更大概率让执行指令并行执行,比如下面的例子:
for(int i = 0; i < n; i++){
a[i] += b[i];
c[i] += d[i];
}
做循环展开后:
for(int i = 0; i < n; i += 2){
a[i] += b[i];
c[i] += d[i];
a[i+1] += b[i+1];
c[i+1] += d[i+1];
}
上面是手动写循环展开,来模仿编译器的优化。虽然看着循环展开前后,总的执行次数一样(总的语句数量一样)。
但其实是,在每一个循环里
,各个指令是可以并行执行的,也就是CPU的指令级并行
。
比如第一种情况,同一时刻执行两句话的那些指令(不只是两个指令哦,估摸着是6个)
而第二种情况,则是同时执行四句话的那些指令(12个)。
这样效率就上来了。
(2)减少分支预测失败的次数
因为循环展开可以减少分支语句的数量
,从而减少分支预测失败的次数。当循环被展开时,分支语句在循环内的执行次数也会减少,从而减少了分支预测失败的可能性,如下例子:
for (int i = 0; i < n; i++) {
if (i % 2 == 0) {
// 偶数
sum += i;
} else {
// 奇数
sum -= i;
}
}
循环展开后:
for (int i = 0; i < n; i+=2) {
sum += i; // 偶数
sum -= (i+1); // 奇数
}
可以看到,循环展开后就没有分支了,当然实际项目中是减少分支,而不大可能取消分支,不然就不会写分支了^ _ ^
在未展开的循环中,每次循环都需要执行一次分支语句,根据分支预测的成功率,有一定的概率会出现分支预测失败的情况;而在展开后的循环中,分支语句的数量减少了一些,从而减少了分支预测失败的次数。