C语⾔代码优化⽅案(深度好⽂)
转⾃公号:嵌⼊式云IOT技术圈
1、选择合适的算法和数据结构
选择⼀种合适的数据结构很重要,如果在⼀堆随机存放的数中使⽤了⼤量的插⼊和删除指令,那使⽤链表要快得多。数组与指针语句具有⼗分密切的关系,⼀般来说,指针⽐较灵活简洁,⽽数组则⽐较直观,容易理解。对于⼤部分的编译器,使⽤指针⽐使⽤数组⽣成的代码更短,执⾏效率更⾼。
在许多种情况下,可以⽤指针运算代替数组索引,这样做常常能产⽣⼜快⼜短的代码。与数组索引相⽐,指针⼀般能使代码速度更快,占⽤空间更少。使⽤多维数组时差异更明显。下⾯的代码作⽤是相同的,但是效率不⼀样。
数组索引指针运算
For(;;){              p=array
A=array[t++];          for(;;){
a=*(p++);
。。。。。。。。。。。。。。。
}                      }
指针⽅法的优点是,array的地址每次装⼊地址p后,在每次循环中只需对p增量操作。在数组索引⽅法中,每次循环中都必须根据t值求数组下标的复杂运算。
2、使⽤尽量⼩的数据类型
能够使⽤字符型(char)定义的变量,就不要使⽤整型(int)变量来定义;能够使⽤整型变量定义的变量就不要⽤长整型(long int),能不使⽤浮点型(float)变量就不要使⽤浮点型变量。当然,在定义变量后不要超过变量的作⽤范围,如果超过变量的范围赋值,C编译器并不报错,但程序运⾏结果却错了,⽽且这样的错误很难发现。
在ICCAVR中,可以在Options中设定使⽤printf参数,尽量使⽤基本型参数(%c、%d、%x、%X、%u和%s格式说明符),少⽤长整型参数(%ld、%lu、%lx和%lX格式说明符),⾄于浮点型的参数(%f)则尽量不要使⽤,其它C编译器也⼀样。在其它条件不变的情况下,使⽤%f 参数,会使⽣成的代码的数量增加很多,执⾏速度降低。
3、减少运算的强度
(1)、查表(游戏程序员必修课)
⼀个聪明的游戏⼤虾,基本上不会在⾃⼰的主循环⾥搞什么运算⼯作,绝对是先计算好了,再到循环⾥查表。看下⾯的例⼦:
旧代码:
long factorial(int i)
{
if (i == 0)
return 1;
else
return i * factorial(i - 1);
}
新代码:
static long factorial_table[] = {1, 1, 2, 6, 24, 120, 720  /* etc */ };
long factorial(int i)
{
return factorial_table[i];
}
如果表很⼤,不好写,就写⼀个init函数,在循环外临时⽣成表格。
(2)求余运算
a=a%8;
可以改为:
a=a&7;
说明:位操作只需⼀个指令周期即可完成,⽽⼤部分的C编译器的“%”运算均是调⽤⼦程序来完成,代码长、执⾏速度慢。通常,只要求是求2n⽅的余数,均可使⽤位操作的⽅法来代替。
(3)平⽅运算
a=pow(a, 2.0);
可以改为:
a=a*a;
说明:在有内置硬件乘法器的单⽚机中(如51系列),乘法运算⽐求平⽅运算快得多,因为浮点数的求平⽅是通过调⽤⼦程序来实现的,在⾃带硬件乘法器的AVR单⽚机中,如ATMega163中,乘法运算只需2个时钟周期就可以完成。既使是在没有内置硬件乘法器的AVR单⽚机中,乘法运算的⼦程序⽐平⽅运算的⼦程序代码短,执⾏速度快。
如果是求3次⽅,如:
a=pow(a,3.0);
更改为:
a=a*a*a;
则效率的改善更明显。
(4)⽤移位实现乘除法运算
a=a*4;
b=b/4;
可以改为:
a=a<<2;
b=b>>2;
通常如果需要乘以或除以2n,都可以⽤移位的⽅法代替。在ICCAVR中,如果乘以2n,都可以⽣成左移的代码,⽽乘以其它的整数或除以任何数,均调⽤乘除法⼦程序。⽤移位的⽅法得到代码⽐调⽤乘除法⼦程序⽣成的代码效率⾼。实际上,只要是乘以或除以⼀个整数,均可以⽤移位的⽅法得到结果,如:
a=a*9
可以改为:
a=(a<<3)+a
采⽤运算量更⼩的表达式替换原来的表达式,下⾯是⼀个经典例⼦:
旧代码:
x = w % 8;
y = pow(x, 2.0);
z = y * 33;
for (i = 0;i < MAX;i++)
{
h = 14 * i;
printf("%d", h);
}
新代码:
x = w & 7;                /* 位操作⽐求余运算快*/
y = x * x;                /* 乘法⽐平⽅运算快*/
z = (y << 5) + y;          /* 位移乘法⽐乘法快 */
for (i = h = 0; i < MAX; i++)
{
h += 14;              /* 加法⽐乘法快 */
printf("%d",h);
}
(5)避免不必要的整数除法
整数除法是整数运算中最慢的,所以应该尽可能避免。⼀种可能减少整数除法的地⽅是连除,这⾥除法可以由乘法代替。这个替换的副作⽤是有可能在算乘积时会溢出,所以只能在⼀定范围的除法中使⽤。
不好的代码:
int i, j, k, m;
m = i / j / k;
推荐的代码:
int i, j, k, m;
m = i / (j * k);
(6)使⽤增量和减量操作符
在使⽤到加⼀和减⼀操作时尽量使⽤增量和减量操作符,因为增量符语句⽐赋值语句更快,原因在于对⼤多数CPU来说,对内存字的增、减量操作不必明显地使⽤取内存和写内存的指令,⽐如下⾯这条语句:
x=x+1;
模仿⼤多数微机汇编语⾔为例,产⽣的代码类似于:
move A,x      ;把x从内存取出存⼊累加器A
add A,1      ;累加器A加1
store x        ;把新值存回x
如果使⽤增量操作符,⽣成的代码如下:
incr x          ;x加1
显然,不⽤取指令和存指令,增、减量操作执⾏的速度加快,同时长度也缩短了。
(7)使⽤复合赋值表达式
复合赋值表达式(如a-=1及a+=1等)都能够⽣成⾼质量的程序代码。
(8)提取公共的⼦表达式
在某些情况下,C++编译器不能从浮点表达式中提出公共的⼦表达式,因为这意味着相当于对表达式重新排序。需要特别指出的是,编译器在提取公共⼦表达式前不能按照代数的等价关系重新安排表达式。这时,程序员要⼿动地提出公共的⼦表达式(在VC.NET⾥有⼀项“全局优化”选项可以完成此⼯作,但效果就不得⽽知了)。
不好的代码:
float a, b, c, d, e, f;
。。。
e = b * c / d;
f = b / d * a;
推荐的代码:
float a, b, c, d, e, f;
。。。
const float t(b / d);
e = c * t;
f = a * t;
不好的代码:
float a, b, c, e, f;
。。。
e = a / c;
f = b / c;
推荐的代码:
float a, b, c, e, f;
。。。
const float t(1.0f / c);
e = a * t;
f = b * t;
4、结构体成员的布局
很多编译器有“使结构体字,双字或四字对齐”的选项。但是,还是需要改善结构体成员的对齐,有些编译器可能分配给结构体成员空间的顺序与他们声明的不同。但是,有些编译器并不提供这些功能,或者效果不好。所以,要在付出最少代价的情况下实现最好的结构体和结构体成员对齐,建议采取下列⽅法:
(1)按数据类型的长度排序
把结构体的成员按照它们的类型长度排序,声明成员时把长的类型放在短的前⾯。编译器要求把长型数据类型存放在偶数地址边界。在申明⼀个复杂的数据类型 (既有多字节数据⼜有单字节数据) 时,应该⾸先存放多字节数据,然后再存放单字节数据,这样可以避免内存的空洞。编译器⾃动地把结构的实例对齐在内存的偶数边界。
(2)把结构体填充成最长类型长度的整倍数
把结构体填充成最长类型长度的整倍数。照这样,如果结构体的第⼀个成员对齐了,所有整个结构体⾃然也就对齐了。下⾯的例⼦演⽰了如何对结构体成员进⾏重新排序:
不好的代码,普通顺序:
struct
{
char a[5];
long k;
double x;
} baz;
推荐的代码,新的顺序并⼿动填充了⼏个字节:
struct
{
double x;
c语言好的网课long k;
char a[5];
char pad[7];
} baz;
这个规则同样适⽤于类的成员的布局。
(3)按数据类型的长度排序本地变量
当编译器分配给本地变量空间时,它们的顺序和它们在源代码中声明的顺序⼀样,和上⼀条规则⼀样,应该把长的变量放在短的变量前⾯。如果第⼀个变量对齐了,其它变量就会连续的存放,⽽且不⽤填充字节⾃然就会对齐。有些编译器在分配变量时不会⾃动改变变量顺序,有些编译器不能产⽣4字节对齐的栈,所以4字节可能不对齐。下⾯这个例⼦演⽰了本地变量声明的重新排序:
不好的代码,普通顺序

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。