如何协助编译器生成高效代码

本文介绍了如何通过消除连续函数调用、减少过程调用、消除不必要的内存引用、循环展开、提高并行性和重新结合变换等方法,帮助编译器生成更高效的代码。针对C语言,这些策略可以提升程序性能,降低开销,同时适用于C++和Java等语言。
摘要由CSDN通过智能技术生成

优化程序性能的基本策略,一个是选择合适的数据结构和算法,另一个是遵循基本编码规则,避免限制优化的因素,协助编译器产生高效的代码。

本文从C语言的层面介绍协助编译器代码优化的一些建议,其他语言比如C++、Java亦可借鉴。

示例代码:

typedef struct{
  long len;
  data_t* data;
}vec_rec, *vec_ptr;

//实际程序中,数据类型data_t可以被声明为int、long、double等基本数据类型

//从数组中获取一个元素,保存在dest所指位置
int get_vec_element(vec_ptr v, long index, data_t* dest){
  if(index < 0 || index >= v->len)
    return 0;
  *dest = v->data[index];
  return 1;
}

//获取数组的长度
long vec_length(vec_ptr v){
  return v->len;
}

1、消除连续的函数调用

代码1:

void combine1(vec_ptr v, data_t* dest){
  long i;
  
  *dest = 0;
  for(i = 0; i < vec_length(v); ++i){
    data_t val;
    get_vec_element(v, i, &val);
    *dest = *dest + val;
  }
}

在代码1中,for循环的判断条件中调用了 vec_length(v) 这个函数,每次循环都需要调用这个函数。

代码2:

void combine2(vec_ptr v, data_t* dest){
  long i;
  
  *dest = 0;
  long length = vec_length(v);
  for(i = 0; i < length; ++i){
    data_t val;
    get_vec_element(v, i, &val);
    *dest = *dest + val;
  }
}

在代码2中,将原先for循环中的 vec_length(v) 函数移到了循环外部,单独保存结果,再应用到for循环中,避免每轮循环都调用返回值固定的函数。

虽然编译器的优化工作可能会尝试对代码1作出如代码2一样的优化,但它通常会非常谨慎,因为编译器无法确定这样的优化是否会产生副作用,导致优化后程序的运行结果与原来不一致。

因此,为了改进代码,用户通常需要显式地辅助编译器作出这样的优化。

2、减少过程调用

代码3:

void combine3(vec_ptr v, data_t* dest){
  long i;
  
  *dest = 0;
  long length = vec_length(v);
  data_t* data = v.data;
 	
  for(i = 0; i < length; ++i){
    *dest = *dest + data[i];
  }
}

在代码3中,去除了代码2循环中的过程调用,因为过程调用会带来一些开销。

3、消除不必要的内存引用

代码4:

void combine4(vec_ptr v, data_t* dest){
  long i;
  
  *dest = 0;
  long length = vec_length(v);
  data_t* data = v.data;
  data_t acc = 0;
 	
  for(i = 0; i < length; ++i){
    acc += data[i];
  }
  *dest = acc;
}

在代码4中,将累积相加的结果保存在局部变量acc中,这样消除了原来代码3中每次循环都要先从内存中读再将更新值写回的需要。

优化后,每次循环只要从内存中加载data[i]的值,本例中局部变量是保存在寄存器中,因此将在寄存器中累积计算该值。

:很多时候,局部变量会存储在寄存器中。但是也有一些情况下,局部数据必须存放在内存中,常见的情况包括:

  • 寄存器不足够存放所有的本地数据
  • 对一个局部变量使用地址运算符"&",必须能够为它产生一个地址
  • 某些局部变量是数组或结构体,因此必须能够通过引用才能被访问到

4、循环展开

代码5:

void combine5(vec_ptr v, data_t* dest){
  long i;
  long length = vec_length(v);
  long limit = length - 1;
  data_t* data = v.data;
  data_t acc = 0;
  
  for(i = 0; i < limit ;i+=2){
    acc = (acc + data[i]) + data[i+1];
  }
  
  for(; i < length; ++i){
    acc = acc + data[i];
  }
  *dest = acc;
}

循环展开是一种程序变换,通过增加每次迭代计算的元素的数量,减少了循环的迭代次数。

代码5采用了“2×1”循环展开,每个循环处理数组的2个元素。

然而,虽然迭代次数减半了,但每次迭代中还是有两个顺序的加法操作。关键路径的操作数依然受限。

5、提高并行性

在代码5中,我们将累积值放在单独的变量acc中,在前面的计算完成之前,都不能计算acc的值。

代码6:

void combine6(vec_ptr v, data_t* dest){
  long i;
  long length = vec_length(v);
  long limit = length - 1;
  data_t* data = v.data;
  data_t acc0 = 0;
  data_t acc1 = 0;
  
  for(i = 0; i < limit ;i+=2){
    acc0 = acc0 + data[i];
    acc1 = acc1 + data[i+1];
  }
  
  for(; i < length; ++i){
    acc0 = acc + data[i];
  }
  
  *dest = acc0 + acc1;
}

在代码6中,我们将一组合并运算分割成两个部分,acc0和acc1的值互相没有关系,那么就可以进行两路并行计算了,性能得到了很好地提升。

我们知道,补充运算是可交换和可结合的,就算是溢出时也是如此。对于整数类型,在所有可能的情况下,代码6的结果和代码5相同。因此,优化编译器也能够潜在地将代码4转换成代码5,再转换成代码6。

但是,浮点乘法和加法是不可结合的,四舍五入或溢出可能会让代码5和代码6所产生的结果不同。因此,大多数编译器不会尝试对浮点代码进行这种变换。程序员需要与用户进行协商,看看修改程序后的结果是否能被接收。

6、重新结合变换

void combine7(vec_ptr v, data_t* dest){
  long i;
  long length = vec_length(v);
  long limit = length - 1;
  data_t* data = v.data;
  data_t acc = 0;
  
  for(i = 0; i < limit ;i+=2){
    acc = acc + (data[i] + data[i+1]);
  }
  
  for(; i < length; ++i){
    acc = acc + data[i];
  }
  *dest = acc;
}

代码7在代码5的基础上,更改了循环内部加法的结合方式。虽然还是有两次的加法操作,但是只有一个加法操作形成了循环寄存器的数据相关链,也就是说第一个加法不需要等待前一次迭代的累加值就可以执行,性能可以得到提升。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值