关于c++:C语言高效编程与代码优化

9次阅读

共计 14454 个字符,预计需要花费 37 分钟才能阅读完成。

英文原文:https://www.codeproject.com/A…

翻译作者:码农网 – gunner

在本篇文章中,我收集了很多教训和办法。利用这些教训和办法,能够帮忙咱们从执行速度和内存应用等方面来优化 C 语言代码。

简介

在最近的一个我的项目中,咱们须要开发一个运行在挪动设施上但不保障图像高质量的轻量级 JPEG 库。期间,我总结了一些让程序运行更快的办法。在本篇文章中,我收集了一些教训和办法。利用这些教训和办法,能够帮忙咱们从执行速度和内存应用等方面来优化 C 语言代码。

只管在 C 代码优化方面有很多的指南,然而对于编译和你应用的编程机器方面的优化常识却很少。

通常,为了让你的程序运行的更快,程序的代码量可能须要减少。代码量的减少又可能会对程序的复杂度和可读性带来不利的影响。这对于在手机、PDA 等对于内存应用有很多限度的小型设施上编写程序时是不被容许的。因而,在代码优化时,咱们的座右铭应该是确保内存应用和执行速度两方面都失去优化。

申明

实际上,在我的我的项目中,我应用了很多优化 ARM 编程的办法(该我的项目是基于 ARM 平台的),也应用了很多互联网下面的办法。但并不是所有文章提到的办法都能起到很好的作用。所以,我对有用的和高效的办法进行了总结收集。同时,我还批改了其中的一些办法,使他们实用于所有的编程环境,而不是局限于 ARM 环境。

哪里须要应用这些办法

没有这一点,所有的探讨都无从谈起。程序优化最重要的就是找出待优化的中央,也就是找出程序的哪些局部或者哪些模块运行迟缓亦或耗费大量的内存。只有程序的各局部通过了优化,程序能力执行的更快。

程序中运行最多的局部,特地是那些被程序外部循环反复调用的办法最该被优化。

对于一个有教训的码农,发现程序中最须要被优化的局部往往很简略。此外,还有很多工具能够帮忙咱们找出须要优化的局部。我应用过 Visual C++ 内置的性能工具 profiler 来找出程序中耗费最多内存的中央。另一个我应用过的工具是英特尔的 Vtune,它也能很好的检测出程序中运行最慢的局部。依据我的教训,外部或嵌套循环,调用第三方库的办法通常是导致程序运行迟缓的最次要的起因。

整形数

如果咱们确定整数非负,就应该应用 unsigned int 而不是 int。有些处理器解决无符号 unsigned 整形数的效率远远高于有符号 signed 整形数(这是一种很好的做法,也有利于代码具体类型的自解释)。

因而,在一个严密循环中,申明一个 int 整形变量的最好办法是:

register unsigned int variable_name;

记住,整形 in 的运算速度高浮点型 float,并且能够被处理器间接实现运算,而不须要借助于 FPU(浮点运算单元)或者浮点型运算库。只管这不保障编译器肯定会应用到寄存器存储变量,也不能保障处理器解决能更高效解决 unsigned 整型,但这对于所有的编译器是通用的。

例如在一个计算包中,如果须要后果准确到小数点后两位,咱们能够将其乘以 100,而后尽可能晚的把它转换为浮点型数字。

除法和取余数

在规范处理器中,对于分子和分母,一个 32 位的除法须要应用 20 至 140 次循环操作。除法函数耗费的工夫包含一个常量工夫加上每一位除法耗费的工夫。

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

对于 ARM 处理器,这个版本须要 20+4.3N 次循环。这是一个耗费很大的操作,应该尽可能的防止执行。有时,能够通过乘法表达式来代替除法。例如,如果咱们晓得 b 是负数并且 b c 是个整数,那么 (a/b)>c 能够改写为 a >(cb)。如果确定操作数是无符号 unsigned 的,应用无符号 unsigned 除法更好一些,因为它比有符号 signed 除法效率高。

合并除法和取余数

在一些场景中,同时须要除法(x/y)和取余数(x%y)操作。这种状况下,编译器能够通过调用一次除法操作返回除法的后果和余数。如果既须要除法的后果又须要余数,咱们能够将它们写在一起,如下所示:

int func_div_and_mod (int a, int b) 
{return (a / b) + (a % b);
}

