2013 0414 优化程序性能

  本周继续。

  如何让程序跑的更快,是每一个做程序员的职责。让程序跑的快的好处这里就不阐述了,编写搞笑的程序需要几类活动:

1、选择高效的算法和数据结构

2、能够写出编译器有效优化转换成可执行代码的源代码(这需要理解编译器的能力和局限性)

3、针对处理运算量特别大的计算,需要将一个任务分成多部分


编译器的能力和局限性

void twiddle1(int *xp, int *yp)

{

*xp += *yp;

*xp += *yp;

}


void twiddle2(int *xp, int *yp)

{

*xp += 2* *yp;

}

  乍一看,这两个函数的功能是一样的。 而且twiddle2 的效率更好一点,它只要求3次存储器的引用,而twiddle1却要6次存储器的引用。

不过考虑 xp 等于yp的情况下,teiddle1会执行下面的结果:

*xp += *xp; *xp +=  *xp;  结果是xp的结果会增加4倍,

而teiddle2来说

*xp += 2* *xp; 这时xp的结果是增加3倍的

编译器不知道twiddle1何时被调用,因此它必须假设参数xp和yp可能会相等, 因此它是不能产生teiddle2这样的优化版本的。

第二个妨碍优化的因素是函数调用:

int f();

int func1(){ return f() + f() +f() + f(); }

int func2(){ reutrn 4*f();}

最初看上去func1 和func2 计算的过程是相同的,当以func1为源时,很容易想象func2的优化

不过,考虑一下代码

int count = 0;

int f() {

return count++;

}

显然,这个函数是有严重的副作用的,大多数编译器不会试图判断一个函数有没有副作用,但是编译器会假设最糟糕的情况,并保持所有的函数调用不变。

所以,大多数情况下,我们不能太依赖于编译器的优化能力,需要自己写出能适应编译器高效优化的代码。

看下面的例子

typedef int data_t ;

 #define OP  +

#define OP *  //这里把它理解为两个不同的操作,待会是介绍

typedef struct{

long int len;

data_t *data;

}vec_rec, *vec_ptr;


vec_ptr new_vec(long int len)

{

vec_ptr = result =  (vec_ptr) malloc(sizeof(struct vec_rec));

if(!result) { return NULL;}

result->len = len;

if(len >0)

{

data_t *data = (data_t*) calloc (len, sizeof(struct vec_rec));

if(!data){ free((void *)result); return NULL;}

result->data = data;

}

else

{ result->data = NULL; }

return result;

}


int get_vec_element(vec_ptr v , long int index, data_t *dest)

{

if( index<0 || index >= v->len)

return 0;

*dest = v->data[index];

return 1;

}


long int vec_length(cec_ptr v)

{ return v->len; }


void combine1(vec_ptr v , data_t *dest)

{

long int i;

*dest = INDEX;

for(int i=0; i<vec_length(v); i++)

{

data_t val;

get_vec_element(v, i , &val);

*dest = *dest OP val;

}

}

在实际程序中, 数据类型data_t 被声明为int、float 和double

对于程序的性能,我们使用度量标准 每元素的周期数作为性能的衡量(数字越小  性能越好)

对于上面的程序

函数 方法 整数 (+ *)

combine1              抽象的未优化 29.02 29.21

combine1 抽象的-O1  12.00   12.00

从上面可以看到,使用优化的级别1,程序员不需要做任何工作,就可以把系能提高一倍,以后优化我们暂且跟编译器优化级别1 来做比较


消除循环的低效率

可以观察到,combine1 调用函数vec_length作为for循环的测试条件,不仅每一次都会对测试条件求值,而且我们发现这个值是不会变化的,改成下面的代码

void combine2(vec_ptr v , data_t *dest)

{

long int i;

*dest = INDEX;

long int length = vec_length(v);

for(int i=0; i< length; i++)

{

data_t val;

get_vec_element(v, i , &val);

*dest = *dest OP val;

}

}

函数 方法 整数 (+ *)

combine1 抽象的-O1  12.00   12.00

combine2              抽象的未优化 8.03 8.09

这个优化的效果是显而易见的,超过了编译器的第一级别的优化,这个优化是一类常见的优化的一个例子,成为代码移动,这列优化包括识别要执行多次但是计算结果不会改变的计算。


减少过程调用

像我们看到的那样,过程调用会带来相当大的开销,而且妨碍大多数形式的程序优化,从combine2中来看,每次循环都会调用get_vec_element来获取下一个向量的元素。


data_t *get_vec_start(vec_ptr)

{ return v->data; }


void combine3(vec_ptr v , data_t *dest)

{

long int i;

*dest = INDEX;

long int length = vec_length(v);

data_t *data = get_vec_start(v);


for(int i=0; i< length; i++)

{

data_t val;

*dest = *dest OP data[i];

}

}

这样做会可能会严重损害程序的模块性,因为这么做没有做 i 的界限判断,这个问题是很严重的,而且看效率

函数 方法 整数 (+ *)

combine2              抽象的未优化 8.03 8.09

combine3 直接访问数组 6.01   8.01

我们得到的性能的提升出乎意料的普通, 只提高了整数的性能


消除不必要的存储器引用

在循环中,有这样的代码:

*dest = *dest OP data[i];

汇编代码可以清晰的看到 取了两次寄存器中的值,而且循环还在继续,所以改!!!

void combine4(vec_ptr v , data_t *dest)

{

long int i;

*dest = INDEX;

long int length = vec_length(v);

data_t *data = get_vec_start(v);

data_t acc  = INDEX;

for(int i=0; i< length; i++)

{

data_t val;

acc = acc OP data[i];

}

*dest   =acc;

}

看性能

函数 方法 整数 (+ *)

combine3 直接访问数组 6.01   8.01

combine4            累积在临时变量中 2.00 3.00

可以看到  性能的提升是显著的


理解现代处理器

现在处理器都是多核并且可以并行处理


void combine5(vec_ptr v , data_t *dest)

{

long int i;

*dest = INDEX;

long int length = vec_length(v);

data_t *data = get_vec_start(v);

data_t acc  = INDEX;

for(int i=0; i< length; i+=2)

{

acc = (acc OP data[i] ) OP data[i];

}


for(; i<length; i++)

{

acc = acc OP data[i];

}

*dest   =acc;

}

这里的i =2 表示展开两次,


函数 方法 整数 (+ *)

combine3 直接访问数组 6.01   8.01

combine4            累积在临时变量中 2.00 3.00

combine5            展开2次 2.00. 1.50

    展开3次(i=3) 1.00 1.00

可以看到 当i=3时  性能几乎到达极限


看处理器在一个的特点:处理器的分支预测

int  minmax1(int a[], int b[], int n)

{

int i ;

for(i=0; i< n; i++)

{

if(a[i] > b[i])

{

int t = a[i];

a[i] = b[i];

b[i] = t;

}

}

}

对于这个函数的CPE值大约14.50  对于可预测的数据,CPE的值为3.00~4.00,很明显是高于预测错误惩罚的痕迹。

void minmax2(int a[], int b[], int n)

{

int i ;

for( i =0; i<n; i++)

{

int min = a[i]>b[i]?a[i]"b[i];

int max = a[i] <b[i]?a[i]:b[i];

a[i] = min;

b[i] = max;

}

}

对于这个函数的测试,无论数据是任意的,还是可以预测的,CPE的值都大约是5.0.

不是所有的条件行为都是可以预测的,但是一些条件还是可以通过程序来实现的,而且效果会很好。


总结一下, 提升程序性能的一些因素:

高级设计、消除连续函数调用、消除不必要的存储器调用、展开循环、用功能的风格重写代码


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值