c语言优化.docx

上传人:b****8 文档编号:10944767 上传时间:2023-02-23 格式:DOCX 页数:36 大小:38.71KB
下载 相关 举报
c语言优化.docx_第1页
第1页 / 共36页
c语言优化.docx_第2页
第2页 / 共36页
c语言优化.docx_第3页
第3页 / 共36页
c语言优化.docx_第4页
第4页 / 共36页
c语言优化.docx_第5页
第5页 / 共36页
点击查看更多>>
下载资源
资源描述

c语言优化.docx

《c语言优化.docx》由会员分享,可在线阅读,更多相关《c语言优化.docx(36页珍藏版)》请在冰豆网上搜索。

c语言优化.docx

c语言优化

C语言常规优化策略

从理论上讲,程序的优化一般分为局部优化、循环优化和全局优化三个层次。

所谓局部优化,重点在于删除程序中的无用赋值,利用语言的特性对基本赋值语句优化,局部优化一般不宜过多采用,但如果程序中总是有一些无效赋值或没有引用的变量,这可能给别人造成幼稚的印象;循环优化和全局优化往往能大幅提升程序效率,因此有关的技术对于高质量的程序设计是至关重要的。

本文讨论C语言程序常规优化策略,其重点在于局部优化和循环优化,包括赋值语句优化、条件语句优化、各类循环优化策略、参数传递、全局变量及宏的使用等内容,其中避免乘、除运算及浮点运算的方法是非常巧妙的。

这些方法均为程序员广泛熟知并采用,这里,仅仅将它们收集在一起以备大家参考。

当然,各种优化策略的使用应具备时机,并遵循程序开发的基本准则。

例如,对于循环优化,很多成熟的编译器均有十分全面的处理,非特别影响效率的代码段,一般不必考虑,而全局变量的采用往往会带来很多不良的副作用,一般也不宜采用。

1、赋值语句优化

1.1 避免无用赋值

在代码中,若一个变量经赋值后在后面语句的执行过程中不再引用,则这一赋值语句就称为无用赋值。

且看下面杜撰的代码段

intDoSmth(intx)

{

 inty,z;

 if(x>=0)

  x=x;

 else

  x=-x;

 y=3;

 z=f(x);

 returnz;

}

其中y=3为无用赋值语句,可以删除,而x=x语句是为了填补条件语句中条件成功分支的空缺,同样是无用的赋值。

在这种情况下,可以直接删除该语句,只保留一个分号作为空语句标识,如果为醒目起见,则可用null来代替。

下面给出修改后的代码段

intDoSmth(intx)

{

 intz;

 if(x>=0)

  null;

 else

  x=-x;

 z=f(x);

 returnz;

}

当然程序可以采用更佳的结构以驱除null语句:

intDoSmth(intx)

{

 if(x<0)

  x=-x;

 returnf(x);

}

但有时代码中为了保持逻辑上的完整性,或者出于理解代码的原因,有时会出现空语句,建议采用null的写法以警醒自己或其它人。

在C程序中,无效的变量声明应当从程序中删除,当出现无效的变量声明时,编译器一般会用“没有引用的变量”来警告你。

~~~~~~~~~~~~~~

1.2 合并已知量

我们要计算两点之间的距离,相应的点结构及代码的如下:

typedefstructtagPoint

{

 doublex,y;

} Point;

doubleDist(PointP1,PointP2)

{

 returnsqrt((P1.x-P2.x)*(P1.x-P2.x)+(P1.y-P2.y)(P1.y-P2.y));

}

代码中,P1.x-P2.x,P1.y-P2.y均计算两次,如果我们将一次计算的结果保留下来,就可以减少相应的操作次数

doubleDist(PointP1,PointP2)

{

 doublexDelta=P1.x-P2.x;

 doubleyDelta=P1.y-P2.y;

 returnsqrt(xDelta*xDelta+yDelta*yDelta);

}

程序设计中还存在一种现象,为了方便,我们通常定义一系列常量,在代码中会反复引用这些常量,例如下面的代码中定义了一个圆周率常量,并在圆周长的计算中出现对它的引用

#definePI 3.1416

doubleCircum(doubler)

{

 return2.0*PI*r;

}

