程序性能优化——五、程序编写时的优化(下)

对程序的编写进行优化是最常用、学习成本最低、硬件成本也最低的优化方式,所以我打算从这部分开始写具体的优化方式。以下从算法优化、数据结构优化、函数优化、循环优化、语句优化,五个方面展开论述

1算法优化

《五、程序编写时的优化(上)》

2数据结构优化

《五、程序编写时的优化(上)》

3函数优化

《五、程序编写时的优化(上)》

4循环优化

通常程序消耗时间最长的部分就是循环了,虽然主流的编译器会对循环进行一定程度的优化,但是很多复杂的控制过程还是需要手动修改达到最佳的性能提升效果。

循环不变量外提

循环不变量指的是在本循环范围内不改变的量,不单单是指常量,还可以指在本次循环中结果不变的表达式。

优化前

    for (i = 0; i < 5; i++) {
        for (j = 0; j < 3; j++) {
            sum += (j + 1)*array[i] * constant;
        }
    }

优化后

    for (i = 0; i < n; i++) {
        // 将循环内的计算移到循环外部
        int temp = array[i] * constant; // 计算循环外的不变量
        
        for (j = 0; j < m; j++) {
            sum += temp * (j + 1);
        }
    }

循环展开和压紧

将循环体内的代码复制多次执行,可以将迭代间并行转为迭代内并行,还可以发掘出数据级并行,利用向量运算等形式提高性能。

优化前

for (i = 0; i < 10; i++) {
        sum += i * i + i - 1; 
    }

优化后

for (i = 0; i < 10; i += 2) {
        sum += i * i + i - 1; 
        sum += (i + 1) * (i + 1) + i; 
    }

循环压紧并非是循环展开的逆运算,而是进一步将原本复制的循环语句转化为更紧凑的步骤执行,减少其中的冗余运算,比如向量化运算就是。

循环合并

循环合并是指将具有相同迭代空间的两个循环合并为一个循环。但是要注意好逻辑关系,有的循环关系是不能合并的。

优化前

    // 第一个循环
    for (i = 0; i < 10; i++) {
        sum1 += array[i] * i; 
    }

    // 第二个循环
    for (i = 0; i < 10; i++) {
        sum2 += array[i] * (i + 1); 
    }

优化后

    for (i = 0; i < 10; i++) {
        sum1 += array[i] * i; 
        sum2 += array[i] * (i + 1); 
    }

错误的合并

//合并前
for(int i=0; i<N; i++)
  A[i] = B[i] + C;
for(int i=0; i<N; i++)
  D[i] = A[i+1] + E;
//合并后
for(int i=0; i<N; i++){
  A[i] = B[i] + C;
  D[i] = A[i+1] + E;
}

循环分段

将单层循环变为双层循环,本身没啥,但有助于后续优化。

优化前

for(int i=0; i<128; i++){
  A[i] = B[i] + C[i];
}

优化后

int k=32;
for(int i=0; i<128; i+=k){
  for(int j=i; j<i+k-1; j++){
    A[j] = B[j]+C[j];
  }
}

循环交换

当两个嵌套的循环之间没有另外的语句时,称其为紧嵌套循环。有时候改变两个循环的内外关系,可以利用局部性原理对程序进行优化。

优化前

for(int i=0; i<N; i++){
  for(int j=0; j<N; j++){
    A[j][i] = B[j][i] + C[j][i];
  }
}

优化后

for(int j=0; j<N; j++){
  for(int i=0; i<N; i++){
    A[j][i] = B[j][i] + C[j][i];
  }
}

循环倾斜

讲起来比较麻烦,看图吧。

在这里插入图片描述

如图一所示,在对该二维数组进行运算时,内循环存在明显的依赖关系,所以内部循环无法并行执行(代码如下所示)。

for(int i=0; i<N; i++){
  for(int j=0; j<N; j++){
    A[i][j] = 1;
  }
}
for(int i=1; i<N; i++){
  for(int j=1; j<N; j++){
    A[i][j] = A[i-1][j] + A[i][j-1];
  }
}

在这里插入图片描述

但是,当我们将该二维数组倾斜,可以看到,每一列之间是可以并行执行的,所以可以利用如下代码加强它的并行性。

for(int i=0; i<N; i++){
  for(int j=0; j<N; j++){
    A[i][j] = 1;
  }
}
for(int j=2; j<2*N; j++){
  for(int i=max(1,j-N); i<min(N,j); i++){
    A[i][j-1] = A[i-1][j-i] + A[i][j-i-1];
  }
}

5语句优化

删除冗余语句

由于多次优化等问题,程序中可能存在死代码,即除了初始化之后再没有调用过的变量,或者永远不会执行的语句。将它们删除后能够减少运行的时空间。

代数变换

将复杂的、执行步骤多的代数过程,替换为等价的简单的、执行步骤少的代数过程。这也可以理解为程序员人脑预计算的过程。

优化前

a = (a+a)+(6*a)/2;

优化后

a =  5*a;

去除相关性

如果两个语句具有相关性,即先执行完A才能执行B,那么编译器就不能自动调整语序、也不能进行向量化等操作。所以,为了编译器的进一步优化,应该手动避免语句具有相关性。

