超全 | 只有高手才知道的C语言高效编程与代码优化方法(二)

懒检测开发

在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个参数的存储。

减少函数参数传递消耗

减少函数参数传递消耗的方法有:

  • 尽量保证函数使用少于四个参数。这样就不会使用栈来存储参数值;
  • 如果函数需要多于四个的参数,尽量确保使用后面参数的价值高于让其存储于栈所付出的代价;
  • 通过指针传递参数的引用而不是传递参数结构体本身;
  • 将参数放入一个结构体并通过指针传入函数,这样可以减少参数的数量并提高可读性;
  • 尽量少用占用两个字大小的long类型参数。对于需要浮点类型的程序,double也因为占用两个字大小而应尽量少用;
  • 避免函数参数既存在于寄存器又存在于栈中(称之为参数拆分)。现在的编译器对这种情况处理的不够高效:所有的寄存器变量也会放入到栈中;
  • 避免变参。变参函数将参数全部放入栈。

叶子函数

不调用任何函数的函数称之为叶子函数。在以下应用中,近一半的函数调用是调用叶子函数。

由于不需要执行寄存器变量的存储和读取,叶子函数在任何平台都很高效。

寄存器变量读取的性能消耗,相比于使用四五个寄存器变量的叶子函数所做的工作带来的系能消耗是非常小的。

所以尽可能的将经常调用的函数写成叶子函数。函数调用的次数可以通过一些工具检查。

下面是一些将一个函数编译为叶子函数的方法:

  • 避免调用其他函数:包括那些转而调用C库的函数(比如除法或者浮点数操作函数);
  • 对于简短的函数使用__inline修饰()。

内联函数

内联函数禁用所有的编译选项。

使用__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查找表可能更合适。

当使用查找表时,尽可能将相似的操作放入查找表,这样比使用多个查找表更快,更能节省存储空间。

浮点运算

尽管浮点运算对于所有的处理器都很耗时,但对于实现信号处理软件时我们仍然需要使用。

在编写浮点操作程序时,记住如下几点:

  • 浮点除法很慢。浮点除法比加法或者乘法慢两倍。通过使用常量将除法转换为乘法(例如,x=x/3.0可以替换为x=x*(1.0/3.0))。常量的除法在编译期间计算;
  • 使用float代替double。Float类型的变量消耗更好的内存和寄存器,并由于精度低而更加高效。如果精度够用,尽可能使用float;
  • 避免使用先验函数。先验函数,例如sin、exp和log是通过一系列的乘法和加法实现的(使用了精度扩展)。这些操作比通常的乘法至少慢十倍;
  • 简化浮点运算表达式。编译器并不能将应用于整型操作的优化手段应用于浮点操作。 例如,3*(x/3)可以优化为x,而浮点运算就会损失精度。因此,如果知道结果正确,进行必要手工浮点优化是有必要的。

然而,浮点运算的表现可能不能满足特定软件对性能的需求。

这种情况下,最好的办法或许是使用定点算数运算。

当值的范围足够小,定点算数操作比浮点运算更精确、更快速。

其他技巧

通常,可以使用空间换时间。

如果你能缓存经常用的数据而不是重新计算,这便能更快的访问。

比如sine和cosine查找表,或者伪随机数。

  • 尽量不在循环中使用++和–。例如:while(n–){},这有时难于优化;
  • 减少全局变量的使用;
  • 除非像声明为全局变量,使用static修饰变量为文件内访问;
  • 尽可能使用一个字大小的变量(int、long等),使用它们(而不是char,short,double,位域等)机器可能运行的更快;
  • 不使用递归。递归可能优雅而简单,但需要太多的函数调用;
  • 不在循环中使用sqrt开平方函数,计算平方根非常消耗性能;
  • 一维数组比多维数组更快;
  • 编译器可以在一个文件中进行优化-避免将相关的函数拆分到不同的文件中,如果将它们放在一起,编译器可以更好的处理它们(例如可以使用inline);
  • 单精度函数比双精度更快;
  • 浮点乘法运算比浮点除法运算更快-使用val*0.5而不是val/2.0;
  • 加法操作比乘法快-使用val+val+val而不是val*3;
  • put()函数比printf()快,但不灵活;
  • 使用#define宏取代常用的小函数;
  • 二进制/未格式化的文件访问比格式化的文件访问更快,因为程序不需要在人为可读的ASCII和机器可读的二进制之间转化。如果你不需要阅读文件的内容,将它保存为二进制;
  • 如果你的库支持mallopt()函数(用于控制malloc),尽量使用它。MAXFAST的设置,对于调用很多次malloc工作的函数由很大的性能提升。
  • 如果一个结构一秒钟内需要多次创建并销毁,试着设置mallopt选项。

最后,最重要的是将编译器优化选项打开!

看上去很显而易见,但却经常在产品推出时被忘记。

编译器能够在更底层上对代码进行优化,并针对目标处理器执行特定的优化处理。

原文发布于微信公众号 - 老九学堂(xuetang9)

原文发表时间:2019-05-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券