我们可以将常量PI与2.0的计算事先进行合并,以提高Circum函数运算效率

#definePI 3.1416

#defineTwoPI  6.2832   

doubleCircum(doubler)

{

 returnTwoPI*r;

}

~~~~~~~~~~~~~

1.3 避免乘法

在C程序中,由于加减运算与位运算一般比乘法快2到10倍,大部分程序员在乘法中出现2的整数次幂(2、4、8、16等)时,往往愿意将乘法操作改造成位操作以提高效率。

以z=8x+y为例,多数C程序员会将其写成如下的代码

z=(x<<3)+y;

其中将x右移3位其效果等同于乘8,例如:

x=19表示成二进制形式为:

  0000000000010011

右移3位变成

  0000000010011000

其值为152。

有时,当乘数不是2的整数幂时,出于需要,我们可以根据乘数的二进制表示,将乘法改变成二进制乘法,进一步用移位和加法操作来代替乘法,例如我们要计算z=5x,由于5=4+1,其中1和4均为2的整数次幂,从而z=5x可以表示成z=4x+x,相应的语句为       

  z=(x<<2)+x;

这一转换通常称为二进制乘法。

二进制乘法在计算机图形图象处理中经常采用,例如,对于640X480的显示屏,一般在计算机内有一块相应的显示缓冲区来保存相应的屏幕元素,我们可以用一个480行,640列的二维数组VideoBuf来指示该缓冲区。

屏幕的显示是通过向缓冲区填写数据(颜色或其索引值)而实现的,假设我们要向x列、y行设置一个值color,相应的程序为:

voidPlotPixel(intx,inty,intcolor)

{

 *(VideoBuf+(long)640*y+x)=color;

}

VideoBuf为一全局变量,不作为函数的参量来传递。

根据二进制乘法,由640=512+128,可将PlotPixel函数改进为

voidPlotPixel(intx,inty,intcolor)

{

 //640y=512y+128y=((y<<2)+y)<<7

 *(VideoBuf+((((long)(y<<2)+y))<<7)+x)=color;

}

有两点值得指出:

(1)二进制乘法会改变程序的可读性,因此,有必要在程序中用注释段说明你的思想。

在改进后的PlotPixel函数中用相应的注释指出了此处二进制乘法的原理。

(2)移位运算比”十”、”一”运简优先级要低,因此,在计算z=8x+y时,切不可写成z=x<<3+y。

这是程序员常犯的一类错误。

~~~~~~~~~~~~~~~~~~

1.4 避免除法

同样,当以2的整数幂作为除数时,可用移位操作来避免除法,例如

z=x/8+y;

就可以改善为

z=(x>>3)+y;

其中x、y、z均为整数。

对于除数不是2的整数幂的情况,没有一种适当的方法将除法改进为二进制除法。

通常的做法有两种:

(1)将x/y转换为x*(1.0/y),一般来说求倒数比除法快;

(2)对除数进行规范化,将其变成2的整数幂,然后进行后续处理。

例如给定两个整型数组u、v,其维数均为n,我们要将u、v对应元素进行调配以生成一个新的数组w,设r为调配比例,调配公式为

 w[i]=ru[i]+(1-r)v[i]

其中r在0、1之间。

在程序实现时,调配比例一般为百分比数值,即用户输入一个百分比Ratio,相应地r=Ratio/100。

下面是两个数组进行调配的程序:

void(int*w,int*u,int*v,intn,intRatio)

