程序设计基石与实践系列之编写高效的C程序与C代码优化

摘要:
您应该使用无符号int而不是int。某些处理器处理整数算术运算的速度比int快。因为处理器可以直接执行整数算术运算。浮点运算依赖于外部浮点处理器或浮点数学库。如果我们已经知道b是一个非负数,并且b*c不超过整数的取值范围,那么在处理小数时(例如,当我们执行一个简单的统计程序时),我们应该更加精确。假设我们可以确定其中一个操作数是无符号的。

原文出处: codeproject:Writing Efficient C and C Code Optimization

虽然对于优化C代码有非常多有效的指导方针,可是对于彻底地了解编译器和你工作的机器依旧无法取代,通常。加快程序的速度也会加大代码量。这些添加的代码也会影响一个程序的复杂度和可读性。这是不可接受的,比方你在一些小型的设备上编程,比如:移动设备、PDA……。这些有着严格的内存限制,于是。在优化的座右铭是:写代码在内存和速度都应该优化。

整型数 / Integers

在我们知道使用的数不可能是负数的时候,应该使用unsigned int取代int。一些处理器处理整数算数运算的时候unsigned int比int快,于是,在一个紧致的循环里面定义一个整型变量,最好这样写代码:

register unsigned int variable_name;
然而。我们不能保证编译器会注意到那个registerkeyword,也有可能,对某种处理器来说。有没有unsigned是一样的。

这两个keyword并非能够在全部的编译器中应用。记住,整形数运算要比浮点数运算快得多,由于处理器能够直接进行整型数运算。浮点数运算须要依赖于外部的浮点数处理器或者浮点数数学库。我们处理小数的时候要精确点些(比方我们在做一个简单的统计程序时),要限制结果不能超过100。要尽可能晚的把它转化成浮点数。

除法和余数 / Division and Remainder

在标准的处理器中。根据分子和分母的不同,一个32位的除法须要20-140个时钟周期来运行完毕,等于一个固定的时间加上每一个位被除的时间。

Time (numerator / denominator) = C0 + C1* log2 (numerator / denominator)
     = C0 + C1 * (log2 (numerator) - log2 (denominator)).

PS: numerator :分子 denominator:分母

ARM处理器须要消耗20+4.3N个时钟周期。这是一个非常费时的操作。要尽可能的避免。

在有些情况下。除法表达式能够用乘法表达是来重写。比方说,(a/b)>c能够写成a>(c*b),条件是我们已经知道b为非负数并且b*c不会超过整型数的取值范围。假设我们能够确定当中的一个操作数为unsigned。那么使用无符号除法将会更好。由于它要比有符号除法快得多。


合并除法运算和取余运算 / Combining division and remainder

在一些情况下。除法运算和取余运算都须要用到。在这样的情况下。编译器会将除法运算和取余运算合并。由于除法运算总是同一时候返回商和余数。假设两个运算都要用到,我们能够将他们写到一起。

typedef unsigned int uint;
uint div32u (uint a) {
     return a / 32;
 }
int div32s (int a){
     return a / 32;
}
这两种除法都会避免调用除法函数,另外,无符号的除法要比有符号的除法使用更少的指令。

有符号的除法要耗费很多其它的时间,由于这样的除法是使终于结果趋向于零的,而移位则是趋向于负无穷。

取模运算的替换 / An alternative for modulo arithmetic

我们一般使用取余运算进行取模,只是。有时候使用 if 语句来重写也是可行的。

考虑以下的两个样例:

uint modulo_func1 (uint count)
{
   return (++count % 60);
}

uint modulo_func2 (uint count)
{
   if (++count >= 60)
  count = 0;
  return (count);
}
第二个样例要比第一个更可取,由于由它产生的代码会更快,注意:这仅仅是在count取值范围在0 – 59之间的时候才行。

可是我们能够使用例如以下的代码(笔者补充)实现等价的功能:

uint modulo_func3 (uint count)
{
    if (++count >= 60)
        count %= 60;
    return (count);
}

使用数组索引 / Using array indices

假设你要根据某个变量的值,设置还有一个变量的取值为特定的字符,你可能会这样做:

switch ( queue ) {
case 0 :   letter = 'W';
   break;
case 1 :   letter = 'S';
   break;
case 2 :   letter = 'U';
   break;
}
或者这样:

