GCC编译优化与循环展开

一、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 函数调用了 addmul 两个函数,可以将它们合并为一个函数调用,如下所示:

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++) {
    
    
    sum += i; // 偶数
    sum -= (i+1); // 奇数
}

可以看到,循环展开后就没有分支了,当然实际项目中是减少分支,而不大可能取消分支,不然就不会写分支了^ _ ^
在未展开的循环中,每次循环都需要执行一次分支语句,根据分支预测的成功率,有一定的概率会出现分支预测失败的情况;而在展开后的循环中,分支语句的数量减少了一些,从而减少了分支预测失败的次数。

猜你喜欢

转载自blog.csdn.net/mrqiuwen/article/details/130215980