{

 int i;

 

 for(i=0;i

  w[i]=(Ratio*u[i]+(100-Ratio)*v[i])/100;

}

为了提高效率,我们可将比值Ratio规范化为0~128这一范围,记R=Ratio*128/100,相应的调配公式为

  w[i]=(R*u[i]+(128-R)*v[i])/128

改进后的程序为:

void(int*w,int*u,int*v,intn,intRatio)

{

 int i;

 

 for(i=0;i

  w[i]=((R*(u[i]-v[i]))>>7)+v[i];

}

为什么不对传入的Ratio参数直接进行限制,将其规范为0~128呢?

这是因为Ratio由用户输入,在用户界面的设计时,参数的意义应适合用户的习惯,在本问题中,让用户输入一个百分比值当然比输入一个0~128之间的数要直观得多。

~~~~~~~~~~~~~

1.5 避免浮点运算

C语言中的浮点型float及双精度浮点型double运算比短整型,整型、长整型运算要慢得多,因此避免浮点运算就非常有必要。

在上面避免除法运算的函数调配例子中,已经使用到了避免浮点运算的策略,百分比在通常情况下只能用一个浮点数表示,而我们将其表示为整数Ratio与100之比。

1.5.1 中点线算法

避免浮点运算的一个经典例子为Bresenhem的画线算法,直线用两点(x1,y1),(x2,y2)刻划,且要求x1

voidPlotLine(intx1,inty1,intx2,inty2,intcolor)

{

 float m,y,b;  //m为斜率,b为截距

 intx,dx,dy;

 dx=x2-x1;

 dy=y2-y1;

 m=(float)dy/(float)dx;

 b=(float)(x2*y1-x1*y2)/(float)dx;

 for(x=x1;x<=x2;x++)

 {

  y=m*x+b;

  PlotPixel(x,(int)(y+0.5),color);

 }

}

朴素算法存在两个缺点:

(1)涉及浮点操作,画线速度有限;

(2)(int)(y+0.5)为取与y最接近的整数,这将导致精度和时间的损失。

Brensenham提出了一种避开浮点运算的画线算法,但Brensenham的思想讨论起来比较麻烦,这里我们采用Pitteway和VanAken等采用的中点技术。

中点技术从理论上来讲与Brensenham的技术是一致的,特别是在实际画线过程中,两者产生相同的结果。

同朴素画线算法一样,我们限制0

假定直线左下角顶点为(x1,y1),右上角顶点为(x2,y2),对于过这两点的直线可以表示为:

   |x y 1|

   |x1y11|=0

   |x2y21|

或者

  (y2-y1)x-(x2-x1)y+(x2y1-x1y2)=0

  F(x,y)=(y2-y1)x-(x2-x1)y+(x2y1-x1y2)

直线F(x,y)=0将平面划分成三个部分

              P2

  F(x,y)<0  *

           *

     F(x,y)=0 F(x,y)>0

       * 

 P1 *

中点线算法的基本思想为:

(1)先画P1点;

(2)判断P1点右边或右上方的两个点E及NE中哪一个离直线更近,判断方法是确定NE和E的中点M在直线P1P2所确定的三个区域的哪一个内,这时有三种情况:

(i)F(M)=0,E、NE与直线距离相等,因此可任选一个作为直线上一点,通常我们取右边的点E;

(ii)F(M)<0,说明M在直线的上方,E离直线更近,选E作为下一点;

       *P2

     *NE

                                       | 

     |M

                                       | 

                         |

    P1*-----*E

(iii)F(M)>0,说明M在直线下方,NE离直线更近,选NE作为下一顶点。

其中F(M)=(x1+1)dy-(y1+0.5)dx+(x1y2-x2y1),为避开0.5,可用2F(M)作为判别条件。

(3)更一般,在第p步我们得到直线上一点P(x[p],y[p])后,下一步(x[p+1],y[p+1])怎么选取呢?

候选的点只能是P的右点E或右上方的点NE,原因在于直线通过x=x[p]时,交点(x[p],y’[p]满足

    y[p]-1/2

而x[p+1]=x[p]+1,直线与x=x[p]+1的交点确定了y[p+1]的范围为

    y[p]-1/2

因为

    y’[p+1]=mx[p+1]+b=(mx[p]+b)+m=y’[p]+m

即y[p+1]只能取y[p]或y[p]+1.

至于具体取E或NE,可由

(2)中介绍的中点技术确定,由此得到中点线算法:

voidMidpointLine(intx1,inty1,intx2,inty2,intcolor)   

{

 intdx=x2-x1;

 intdy=y2-y1;

 intx,y,F;

 x=x1;

 y=y1;

 PlotPixel(x,y,color);

 while(x

 {

  F=2*(x+1)*dy-(2*y+1)*dx+2*(x1*y2-x2*y1);

  x++;

  if(F<=0)

     y++;

  PlotPixel(x,y,color);

 }

}

需要指出的是:

朴素画线算法及中点线算法均可以进一步改进,相应的技术可参考循环优化部分。

1.5.2 定点数算术

若我们在计算中需要用到实数运算,但又不太关心实数的精度时,可以采用一种称之为定点数的算术。

预先声明:

C语言中没有定点数,它是我们人为造出来的一类数。

一般地,我们用一个长整数来表示一个定点数,其前24位表示整数部分,后8位表示小数部分,根据问题的需要可设计相应的定点数。

这样,一个长整数类型的数就被重命名为定点数:

typedef long fixed;

下面讨论定点数的赋值及算术运算,我们可以将一个整型数、浮点数或一个双精度型数赋给一个定点型变量,相应的形式为

fixed AssignInt (intx)

{

 return(fixed)x<<8;

}

fixed AssignLong(longx)

{

  return(fixed)(x<<8);

}

fixedAssignFloat(floatx)

{

  return(fixed)(x*256.0F);

}

fixedAssignDouble(double x)

{

  return (fixed)(x*256.0);

}

由于一个定点数的前24位表示其整数部分,因此对C标准类型数必须乘上256后才能变成定点数。

定点数的加减法与普通加减法类似:

fixed Add(fixedx,fixedy)

{

 returnx+y;

fixedSub(fixedx,fixedy)

{

 returnx-y;

}

与标准类型稍有差异的是定点数乘法:

fixedMul(fixedx,fixedy)

{

 return((x*y)>>8);

}

右移8位(除256)的理由在于:

设任一定点数x的整数部分为Ix,小数部分为Fx,则

    x=256(Ix+Fx/256)

两个定点数乘法是为了模拟它们所对应的实数乘法,设两个实数为x’,y’,它们对应的定点数为x,y

    x’y’=(Ix+Fx/256)(Iy+Fy/256)

      =IxIy+(IxFy+FxIy)/256+FxFy/(256*256)

而x’y’的定点数表示为

    256IxIy+IxFy+FxIy+FxFy/256

可以看到xy与x’y’的定点数表示之间相差256倍,这就是移8位原因所在。

定点数及其定点数表示相互转化方式为

fixedtrans2fixed(intu,intv) //u,v为定点数x的整数部分和小数部分

{

 return((fixed)u<<8)+v;

}

voidtransfixed2Real(int*u,int*v,fixedx)

{

 *u=(int)(x>>8);

 *v=(int)x&255;

}

相应地可以设计打印定点数的程序。

上面对定点数算术的讨论均以函数形式进行,在实际使用中,为提高效率,可利用定点算术的思想直接进行相应的运算,例如我们要完成两个定点数17.28及9.64的一系列运算,相应的代码为

voiddosmth()

{

 intIx=17,Fx=28; 

 intIy=9,Fy=64;

 fixed x,y,z1,z2;

 x=((fixed)Ix<<8)+Fx;

 y=((fixed)Iy<<8)+Fy;

 z1=x+y;    //z1为17.28+9.64的定点表示

 z2=(x*y)>>8;   //z2为17.8*9.64的定点表示

 z=z1-z2;    //z为17.28+9.64-17.28*9.64的定点表示

 printf(“x=%d.%d”,(int)(x>>8),(int)x&255);

 printf(“y=%d.%d”,(int)(y>>8),(int)y&255);

 printf(“x+y-xy=%d.%d”,(int)(z>>8),(int)z&255);

}

关于定点数算术还有两问题需要讨论:

(1)定点数的精度较低,只能精确表示十进制2位小数。

(2)为了保证定点数运算不发生溢出,必须要求每一个参予运算的数不能太大,这就引出了定点数最大能表示多大的数的问题。

理论上来说,一个定点数最大能表示(2^31-1)/256,但由于加减及乘法运算可能导致溢出,因此最大的数应控制在一定范围内,例如,若x为一定点数,则要保证x*x<=(2^31-1)/256,必须x<=28938。

2 条件语句优化

2.1 多分枝条件语句优化

多分枝条件语句一般采用switch语句,这样的程序无论从清晰性和效率上都比原来的程序要好。

例如下面的函数采用三种函数形式分别计算x在Alpha,Beta和Gamma处的值,通常的写法为:

int f(intx)

{

 int y;

 if(x==Alpha)

  y=f1(x);

 else

  if(x==Beta)

   y=f2(x);

  else

   if(x==Gamma)

    y=f3(x);

 returny;

}

采用switch语句可写成:

int f(intx)

{

 int y;

 switch(x)

 {

 caseAlpha:

 

  y=f1(x); 

  break;

 caseBeta:

  y=f2(x); 

  break;

 caseGamma:

 

  y=f3(x); 

  break;

 }

 returny;

}

值得指出的是,在多分支条件语句不能改造成语句时,我们一般采用紧缩的写法,例如我们要计算下面的分段函数值

 f(x)= f1(x) x<=Alpha

  f2(x) Alpha

  f3(x) Beta

  f4(x) Gamma

紧缩的写法为

int f(intx)

{

 int y;

 if(x<=Alpha)

    y=f1(x);

 elseif(x<=Beta)

    y=f2(x);

 elseif(x<=Gamma)

    y=f3(x);

 else

    y=f4(x);

 returny;

}

2.2 复杂条件分析与条件表达式化简

有时通过对复杂的条件表达式进行分析,将复杂条件表达式简化,可以提高代码的效率,我们通过几个例子来说明复杂条件分析化简的方法。

2.2.1 三角形测试问题

给定三个正整数a、b、c,问它们是否构成一个三角形的三条边?

三个整数a、b、c构成三角形三边的充要条件为:

   a+b>c     

(1)

   b+c>a     

(2)

   c+a>b     (3)

一般来说,只要有了上述三个条件,我们不必再强调a、b、c>0,这些条件已经蕴含在上述三个条件中,如由

(1)、(3)左、右两边分别相加化简后就可以得到a>0。

由此得到三角形测试问题求解的第一个程序:

typedef int BOOL;

#defineTRUE 1

#defineFALSE 0

BoolIsTriangle(inta,intb,intc)

{

 if(a+b>c&&b+c>a&&c+a>b)

  returnTRUE;

 returnFALSE;

}

上述程序没有考虑计算溢出问题,因为a+b,b+c和c+a均可能超过一个整数的范围。

为避免计算溢出,可采用长整数算术运算和比较运算,但必须考虑到机器中的长整数必须比整数的表示范围要大才行。

例如在Windows95中采用VisualC++编程时,整数和长整数均为32位,如果是这样,这种改造就没意义了。

相应代码为:

BoolIsTriangle(inta,intb,intc)

{

 if((long)a+(long)b>(long)c&&

           (long)b+(long)c>(long)a&&

           (long)c+(long)a>(long)b)

  returnTRUE;

 returnFALSE;

}

这一代码的使用是绝对安全的,唯一的问题是采用长整数算术与比较将会降低效率。

如果我们要绕过长整数算术,同时又要保证代码的安全,可以对判断条件进行变换,例如,虽然三个加法会导致溢出,但只要a,b,c>0,c-b,b-a,a-c却不会产生溢出,因此可将条件

(1)、

(2)、(3)改变为相应的代码为:

BoolIsTriangle(inta,intb,intc)

{

 if(a>0&&b>0&&c>0&&

  a>c-b&&b>a-c&&c>b-a)

  returnTRUE;

 returnFALSE;

}

代码中显示地加入了a,b,c>0测试,而在逻辑上由条件(4)、(5)、(6)是能蕴含这一结论的,这是否冗余的操作呢?

只要注意到计算机内的算术操作受限于表示精度,同一般意义下的算术操作存在根本的差别,我们就知道这三个条件不能省略,否则可能导致减法的溢出,如a=b=-32000,整数为16位字长表示时就能明白。

但在这一具体例子中,能否构造出满足条件(4)、(5)、(6)的整数a、b、c,而它们不全是正整数,有兴趣的读者可以一试。

至此,我们给出了三角形测试的三个版本,应该说三个版本都不能令人完全满意。

为了照顾到代码的安全性和效率,判断条件变得越来越复杂了。

下面我们就给出一个更复杂的版本,可是在这两个方面都能令人愉快,从这一例子中得到的经验是:

有时一个简单的

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 高等教育 > 军事

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1