if ( queue == 0 )
  letter = 'W';
else if ( queue == 1 )
  letter = 'S';
else
  letter = 'U';
有一个简洁且高速的方式是简单的将变量的取值做成一个字符串索引,比如:

static char *classes="WSU";
letter = classes[queue];


全局变量 / Global variables

全局变量不会被分配在寄存器上。改动全局变量须要通过指针或者调用函数的方式间接进行。

所以编译器不会将全局变量存储在寄存器中,那样会带来额外的、不必要的负担和存储空间。所以在比較关键的循环中,我们要不使用全局变量。
假设一个函数要频繁的使用全局变量。我们能够使用局部变量,作为全局变量的拷贝,这样就能够使用寄存器了。条件是本函数调用的不论什么子函数不使用这些全局变量。

举个样例:

int f(void);
int g(void);
int errs;
void test1(void)
{
  errs += f();
  errs += g();
}

void test2(void)
{
  int localerrs = errs;
  localerrs += f();
  localerrs += g();
  errs = localerrs;
}
能够看到test1()中每次加法都须要读取和存储全局变量errs。而在test2()中,localerrs分配在寄存器上,仅仅须要一条指令。

使用别名 / Using Aliases

考虑以下的样例:

void func1( int *data )
{
    int i;

    for(i=0; i<10; i++)
    {
          anyfunc( *data, i);
    }
}
即使*data从来没有变化,编译器却不知道anyfunc()没有改动它。于是程序每次用到它的时候,都要把它从内存中读出来。可能它仅仅是某些变量的别名。这些变量在程序的其它部分被改动。假设能够确定它不会被改变,我们能够这样写:

void func1( int *data )
{
    int i;
    int localdata;

    localdata = *data;
    for(i=0; i<10; i++)
    {
          anyfunc ( localdata, i);
    }
}
这样会给编译器优化工作很多其它的选择余地。

活跃变量和泄漏 / Live variables and spilling

寄存器的数量在每一个处理器当中都是固定的,所以在程序的某个特定的位置。能够保存在寄存器中的变量的数量是有限制的。

有些编译器支持“生命周期切割”(live-range splitting),也就是说在函数的不同部分。变量能够被分配到不同的寄存器或者内存中。变量的生存范围被定义成:起点是对该变量的一次空间分配,终点是在下次空间分配之前的最后一次使用之间。在这个范围内,变量的值是合法的,是活的。在生存范围之外。变量不再被使用,是死的。它的寄存器能够供其它变量使用。这样,编译器就能够安排很多其它的变量到寄存器当中。
可分配到寄存器的变量须要的寄存器数量等于经过生命范围重叠的变量的数目,假设这个数目超过可用的寄存器的数量,有些变量就必须被临时的存储到内存中。

这样的处理叫做“泄漏(spilling)”。


编译器优先释放最不频繁使用的变量。将释放的代价降到最低。

能够通过以下方式避免变量的“释放”:

  • 限制活跃变量的最大数目:通常能够使用简单小巧的表达式,在函数内部不使用太多的变量。把大的函数切割成更加简单的、更加小巧的多个函数。也可能会有所帮助。
  • 使用keywordregister修饰最常常使用的变量:告诉编译器这个变量将会被常常常使用到。要求编译器使用非常高的优先级将此变量分配到寄存器中。虽然如此。在某些情况下。变量还是可能被泄漏。

变量类型 / Variable Types

C编译器支持主要的变量类型:char、short、int、long(signed、unsigned)、float、double。

为变量定义最恰当的类型。非常重要,由于这样能够降低代码和数据的长度,能够非常显著的提高效率。

局部变量 / Local variables

假设可能,局部变量要避免使用char和short。对于char和short类型。编译器在每次分配空间以后,都要将这样的局部变量的尺寸降低到8位或16位。这对于符号变量来说称为符号扩展。对无符号变量称为无符号扩展。这样的操作是通过将寄存器左移24或16位,然后再有符号(或无符号的)右移相同的位数来实现的,须要两条指令(无符号字节变量的无符号扩展须要一条指令)。
这些移位操作能够通过使用int和unsigned int的局部变量来避免。

