您的位置:首页 > 编程语言 > C语言/C++

C语言常规优化策略--赋值语句优化

2007-11-16 08:28 239 查看

C语言常规优化策略

从理论上讲,程序的优化一般分为局部优化、循环优化和全局优化三个层次。所谓局部优化,重点在于删除程序中的无用赋值,利用语言的特性对基本赋值语句优化,局部优化一般不宜过多采用,但如果程序中总是有一些无效赋值或没有引用的变量,这可能给别人造成幼稚的印象;循环优化和全局优化往往能大幅提升程序效率,因此有关的技术对于高质量的程序设计是至关重要的。
本文讨论C语言程序常规优化策略,其重点在于局部优化和循环优化,包括赋值语句优化、条件语句优化、各类循环优化策略、参数传递、全局变量及宏的使用等内容,其中避免乘、除运算及浮点运算的方法是非常巧妙的。这些方法均为程序员广泛熟知并采用,这里,仅仅将它们收集在一起以备大家参考。
当然,各种优化策略的使用应具备时机,并遵循程序开发的基本准则。例如,对于循环优化,很多成熟的编译器均有十分全面的处理,非特别影响效率的代码段,一般不必考虑,而全局变量的采用往往会带来很多不良的副作用,一般也不宜采用。

1、赋值语句优化

1.1 避免无用赋值

在代码中,若一个变量经赋值后在后面语句的执行过程中不再引用,则这一赋值语句就称为无用赋值。且看下面杜撰的代码段
int DoSmth(int x)
{
int y, z;

if (x>=0)
x=x;
else
x=-x;
y=3;
z=f(x);
return z;
}
其中y=3为无用赋值语句,可以删除,而x=x语句是为了填补条件语句中条件成功分支的空缺,同样是无用的赋值。在这种情况下,可以直接删除该语句,只保留一个分号作为空语句标识,如果为醒目起见,则可用null来代替。下面给出修改后的代码段
int DoSmth(int x)
{
int z;

if (x>=0)
null;
else
x=-x;
z=f(x);
return z;
}
当然程序可以采用更佳的结构以驱除null语句:
int DoSmth(int x)
{
if (x<0)
x=-x;
return f(x);
}
但有时代码中为了保持逻辑上的完整性,或者出于理解代码的原因,有时会出现空语句,建议采用null的写法以警醒自己或其它人。
在C程序中,无效的变量声明应当从程序中删除,当出现无效的变量声明时,编译器一般会用“没有引用的变量”来警告你。
~~~~~~~~~~~~~~

1.2 合并已知量

我们要计算两点之间的距离,相应的点结构及代码的如下:
typedef struct tagPoint
{
double x,y;
} Point;

double Dist(Point P1, Point P2)
{
return sqrt((P1.x-P2.x)* (P1.x-P2.x)+(P1.y-P2.y) (P1.y-P2.y));
}
代码中, P1.x-P2.x,P1.y-P2.y均计算两次,如果我们将一次计算的结果保留下来,就可以减少相应的操作次数
double Dist(Point P1, Point P2)
{
double xDelta= P1.x-P2.x;
double yDelta= P1.y-P2.y;
return sqrt(xDelta*xDelta+yDelta*yDelta);
}
程序设计中还存在一种现象,为了方便,我们通常定义一系列常量,在代码中会反复引用这些常量,例如下面的代码中定义了一个圆周率常量,并在圆周长的计算中出现对它的引用
#define PI 3.1416

double Circum(double r)
{
return 2.0*PI*r;
}
我们可以将常量PI与2.0的计算事先进行合并,以提高Circum函数运算效率
#define PI 3.1416
#define TwoPI 6.2832

double Circum(double r)
{
return TwoPI*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,相应的程序为:
void PlotPixel(int x, int y, int color)
{
*(VideoBuf+(long)640*y+x)=color;
}
VideoBuf为一全局变量,不作为函数的参量来传递。
根据二进制乘法,由640=512+128,可将PlotPixel函数改进为
void PlotPixel(int x, int y, int color)
{
// 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, int n, int Ratio)
{
int i;

for (i=0; i<n; 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, int n, int Ratio)
{
int i;

for (i=0; i<n; 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<x2,直线的斜率在0, 1之间. 通常的程序为:
void PlotLine(int x1, int y1, int x2, int y2, int color)
{
float m,y,b; // m为斜率,b为截距
int x, 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和Van Aken等采用的中点技术。中点技术从理论上来讲与Brensenham的技术是一致的,特别是在实际画线过程中,两者产生相同的结果。
同朴素画线算法一样,我们限制0<m<1,其它情况的画线算法可以对称地推出,m=0,1等特殊情况的处理也非常容易。假定直线左下角顶点为(x1,y1),右上角顶点为(x2,y2),对于过这两点的直线可以表示为:
| x y 1 |
| x1 y1 1 | =0
| x2 y2 1 |
或者
(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<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]<y[p]+3/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)中介绍的中点技术确定,由此得到中点线算法:
void MidpointLine (int x1, int y1, int x2, int y2, int color)
{
int dx=x2-x1;
int dy=y2-y1;
int x, y, F;

x=x1;
y=y1;
PlotPixel(x,y,color);
while (x<x2)
{
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 (int x)
{
return (fixed)x<<8;
}

fixed AssignLong (long x)
{
return (fixed)(x<<8);
}

fixed AssignFloat (float x)
{
return (fixed)(x*256.0F);
}

fixed Assign Double (double x)
{
return (fixed)(x*256.0);
}
由于一个定点数的前24位表示其整数部分,因此对C标准类型数必须乘上256后才能变成定点数。
定点数的加减法与普通加减法类似:
fixed Add (fixed x, fixed y)
{
return x+y;

fixed Sub(fixed x,fixed y)
{
return x-y;
}
与标准类型稍有差异的是定点数乘法:
fixed Mul (fixed x, fixed y)
{
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位原因所在。
定点数及其定点数表示相互转化方式为
fixed trans2fixed (int u, int v) //u,v为定点数x的整数部分和小数部分
{
return ((fixed)u<<8)+v;
}

void transfixed2Real(int *u, int *v, fixed x)
{
*u= (int)(x>>8);
*v=(int)x&255;
}
相应地可以设计打印定点数的程序。
上面对定点数算术的讨论均以函数形式进行,在实际使用中,为提高效率, 可利用定点算术的思想直接进行相应的运算,例如我们要完成两个定点数17.28及9.64的一系列运算,相应的代码为
void dosmth()
{
int Ix=17, Fx=28;
int Iy=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。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: