懒检测开发
在if(a>10 && b=4)这样的语句中,确保AND表达式的第一部分最可能较快的给出结果(或者最早、最快计算),这样第二部分便有可能不需要执行。
用switch()函数替代if...else...
对于涉及if…else…else…这样的多条件判断,例如:
if( val == 1)
dostuff1();else if (val == 2)
dostuff2();else if (val == 3)
ostuff3();
使用switch可能更快:
switch( val )
{ case 1: dostuff1(); break; case 2: dostuff2(); break; case 3: dostuff3(); break;
}
在if()语句中,如果最后一条语句命中,之前的条件都需要被测试执行一次。
Switch允许我们不做额外的测试。
如果必须使用if…else…语句,将最可能执行的放在最前面。
二分中断
使用二分方式中断代码而不是让代码堆成一列,不要像下面这样做:
if(a==1) {
} else if(a==2) {
} else if(a==3) {
} else if(a==4) {
} else if(a==5) {
} else if(a==6) {
} else if(a==7) {
} else if(a==8)
{
}
使用下面的二分方式替代它,如下:
if(a<=4</=) { if(a==1) {
} else if(a==2) {
} else if(a==3) {
} else if(a==4) {
}
}else{ if(a==5) {
} else if(a==6) {
} else if(a==7) {
} else if(a==8) {
}
}
或者如下:
if(a<=4</=)
{ if(a<=2)
{ if(a==1)
{ /* a is 1 */
} else
{ /* a must be 2 */
}
} else
{ if(a==3)
{ /* a is 3 */
} else
{ /* a must be 4 */
}
}
}else{ if(a<=6)
{ if(a==5)
{ /* a is 5 */
} else
{ /* a must be 6 */
}
} else
{ if(a==7)
{ /* a is 7 */
} else
{ /* a must be 8 */
}
}
}</=</=
比较如下两种case语句:
慢而低效的代码 | 快而高效的代码 |
---|---|
c=getch();switch(c){ case 'A': { do something; break; } case 'H': { do something; break; } case 'Z': { do something; break; }} | c=getch();switch(c){ case 0: { do something; break; } case 1: { do something; break; } case 2: { do something; break; }} |
switch语句vs查找表
Switch的应用场景如下:
如果case标签很多,在switch的前两个使用场景中,使用查找表可以更高效的完成。
例如下面的两种转换字符串的方式:
char * Condition_String1(int condition) { switch(condition) { case 0: return "EQ"; case 1: return "NE"; case 2: return "CS"; case 3: return "CC"; case 4: return "MI"; case 5: return "PL"; case 6: return "VS"; case 7: return "VC"; case 8: return "HI"; case 9: return "LS"; case 10: return "GE"; case 11: return "LT"; case 12: return "GT"; case 13: return "LE"; case 14: return ""; default: return 0; }}char * Condition_String2(int condition) { if ((unsigned) condition >= 15) return 0; return "EQ\0NE\0CS\0CC\0MI\0PL\0VS\0VC\0HI\0LS\0GE\0LT\0GT\0LE\0\0" + 3 * condition;
}
第一个程序需要240 bytes,而第二个仅仅需要72 bytes。
循环
循环是大多数程序中常用的结构;
程序执行的大部分时间发生在循环中,因此十分值得在循环执行时间上下一番功夫。
循环终止
如果不加注意,循环终止条件的编写会导致额外的负担。
我们应该使用计数到零的循环和简单的循环终止条件。
简单的终止条件消耗更少的时间。
看下面计算n!的两个程序。第一个实现使用递增的循环,第二个实现使用递减循环。
int fact1_func (int n){ int i, fact = 1; for (i = 1; i <= n;="" i++)="" ="" fact="" *="i;" (fact);
}int fact2_func(int n){ int i, fact = 1; for (i = n; i != 0; i--)
fact *= i; return (fact);
}
第二个程序的fact2_func执行效率高于第一个。
更快的for()循环
这是一个简单而高效的概念。通常,我们编写for循环代码如下:
for( i=0; i<10; i++){ ... }
i从0循环到9。如果我们不介意循环计数的顺序,我们可以这样写:
for( i=10; i--; ) { ... }
这样快的原因是因为它能更快的处理i的值–测试条件是:i是非零的吗?
如果这样,递减i的值。对于上面的代码,处理器需要计算“计算i减去10,其值非负吗?
如果非负,i递增并继续”。简单的循环却有很大的不同。
这样,i从9递减到0,这样的循环执行速度更快。
这里的语法有点奇怪,但确实合法的。循环中的第三条语句是可选的(无限循环可以写为for(;;))。
如下代码拥有同样的效果:
for(i=10; i; i--){}
或者更进一步的:
for(i=10; i!=0; i--){}
这里我们需要记住的是循环必须终止于0(因此,如果在50到80之间循环,这不会起作用),并且循环计数器是递减的。
使用递增循环计数器的代码不享有这种优化。
合并循环
如果一个循环能解决问题坚决不用二个。但如果你需要在循环中做很多工作,那么你并不适合处理器的指令缓存。
这种情况下,两个分开的循环可能会比单个循环执行的更快。下面是一个例子:
//Original Code :for(i=0; i<100; i++){ stuff();}for(i=0; i<100; i++){ morestuff();} | //It would be better to do:for(i=0; i<100; i++){ stuff(); morestuff();} |
---|
函数循环
调用函数时总是会有一定的性能消耗。不仅程序指针需要改变,而且使用的变量需要压栈并分配新变量。
为提升程序的性能,在函数这点上有很多可以优化的。
在保持程序代码可读性的同时也需要代码的大小是可控的。
如果在循环中一个函数经常被调用,那么就将循环纳入到函数中,这样可以减少重复的函数调用。代码如下:
for(i=0 ; i<100 ; i++)
{ func(t,i);
}
-
-
-
void func(int w,d){
lots of stuff.
}
应改为:
func(t);
-
-
-
void func(w){ for(i=0 ; i<100 ; i++)
{ //lots of stuff.
}
}
循环展开
简单的循环可以展开以获取更好的性能,但需要付出代码体积增加的代价。
循环展开后,循环计数应该越来越小从而执行更少的代码分支。
如果循环迭代次数只有几次,那么可以完全展开循环,以便消除循坏带来的负担,这会带来很大的不同。
循环展开可以带非常可观的节省性能,原因是代码不用每次循环需要检查和增加i的值。例如:
for(i=0; i<3; i++){ something(i);}//is less efficient than | something(0);something(1);something(2); |
---|
编译器通常会像上面那样展开简单的,迭代次数固定的循环。但是像下面的代码:
for(i=0;i< limit;i++) { ... }
下面的代码(Example 1)明显比使用循环的方式写的更长,但却更有效率。
block-sie的值设置为8仅仅适用于测试的目的,只要我们重复执行“loop-contents”相同的次数,都会有很好的效果。
在这个例子中,循环条件每8次迭代才会被检查,而不是每次都进行检查。由于不知道迭代的次数,一般不会被展开。
因此,尽可能的展开循环可以让我们获得更好的执行速度。
//Example 1#include#define BLOCKSIZE (8)void main(void){int i = 0;int limit = 33; /* could be anything */int blocklimit;/* The limit may not be divisible by BLOCKSIZE,
* go as near as we can first, then tidy up.
*/blocklimit = (limit / BLOCKSIZE) * BLOCKSIZE;/* unroll the loop in blocks of 8 */while( i < blocklimit )
{ printf("process(%d)\n", i); printf("process(%d)\n", i+1); printf("process(%d)\n", i+2); printf("process(%d)\n", i+3); printf("process(%d)\n", i+4); printf("process(%d)\n", i+5); printf("process(%d)\n", i+6); printf("process(%d)\n", i+7); /* update the counter */
i += 8;
}/*
* There may be some left to do.
* This could be done as a simple for() loop,
* but a switch is faster (and more interesting)
*/if( i < limit )
{ /* Jump into the case at the place that will allow
* us to finish off the appropriate number of items.
*/
switch( limit - i )
{ case 7 : printf("process(%d)\n", i); i++; case 6 : printf("process(%d)\n", i); i++; case 5 : printf("process(%d)\n", i); i++; case 4 : printf("process(%d)\n", i); i++; case 3 : printf("process(%d)\n", i); i++; case 2 : printf("process(%d)\n", i); i++; case 1 : printf("process(%d)\n", i);
}
}
} * go as near as we can first, then tidy up.
*/blocklimit = (limit / BLOCKSIZE) * BLOCKSIZE;/* unroll the loop in blocks of 8 */while( i < blocklimit ){ printf("process(%d)\n", i); printf("process(%d)\n", i+1); printf("process(%d)\n", i+2); printf("process(%d)\n", i+3); printf("process(%d)\n", i+4); printf("process(%d)\n", i+5); printf("process(%d)\n", i+6); printf("process(%d)\n", i+7); /* update the counter */ i += 8;
}/*
* There may be some left to do.
* This could be done as a simple for() loop,
* but a switch is faster (and more interesting)
*/if( i < limit )
{ /* Jump into the case at the place that will allow
* us to finish off the appropriate number of items.
*/
switch( limit - i )
{ case 7 : printf("process(%d)\n", i); i++; case 6 : printf("process(%d)\n", i); i++; case 5 : printf("process(%d)\n", i); i++; case 4 : printf("process(%d)\n", i); i++; case 3 : printf("process(%d)\n", i); i++; case 2 : printf("process(%d)\n", i); i++; case 1 : printf("process(%d)\n", i); }
}
}
统计非零位的数量
通过不断的左移,提取并统计最低位,示例程序1高效的检查一个数组中有几个非零位。
示例程序2被循环展开四次,然后通过将四次移位合并成一次来优化代码。
经常展开循环,可以提供很多优化的机会。
//Example - 1int countbit1(uint n){ int bits = 0; while (n != 0)
{ if (n & 1) bits++;
n >>= 1;
} return bits;
}//Example - 2int countbit2(uint n){ int bits = 0; while (n != 0)
{ if (n & 1) bits++; if (n & 2) bits++; if (n & 4) bits++; if (n & 8) bits++;
n >>= 4;
} return bits;
}
尽早的断开循环
通常,循环并不需要全部都执行。
例如,如果我们在从数组中查找一个特殊的值,一经找到,我们应该尽可能早的断开循环。
例如:如下循环从10000个整数中查找是否存在-99。
found = FALSE;for(i=0;i<10000;i++)
{ if( list[i] == -99 )
{
found = TRUE;
}
}if( found ) printf("Yes, there is a -99. Hooray!\n");
上面的代码可以正常工作,但是需要循环全部执行完毕,而不论是否我们已经查找到。
更好的方法是一旦找到我们查找的数字就终止继续查询。
found = FALSE;for(i=0; i<10000; i++)
{ if( list[i] == -99 )
{
found = TRUE; break;
}
}if( found ) printf("Yes, there is a -99. Hooray!\n");
假如待查数据位于第23个位置上,程序便会执行23次,从而节省9977次循环。
函数设计
设计小而简单的函数是个很好的习惯。这允许寄存器可以执行一些诸如寄存器变量申请的优化,是非常高效的。
函数调用的性能消耗
函数调用对于处理器的性能消耗是很小的,只占有函数执行工作中性能消耗的一小部分。
参数传入函数变量寄存器中有一定的限制。
这些参数必须是整型兼容的(char,shorts,ints和floats都占用一个字)或者小于四个字大小(包括占用2个字的doubles和long longs)。
如果参数限制个数为4,那么第五个和之后的字就会存储在栈上。
这便在调用函数是需要从栈上加载参数从而增加存储和读取的消耗。
看下面的代码:
int f1(int a, int b, int c, int d) { return a + b + c + d;
}int g1(void) { return f1(1, 2, 3, 4);
}int f2(int a, int b, int c, int d, int e, int f) { return a + b + c + d + e + f;
}ing g2(void) { return f2(1, 2, 3, 4, 5, 6);
}
函数g2中的第五个和第六个参数存储于栈上并在函数f2中进行加载,会多消耗2个参数的存储。
减少函数参数传递消耗
减少函数参数传递消耗的方法有:
叶子函数
不调用任何函数的函数称之为叶子函数。在以下应用中,近一半的函数调用是调用叶子函数。
由于不需要执行寄存器变量的存储和读取,叶子函数在任何平台都很高效。
寄存器变量读取的性能消耗,相比于使用四五个寄存器变量的叶子函数所做的工作带来的系能消耗是非常小的。
所以尽可能的将经常调用的函数写成叶子函数。函数调用的次数可以通过一些工具检查。
下面是一些将一个函数编译为叶子函数的方法:
内联函数
内联函数禁用所有的编译选项。
使用__inline修饰函数导致函数在调用处直接替换为函数体。
这样代码调用函数更快,但增加代码的大小,特别在函数本身比较大而且经常调用的情况下。
__inline int square(int x) { return x * x;
}#include double length(int x, int y){ return sqrt(square(x) + square(y));
}
使用内联函数的好处如下:
内联函数的缺陷是如果调用的地方很多,代码的体积会变得很大。
这主要取决于函数本身的大小和调用的次数。
仅对重要的函数使用inline是明智的。
如果使用得当,内联函数甚至可以减少代码的体积:
函数调用会产生一些计算机指令,但是使用内联的优化版本可能产生更少的计算机指令。
使用查找表
函数通常可以设计成查找表,这样可以显著提升性能。查找表的精确度比通常的计算低,但对于一般的程序并没什么差异。
许多信号处理程序(例如,调制解调器解调软件)使用很多非常消耗计算性能的sin和cos函数。
对于实时系统,精确性不是特别重要,sin、cos查找表可能更合适。
当使用查找表时,尽可能将相似的操作放入查找表,这样比使用多个查找表更快,更能节省存储空间。
浮点运算
尽管浮点运算对于所有的处理器都很耗时,但对于实现信号处理软件时我们仍然需要使用。
在编写浮点操作程序时,记住如下几点:
然而,浮点运算的表现可能不能满足特定软件对性能的需求。
这种情况下,最好的办法或许是使用定点算数运算。
当值的范围足够小,定点算数操作比浮点运算更精确、更快速。
其他技巧
通常,可以使用空间换时间。
如果你能缓存经常用的数据而不是重新计算,这便能更快的访问。
比如sine和cosine查找表,或者伪随机数。
最后,最重要的是将编译器优化选项打开!
看上去很显而易见,但却经常在产品推出时被忘记。
编译器能够在更底层上对代码进行优化,并针对目标处理器执行特定的优化处理。