通过 2 的幂次进行除法和取余数


如果除法中的除数是 2 的幂次,咱们能够更好的优化除法。编译器应用移位操作来执行除法。因而,咱们须要尽可能的设置除数为 2 的幂次(例如 64 而不是 66)。并且仍然记住,无符号 unsigned 整数除法执行效率高于有符号 signed 整形除法。

typedef unsigned int uint;
uint div32u (uint a) 
{return a / 32;}
int div32s (int a)
{return a / 32;}

下面两种除法都防止间接调用除法函数,并且无符号 unsigned 的除法应用更少的计算机指令。因为须要移位到 0 和正数,有符号 signed 的除法须要更多的工夫执行。

取模的一种代替办法

咱们应用取余数操作符来提供算数取模。但有时能够联合应用 if 语句进行取模操作。思考如下两个例子:

uint modulo_func1 (uint count)
{return (++count % 60);
}
uint modulo_func2 (uint count)
{if (++count >= 60)
 count = 0; 
 return (count);
}

优先应用 if 语句,而不是取余数运算符,因为 if 语句的执行速度更快。这里留神新版本函数只有在咱们晓得输出的 count 结余 0 至 59 时在能正确的工作。

应用数组下标

如果你想给一个变量设置一个代表某种意思的字符值,你可能会这样做:

switch (queue)
{
 case 0 :letter = 'W';break;
 case1 :letter = 'S';break;
 case2 :letter = 'U';break;}

或者这样做:

if (queue == 0)
 letter = 'W';
else if (queue == 1)
 letter = 'S';
else  letter = 'U';

一种更简洁、更快的办法是应用数组下标获取字符数组的值。如下:

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

全局变量


全局变量绝不会位于寄存器中。应用指针或者函数调用,能够间接批改全局变量的值。因而,编译器不能将全局变量的值缓存在寄存器中,但这在应用全局变量时便须要额定的(经常是不必要的)读取和存储。所以,在重要的循环中咱们不倡议应用全局变量。

如果函数过多的应用全局变量,比拟好的做法是拷贝全局变量的值到局部变量,这样它才能够寄存在寄存器。这种办法仅仅实用于全局变量不会被咱们调用的任意函数应用。例子如下:

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 于寄存器并且只须要一个计算机指令。

应用别名

思考如下的例子:

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-range splitting),也就是说在程序的不同局部,变量能够被调配到不同的寄存器或者内存中。变量的生命周期开始于对它进行的最初一次赋值,完结于下次赋值前的最初一次应用。在生命周期内,变量的值是无效的,也就是说变量是活着的。不同生命周期之间,变量的值是不被须要的,也就是说变量是死掉的。这样,寄存器就能够被其余变量应用,从而容许编译器调配更多的变量应用寄存器。

须要应用寄存器调配的变量数目须要超过函数中不同变量生命周期的个数。如果不同变量生命周期的个数超过了寄存器的数目,那么一些变量必须长期存储于内存。这个过程就称之为宰割。

编译器首先宰割最近应用的变量,用以升高宰割带来的耗费。禁止变量生命周期宰割的办法如下:

  • 限定变量的应用数量:这个能够通过放弃函数中的表达式简略、玲珑、不应用太多的变量实现。将较大的函数拆分为小而简略的函数也会达到很好的成果。
  • 对常常应用到的变量采纳寄存器存储:这样容许咱们通知编译器该变量是须要常常应用的,所以须要优先存储于寄存器中。然而,在某种状况下,这样的变量仍然可能会被宰割出寄存器。

变量类型

C 编译器反对根本类型:char、short、int、long(包含有符号 signed 和无符号 unsigned)、float 和 double。应用正确的变量类型至关重要,因为这能够缩小代码和数据的大小并大幅减少程序的性能。

局部变量

咱们应该尽可能的不应用 char 和 short 类型的局部变量。对于 char 和 short 类型,编译器须要在每次赋值的时候将局部变量缩小到 8 或者 16 位。这对于有符号变量称之为有符号扩大,对于无符号变量称之为零扩大。这些扩大能够通过寄存器左移 24 或者 16 位,而后依据有无符号标记右移雷同的位数实现,这会耗费两次计算机指令操作(无符号 char 类型的零扩大仅须要耗费一次计算机指令)。

能够通过应用 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;}

只管后果均雷同,然而第一个程序片段运行速度高于后两者。

指针