相关性分为数据依赖关系控制依赖关系。

数据依赖关系指的是两个语句访问的是同一个变量,并且其中至少有一个是写操作(修改了变量值)。

依赖关系举例消除方式
真依赖a=1; xxx; b=a;
输出依赖a=1; xxx; a=2;重命名a1=1; xxx; a2=2;
反依赖a=b; xxx; b=2;重命名a=b1; xxx; b2=2;

控制依赖关系指的是a语句是b语句的条件语句,所以b不能先于a进行。

if(a){
  b;
}

以下是3种优化方法:

1.标量扩展

标量指的是只含有一个值的变量。标量扩展指的是将原本循环中需要一个标量的语句用一个数组来代替,从而能进行进一步地并行化。

优化前

#include <stdio.h>
int main() {
    int a[4] = {1, 2, 3, 4};
    int b[4] = {5, 6, 7, 8};
    int c;


    for (int i = 0; i < 4; ++i) {
        c = a[i] + b[i];
        printf("%d ", c);
    }
    printf("\n");

    return 0;
}

优化后

#include <stdio.h>

void vector_add(int *a, int *b, int *c, int n) {
    for (int i = 0; i < n; ++i) {
        c[i] = a[i] + b[i];
    }
}

int main() {
    int a[4] = {1, 2, 3, 4};
    int b[4] = {5, 6, 7, 8};
    int c[4];

    vector_add(a, b, c, 4);

    for (int i = 0; i < 4; ++i) {
        printf("%d ", c[i]);
    }
    printf("\n");

    return 0;
}

更进一步向量化优化

#include <stdio.h>
#include <immintrin.h> // 包含 AVX2 指令集的头文件

void vector_add(int *a, int *b, int *c, int n) {
    int i;
    __m256i va, vb, vc;
    for (i = 0; i < n; i += 8) {
        // 加载 8 个整数到 AVX 寄存器中
        va = _mm256_loadu_si256((__m256i*)&a[i]);
        vb = _mm256_loadu_si256((__m256i*)&b[i]);
        // 执行加法操作
        vc = _mm256_add_epi32(va, vb);
        // 将结果存储回内存
        _mm256_storeu_si256((__m256i*)&c[i], vc);
    }
}

int main() {
    int a[8] = {1, 2, 3, 4, 5, 6, 7, 8};
    int b[8] = {9, 10, 11, 12, 13, 14, 15, 16};
    int c[8];

    vector_add(a, b, c, 8);

    for (int i = 0; i < 8; ++i) {
        printf("%d ", c[i]);
    }
    printf("\n");

    return 0;
}

2.标量重命名

也即上述表中输出依赖和反依赖的优化方法,此处就不举例了。

3.数组重命名

其实类似于标量重命名。在循环中,可能多个数组的运算之间存在依赖关系,通过引入中间临时数组就可以消除该依赖。

优化前

for(int i=1; i<N; i++){
  A[i] = A[i-1] + 1;
  B[i] = A[i] + 2;
  A[i] = B[i] + 3;
}

优化后

for(int i=1; i<N; i++){
  A1[i] = A[i-1] + 1;//临时数组A1
  B[i] = A1[i] + 2;
  A[i] = B[i] + 3;
}

公共子表达式优化

当程序中的表达式含有多个相同的子表达式的时候,仅需要计算一次即可。(提取公因式嘛,也可以理解为局部地预计算以空间换时间)

优化前

if((a+b)>3 && (a+b)<5){
  a = a+b;
}

优化后

temp = a+b;
if(temp>3 && temp<5){
  a = temp;
}

分支语句优化

1.简化判断条件。

优化前

if((a1!=0) && (a2!=0) && (a3!=0)){
  xxx;
}

优化后

if(a1 && a2 &&a3){
  xxx;
}

2.生成选择指令

运用三目运算指令替代部分选择指令。

优化前

if(a>0){
  x=a;
}
else{
  x=b;
}

优化后

x = (a>b)?a:b;

3.运用条件编译

利用#if #ifdef #idndef #elif #endif等语句进行编译优化。

#include <stdio.h>

#define OPTION_A_ENABLED 1

int main() {
#if OPTION_A_ENABLED
    printf("Option A is enabled.\n");
#else
    printf("Option A is disabled.\n");
#endif

    return 0;
}

专栏安排(已有,或将有)

一、程序性能优化的意义

二、程序性能的度量指标

三、程序性能优化流程

四、程序性能的测量和分析

五、程序编写时的优化(上):算法优化、数据结构优化、函数优化

五、程序编写时的优化(下):循环优化、语句优化

六、访存时的优化(上):寄存器优化、缓存优化、内存优化

六、访存时的优化(下):磁盘优化、数据分布

七、编译与运行时的优化(上):编译器结构、编译选项、编译优化

七、编译与运行时的优化(下):数学库优化、运行时的优化

八、系统配置的优化

九、单核优化

十、OpenMP程序优化

十一、MPI程序优化

十二、…

如有不足之处,敬请批评指正

更欢迎在评论区留下你的见解,你的方法,如果有效我会增加在文章中,并@你。

  • 32
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值