这对于那些首先将数据调到局部变量然后利用局部变量进行运算的情况尤其重要。即使数据以8位或16位的形式输入或输出。把他们当作32位来处理仍是有意义的。
我们来考虑以下的三个样例函数:

int wordinc (int a)
{
   return a + 1;
}
short shortinc (short a)
{
    return a + 1;
}
char charinc (char a)
{
    return a + 1;
}
他们的运算结果是相同的。可是第一个代码片断要比其它片断运行的要快。

指针 / Pointers

假设可能。我们应该使用结构体的引用作为參数,也就是结构体的指针,否则。整个结构体就会被压入堆栈。然后传递,这会降低速度。

程序适用值传递可能须要几K字节,而一个简单的指针也能够达到相同的目的。仅仅须要几个字节就能够了。


假设在函数内部不会改变结构体的内容。那么就应该将參数声明为const型的指针。举个样例:

void print_data_of_a_structure ( const Thestruct  *data_pointer)
{
    ...printf contents of the structure...
}
这个样例代码告知编译器在函数内部不会改变外部结构体的内容。訪问他们的时候,不须要重读。还能够确保编译器捕捉不论什么改动这个仅仅读结构体的代码,给结构体以额外的保护。

指针链 / Pointer chains

指针链常常被用来訪问结构体的信息,比方,以下的这段常见的代码:

typedef struct { int x, y, z; } Point3;
typedef struct { Point3 *pos, *direction; } Object;

void InitPos1(Object *p)
{
   p->pos->x = 0;
   p->pos->y = 0;
   p->pos->z = 0;
}
代码中,处理器在每次赋值操作的时候都要又一次装载p->pos,由于编译器不知道p->pos->x不是p->pos的别名。更好的办法是将p->pos缓存成一个局部变量,例如以下:

void InitPos2(Object *p)
{
   Point3 *pos = p->pos;
   pos->x = 0;
   pos->y = 0;
   pos->z = 0;
}
还有一个可能的方法是将Point3结构体包括在Object结构体中,全然避免指针的使用。

条件的运行 / Conditional Execution

条件运行主要用在if语句中,同一时候也会用到由关系运算(<,==,>等)或bool运算(&&, !等)组成的复杂的表达式。尽可能的保持if和else语句的简单是有优点的,这样才干非常好的条件化。关系表达式应该被分成包括类似条件的若干块。


以下的样例演示了编译器怎样使用条件运行:

int g(int a, int b, int c, int d)
{
   if (a > 0 && b > 0 && c < 0 && d < 0)
   //  grouped conditions tied up together//
      return a + b + c + d;
   return -1;
}
条件被分组。便以其能够条件化他们。

Boolean表达式和范围检查 / Boolean Expressions & Range checking

有一种常见的boolean表达式被用来检查是否一个变量取值在某个特定的范围内,比方说,检查一个点是否在一个窗体内。

bool PointInRectangelArea (Point p, Rectangle *r)
{
   return (p.x >= r->xmin && p.x < r->xmax &&
                      p.y >= r->ymin && p.y < r->ymax);
}
这里还有一个更快的方法:把(x >= min && x < max) 转换成 (unsigned)(x-min) < (max-min). 尤其是min为0时,更为有效。以下是优化后的代码:

bool PointInRectangelArea (Point p, Rectangle *r)
{
    return ((unsigned) (p.x - r->xmin) < r->xmax &&
   (unsigned) (p.y - r->ymin) < r->ymax);

}

Boolean表达式&与零的比較 / Boolean Expressions & Compares with zero

在比較(CMP)指令后,相应的处理器标志位就会被设置。这些标志位也能够被其它的指令设置,诸如MOV, ADD, AND, MUL, 也就是主要的数学和逻辑运算指令(数据处理指令)。假如一条数据处理指令要设置这些标志位,那么N和Z标志位的设置方法跟把数字和零比較的设置方法是一样的。N标志位表示结果是不是负数,Z标志位表示结果是不是零。


在C语言中,处理器中的N和Z标志位相应的有符号数的关系运算符是x < 0, x >= 0, x == 0, x != 0。无符号数相应的是x == 0, x != 0 (or x > 0)。


C语言中。每用到一个关系运算符,编译器就会产生一个比較指令。假设关系运算符是上面的当中一个。在数据处理指令紧跟比較指令的情况下。编译器就会将比較指令优化掉。