咱们应该尽可能的应用援用值的形式传递构造数据,也就是说应用指针,否则传递的数据会被拷贝到栈中,从而升高程序的性能。我曾见过一个程序采纳传值的形式传递十分大的构造数据,而后这能够通过一个简略的指针更好的实现。

函数通过参数承受构造数据的指针,如果咱们确定不扭转数据的值,咱们须要将指针指向的内容定义为常量。例如:

void print_data_of_a_structure (const Thestruct  *data_pointer)
{...printf contents of the structure...}

这个示例通知编译器函数不会扭转内部参数的值(应用 const 润饰),并且不必在每次拜访时都进行读取。同时,确保编译器限度任何对只读构造的批改操作从而给予构造数据额定的爱护。

指针链

指针链常常被用于拜访构造数据。例如,罕用的代码如下:

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;
}

另一种办法是在 Object 构造中间接蕴含 Point3 类型的数据,这能齐全打消对 Point3 应用指针操作。

条件执行

条件执行语句大多在 if 语句中应用,也在应用关系运算符(<,==,> 等)或者布尔值表达式(&&,!等)计算简单表达式时应用。对于蕴含函数调用的代码片段,因为函数返回值会被销毁,因而条件执行是有效的。

因而,放弃 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;
}

因为条件被汇集到一起,编译器可能将他们集中处理。

布尔表达式和范畴查看

一个罕用的布尔表达式是用于判断变量是否位于某个范畴内,例如,查看一个图形坐标是否位于一个窗口内:

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);
}

布尔表达式和零值比拟


处理器的标记位在比拟指令操作后被设置。标记位同样能够被诸如 MOV、ADD、AND、MUL 等根本算术和裸机指令改写。如果数据指令设置了标记位,N 和 Z 标记位也将与后果与 0 比拟一样进行设置。N 标记示意后果是否是负值,Z 标记示意后果是否是 0。

C 语言中,处理器中的 N 和 Z 标记位与上面的指令分割在一起:有符号关系运算 x <0,x>=0,x==0,x!=0;无符号关系运算 x ==0,x!=0(或者 x >0)。

C 代码中每次关系运算符的调用,编译器都会收回一个比拟指令。如果操作符是下面提到的,编译器便会优化掉比拟指令。例如:

int aFunction(int x, int y)
{if (x + y < 0)
 return 1; 
 else
 return 0;
}

尽可能的应用下面的判断形式,这能够在要害循环中缩小比拟指令的调用,进而缩小代码体积并进步代码性能。C 语言没有借位和溢出位的概念,因而,如果不借助汇编,不可能间接应用借位标记 C 和溢出位标记 V。但编译器反对借位(无符号溢出),例如:

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

懒检测开发


在 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) 
 dostuff3();

应用 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
 "EQ0NE0CS0CC0MI0PL0VS0VC0HI0LS0GE0LT0GT0LE00" + 3 * condition;
}

第一个程序须要 240 bytes,而第二个仅仅须要 72 bytes。

循环

循环是大多数程序中的罕用的构造;程序执行的大部分工夫产生在循环中,因而非常值得在循环执行工夫高低一番功夫。

循环终止

如果不加留神,循环终止条件的编写会导致额定的累赘。咱们应该应用计数到零的循环和简略的循环终止条件。简略的终止条件耗费更少的工夫。看上面计算 n!的两个程序。第一个实现应用递增的循环,第二个实现应用递加循环。

int fact1_func (int n)
{   int i, fact = 1;
 for (i = 1; i <= n; i++)
 fact *= i;
 return (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<STDIO.H>
#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);
 }
 }
} 

统计非零位的数量

通过一直的左移,提取并统计最低位,示例程序 1 高效的查看一个数组中有几个非零位。示例程序 2 被循环展开四次,而后通过将四次移位合并成一次来优化代码。常常开展循环,能够提供很多优化的机会。

//Example - 1
int countbit1(uint n)
{
 int bits = 0;
 while (n != 0)
 {if (n & 1) bits++;
 n >>= 1;
 }
 return bits;
}
 
//Example - 2
int 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 <MATH.H>
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 选项。

最初,然而是最重要的是 - 将编译器优化选项关上!看上去很不言而喻,但却常常在产品推出时被遗记。编译器可能在更底层上对代码进行优化,并针对指标处理器执行特定的优化解决。

正文完
 0