c语言优化.docx
《c语言优化.docx》由会员分享,可在线阅读,更多相关《c语言优化.docx(36页珍藏版)》请在冰豆网上搜索。
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)刻划,且要求x1voidPlotLine(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,而它们不全是正整数,有兴趣的读者可以一试。
至此,我们给出了三角形测试的三个版本,应该说三个版本都不能令人完全满意。
为了照顾到代码的安全性和效率,判断条件变得越来越复杂了。
下面我们就给出一个更复杂的版本,可是在这两个方面都能令人愉快,从这一例子中得到的经验是:
有时一个简单的