比方:

int aFunction(int x, int y)
{
   if (x + y < 0)
      return 1;
  else
     return 0;
}
这样做,会在关键循环中节省比較指令。使代码长度降低,效率添加。C语言中没有借位(carry)标志位和溢出(overflow)标志位的概念,所以假设不使用内嵌汇编语言,要訪问C和V标志位是不可能的。虽然如此,编译器支持借位标志位(无符号数溢出),比方说:

int sum(int x, int y)
{
   int res;
   res = x + y;
   if ((unsigned) res < (unsigned) x) // carry set?  //
     res++;
   return res;
}

惰性评预计算 / Lazy Evaluation Exploitation

在类似与这样的 if(a>10 && b=4) 语句中, 确保AND表达式的第一部分最有可能为false, 结果第二部分极有可能不被运行.

用switch() 取代if…else…,在条件选择比較多的情况下。能够用if…else…else…,像这样:

if( val == 1)
    dostuff1();
else if (val == 2)
    dostuff2();
else if (val == 3)
    dostuff3();
使用switch能够更快:

switch( val )
{
    case 1: dostuff1(); break;
    case 2: dostuff2(); break;
    case 3: dostuff3(); break;
}
在if语句中,即使是最后一个条件成立,也要先推断全部前面的条件是否成立。Switch语句能够去除这些额外的工作。假设你不得不使用if…else,那就把最可能的成立的条件放在前面。

二分分解 / Binary Breakdown

把推断条件做成二进制的风格。比方,不要使用以下的列表:

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 */
        }
    }
}
慢速、低效:

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;
    }
}
以上是两个case语句之间的比較

switch语句和查找表 / Switch statement vs. lookup tables

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

免责声明:内容来源于网络,仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇基于Boost库的HTTP Post函数【oracle】cursor 概念,cursor经典例子下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

XNA框架基础——XNA介绍

第1章:XNA介绍 欢迎来到XNA的世界。作为一个游戏程序员,你或许知道 DirectX ,甚至也许具有XNA 框架的基础知识。 这一个章节解释了如何安装 XNA Game Studio Express 和如何以有效率的方式使用它。它也包含相当多的技巧,这些技巧甚至可能对任何一个已经知道基础知识的人都有用。 在下面一些章节中,你将直接地开始开发一些很酷的...

kestrel Server的源码分析

今天这一篇博客讲的是.net core 自带的kestrel server,当你开发微服务k8s部署在linux环境下,一般默认开启这个高性能服务,如果大家之前看过我的owin katana的博客,会发现.net core 的好多实现在之前.net standard 的版本已经实现过了,当时开发的asp.net 程序与IIS紧紧耦合在一起,后来的微软团队意...

从 sourcemap 中获取源码

使用 paazmaya/shuji: Reverse engineering JavaScript and CSS sources from sourcemaps 可以从 sourcemap 中获取源码。 一个故事: 今天同事在完全没将代码加到过 stage 的情况下按了 VSCode 的Discard All Changes,然后这个版本的所有代码都丢失...

在W10系统中配置Java环境变量后,cmd命令提示符找不到java

java环境变量配置在W10系统上和以前有所区别,可能是W10版本导致也可能是W10一开始就出问题。 问题的表现就是你在环境变量里已经配置完JAVA_HOME,CLASSPATH,path之后在控制台输入java或者javac或者java -version 提示:“”不是内部或者外部命令,也不是可运行程序或批处理文件“” ,问题出在W10系统path变量配...

批处理bat脚本自动配置java的jdk环境变量

前言 每当更换电脑或者是重装系统之后,都需要重新配置java系统路径。但是又不想每次都去查配置方法,所以写了个脚本自动配置。 脚本内容 @echo off @echo 第一步 输入要设置的JAVA_HOME路径:(As example: D:\Program Files\Java\jdk1.8.0_181) set /p input="请输入JAVA_H...

Shell脚本开发环境的配置和优化实践

1. 配置vim编辑器 1-1. 为什么不使用vi而是vim vi适合编辑普通文本,不适用编写脚本代码,例如:缺少高亮显示代码、自动缩进等重要功能; vim相当于高级编辑器,可以提高开发效率。 1-2. 设置vim为默认编辑器 [root@oldboy scripts]# echo 'alias vi=vim' >>...