优化C代码的实用技巧:如何在内存和速度之间找到平衡

尽管有许多用于优化 C 代码的有效指南,但没有什么可以替代对编译器和您正在使用的机器的透彻理解。通常,使程序变得更快也会增加代码的大小。这些增加的代码也会影响程序的复杂性和可读性,这是不可接受的。例如,如果你在一些小型设备上编程,比如移动设备、PDA……,这些都有严格的内存限制,所以,优化的座右铭是:编写代码应该针对内存和速度进行优化。

优化C代码的实用技巧:如何在内存和速度之间找到平衡

整数/整数

当我们知道使用的数字不能为负数时,我们应该使用 unsigned int 而不是 int。当某些处理器处理整数算术运算时,unsigned int 比 int 更快。因此,在紧密循环中定义一个整型变量,最好像这样编写代码:

 

register unsigned int variable_name;

但是,我们不能保证编译器会注意到register关键字,而且有可能对于某个处理器来说,无论是否无符号,它都是一样的。这两个关键字并非在所有编译器中都可用。请记住,整数运算比浮点运算快得多,因为处理器可以直接执行整数运算,而浮点运算则依赖于外部浮点处理器或浮点数学库。我们在处理小数时需要更加精确(例如,当我们在做一个简单的统计程序时),将结果限制在不超过100,并尽可能晚地将其转换为浮点数。

除法余数/除法余数

在标准处理器中,32位除法需要20-140个时钟周期才能完成,具体取决于分子和分母,这等于固定时间加上每个位被除法的时间。

时间(分子/分母)= C0 + C1* log2(分子/分母)

= C0 + C1 * (log2(分子)- log2(分母))。

目前的ARM处理器需要消耗20+4.3N个时钟周期。这是一个非常耗时的操作,应该尽可能避免。在某些情况下,除法表达式可以用乘法表达式重写。例如,(a/b)>c可以写为a>(c*b),前提是我们已经知道b是非负数并且b*c不超出整数范围。如果我们可以确定其中一个操作数是无符号的,那么最好使用无符号除法,因为它比有符号除法快得多。

结合除法和余数

在某些情况下,除法和求余运算都是需要的。在这种情况下,编译器将结合除法和余数运算,因为除法运算总是返回商和余数。如果两个操作都需要,我们可以写在一起

 

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

除以二的幂并求余

如果除法运算中的除数是2的幂,我们可以进一步优化除法运算。编译器将使用移位运算来执行此除法运算。因此,我们需要尽可能将比例调整为2的幂(例如,使用64而不是66)。如果是无符号数,则比有符号除法快得多。

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

两种除法都避免调用除法函数,并且无符号除法使用的指令少于有符号除法。有符号除法需要更多时间,因为除法趋向于零,而移位趋向于负无穷大。

模运算的替代方法

我们一般使用求余运算来进行取模,但有时使用 if 语句重写也是可行的。考虑以下两个示例:

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

第二个示例比第一个示例更好,因为它生成的代码会更快。注意:仅当计数在 0 – 59 范围内时才有效。

但我们可以使用下面的代码(作者添加的)来实现等效的功能:

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

使用数组索引/使用数组索引

假设您想根据另一个变量的值将一个变量的值设置为特定字符。你可以这样做:

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';elseletter = '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);}

这将为编译器提供更多优化选项。

实时变量和泄漏/实时变量和溢出

每个处理器中的寄存器数量是固定的,因此程序中特定位置的寄存器中可以存储的变量数量是有限的。一些编译器支持“生存范围分割”,这意味着变量可以分配到函数不同部分的不同寄存器或内存。变量的生存范围定义为:起点是变量的一次空间分配,终点是上次使用到下一次空间分配之间。在这个范围内,变量的值是合法的、存活的。在生存范围之外,该变量不再使用并且已死亡。它的寄存器可以被其他变量使用。这样,编译器就可以将更多的变量排列到寄存器中。

可分配给寄存器的变量所需的寄存器数量等于生命周期重叠的变量数量。如果这个数量超过了可用寄存器的数量,则必须将一些变量临时存储在内存中。这种处理称为“溢出”。

编译器首先释放最不常用的变量,以最小化释放成本。可以通过以下方式避免变量的“释放”:

●限制活动变量的最大数量:通常可以使用简单而紧凑的表达式,而无需在函数内使用太多变量。将大函数拆分为更简单、更小的函数也可能会有所帮助。

●使用关键字register来修改最常用的变量:告诉编译器这个变量将被频繁使用,并要求编译器使用很高的优先级将此变量分配给寄存器。不过,在某些情况下,变量可能会泄漏。

变量类型/变量类型

C 编译器支持基本变量类型:char、short、int、long(有符号、无符号)、float、double。为变量定义最合适的类型非常重要,因为这样可以减少代码和数据的长度,并且可以显着提高效率。

局部变量/局部变量

如果可能,请避免使用 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;}

它们的计算结果是相同的,但是第一个代码片段比其他片段运行得更快。

指针/指针

如果可能的话,我们应该使用结构体的引用作为参数,这是一个指向结构体的指针,否则整个结构体会被压入栈然后再传递,这样会减慢速度。程序按值传输可能需要几个 K 字节,但一个简单的指针可以达到相同的目的,只需要几个字节。

如果结构体的内容不会在函数内部更改,则参数应声明为 const 指针。例如:

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

这个示例代码告诉编译器,外部结构体的内容在函数内部不会改变,访问它们时不需要重新读取它们。它还确保编译器捕获修改此只读结构的任何代码,从而为该结构提供额外的保护。

指针链/指针链

指针链经常用来访问结构体信息,例如下面的常用代码:

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 结构中,完全避免使用指针。

条件执行/条件执行

条件执行主要用在if语句中,也可以用在关系运算(等)或布尔运算(&&、!等)组成的复杂表达式中。最好使 if 和 else 语句尽可能简单,以便可以很好地条件化它们。关系表达式应分为包含相似条件的块。

以下示例演示了编译器如何使用条件执行:

int g(int a, int b, int c, int d){if(a > 0 && b > 0 && c < 0 && d < 0)  //分组化的条件被捆绑在一起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);}

布尔表达式和与零比较/布尔表达式和与零比较

在比较(CMP)指令之后,相应的处理器标志位将被设置。这些标志也可以由其他指令设置,例如MOV、ADD、AND、MUL,它们是基本的数学和逻辑运算指令(数据处理指令)。如果数据处理指令设置这些标志,则 N 和 Z 标志的设置方式与将数字与零进行比较的方式相同。 N标志表示结果是否为负数,Z标志表示结果是否为零。

在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;elsereturn 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 表达式的第一部分很可能为 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,请将最可能为 true 的条件放在前面。

二进制分解/二进制分解

将判断条件做成二进制风格,例如不要使用下面的列表:

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 语句与查找表

switch语句通常用于以下情况:

●调用多​​个函数之一

●设置变量或返回值

●执行多个代码片段之一

如果情况表示很密集,则在使用 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 个字节,第二个例程只需 72 个字节。

循环终止/循环终止

如果不小心编写循环终止条件,可能会给程序带来很大的负担。我们应该尝试使用“倒数到零”循环并使用简单的循环终止条件。循环终止条件比较简单,程序执行过程中消耗的时间也比较少。以下面两个计算 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);}

事实证明,第二个例子比第一个例子快得多。

更快的 for() 循环/更快的 for() 循环

这是一个简单而有效的概念。通常,我们习惯这样编写for循环:

 

for( i = 0;  i < 10;  i++){ ... }

i值为:0,1,2,3,4,5,6,7,8,9

不关心循环计数器的顺序,我们可以这样做:

 

for( i = 10;  i--; ) { ... }

i值为:9,8,7,6,5,4,3,2,1,0,循环速度更快

这种方法是可行的,因为它使用了更快的i--作为测试条件,即“i是否为非零数,如果为负一,则继续”。相对于原始代码,处理器必须“从 i 中减去 10,查看结果是否非零,如果是,则增加 i,然后继续”。在紧密循环中,这会产生显着的差异。

这种语法可能看起来有点奇怪,但它是完全合法的。循环中的第三条语句是可选的(无限循环可以这样写 for(;;)),下面的写法也可以达到同样的效果:

 

for(i = 10;  i;  i--){}

或者:

 

for(i = 10;  i != 0;  i--){}

我们唯一需要注意的是要记住循环需要在 0 处停止(如果循环是从 50-80,则这将不起作用),并且循环的计数器会递减计数。

此外,我们还可以将计数器分配给寄存器,这样可以产生更高效的代码。这种将循环计数器初始化为循环数然后将其递减至零的方法也适用于 while 和 do 语句。

混合循环/循环干扰

当可以使用一个循环时,切勿使用两个循环。但是,如果您要在超出处理器指令缓冲区的循环中执行大量工作,在这种情况下,使用两个单独的循环可能会更快,因为两个循环可能都完全保存在命令缓冲区中。

//原先的代码for(i = 0; i < 100; i++){stuff();}for(i = 0; i < 100; i++){morestuff();}//更好的做法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.}}

循环展开

为了提高效率,可以展开小循环,但这会增加代码的大小。循环分割后,循环计数器更新次数将会减少,执行循环的分支数量也会减少。如果循环仅重复几次,则可以将其完全分解,并且循环造成的开销消失。

例如:

for(i = 0; i < 3; i++){something(i);}//更高效的方式:something(0);something(1);something(2);

因为在每次循环中,i的值都会递增,然后检查它是否有效。如果循环数量固定,编译器通常会像这样展开简单循环。对于这样的循环:

 

for(i = 0; i <  limit; i++) { ... }

它无法被拆卸,因为我们不知道它被循环了多少次。然而,打破这种类型的循环并非不可能。

与简单循环相比,以下代码更长,但效率更高。选择块大小 8 仅用于演示目的,任何合适的长度都是可能的。在示例中,循环的条件仅每八次测试一次,而不是每次。如果需要处理的数组的大小确定了,我们就可以将数组的大小作为块大小(或者可以除以数组长度的值)。然而,块的大小与系统的缓存大小有关。

#include#define BLOCKSIZE (8)int 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);}}return 0;}

统计非零位数/统计设置的位数

例1:测试单个的最低位,计数,然后移位。

//example1int countbit1(uint n){int bits = 0;while (n != 0) {if(n & 1) bits++;n >>= 1;}return bits;}

例2:先除以4,然后计算各部分除以4。循环反汇编常常为程序优化带来新的机会。

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

早期断环/早期断环

通常没有必要经历整个循环。例如,当在数组中搜索特定值时,一旦找到我们需要的值,我们就可以退出循环。下面的示例在 10,000 个数字中搜索 -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,则循环终止,忽略剩余的 9977。

功能设计/功能设计

保持函数简短而简洁是正确的做法。这允许编译器更有效地执行其他优化,例如寄存器分配。

函数调用开销

对于处理器来说,调用函数的开销非常小,通常只占被调用函数执行的工作的很小一部分。使用寄存器可以传递的函数参数的数量是有限制的。这些参数可以是整数兼容的(char、short、int、float 均占用 1 个字),也可以是 4 个字以内的结构体(包括 2 个字的 double 和 long long)。如果参数限制为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函数中,第5个和第6个参数被保存在堆栈中并在f2中恢复。每个参数带来2次内存访问。

最小化参数传递开销/最小化参数传递开销

为了最小化向函数传递参数的成本,我们可以:

只要有可能,请确保函数的形式参数不超过四个,甚至更少,以便不使用堆栈来传递参数。

如果一个函数有四个以上的参数,请确保该函数做了很多工作来抵消传递堆栈参数的成本。

使用指向结构的指针作为形式参数,而不是结构本身。

将相关参数放入结构体中,然后将其指针传递给函数,可以减少参数数量,增加程序的可读性。

尽量减少 long 类型的参数数量,因为它使用两个参数的空间。这同样适用于双倍。

避免出现一部分参数使用寄存器传输,另一部分参数使用堆栈传输的情况。在这种情况下,所有参数都将被压入堆栈。

避免函数参数数量可变的情况。在这种情况下,堆栈用于所有参数。

叶函数/叶函数

如果一个函数不再调用其他函数,这样的函数称为叶函数。在许多应用程序中,大约一半的函数调用是对叶函数的调用。叶函数在所有平台上都非常高效地编译,因为它们不需要参数保存和恢复。与需要 4 或 5 个参数的足够复杂的叶函数所做的工作相比,在入口处压入堆栈并在出口处弹出堆栈的成本非常小。如果可能的话,我们应该尽量将频繁调用的函数安排成叶函数。可以使用分析工具来确定调用函数的次数。有几种方法可以确保函数被编译成叶函数:

●不调用其他函数:包括转为调用C语言库函数的操作,如除法、浮点运算等。

●使用关键字__inline来修改小函数。

内联函数/内联函数

与所有调试选项一样,内联函数被禁用。使用inline关键字修饰函数后,与普通的函数调用不同,代码中对函数的调用将会被函数体本身所取代。这会让代码变得更快,但另一方面也会影响代码的长度,特别是当内联函数很大并且调用频繁的时候。

__inline int square(int x) {return x * x;}#include double length(int x, int y){return sqrt(square(x) + square(y));}

使用内联函数有几个优点:

●没有调用函数的开销。

由于直接替换函数,因此没有存储和恢复寄存器等额外开销。

●更低的参数分配开销。

参数传递的开销通常较低,因为它不需要复制变量。如果某些参数是常量,编译器可以执行进一步的优化。

内联函数的缺点是,如果在很多地方调用该函数,就会增加代码的长度。长度差异的大小很大程度上取决于内联函数的大小和调用它的次数。

明智的做法是只内联一些关键函数。如果设置正确,内联函数可以减少代码长度。函数调用需要一定数量的指令,但使用优化的内联函数可以编译为更少的指令。

使用查找表/使用查找表

有些函数可以近似为查找表,这样可以显着提高效率。查找表通常不如计算公式精确,但在大多数程序中,这种精度已经足够了。

很多信号处理软件(如MODEM调制软件)都会大量使用sin和cos函数,这些函数会带来大量的数学运算。对于实时系统,精度并不是很重要,sin/cos查找表更实用。使用查找表时,尝试将类似的操作合并到一个查找表中,这比使用多个查找表更快且占用的空间更少。

浮点运算

虽然浮点运算对于任何处理器来说都很耗时,但有时我们还是不得不使用浮点运算,比如信号处理。尽管如此,在编写浮点运算代码时,我们需要记住:

●浮点除法速度慢

除法的速度是加法或乘法的两倍,我们可以将除以常数写为乘以该数的倒数(例如,x=x/3.0 写为 x=x*(1.0/3.0))。倒数的计算是在编译阶段完成的。

●使用float代替double

浮点变量消耗更少的内存和寄存器,并且由于精度低而更高效。当精度足够时,应使用浮动。

●不要使用超越函数,

先验函数(如sin、cos、log)是通过一系列乘法和加法来实现的,因此这些运算会比普通乘法慢10倍以上。

●简化浮点表达式

编译器不会对混合整数和浮点运算执行很多优化。例如,3 * (x / 3) 将不会优化为 c)。因此,进行手动优化是有利的。

然而,在某些情况下,浮点运算的效率无法达到规定的水平。在这种情况下,最好的办法可能是放弃浮点运算,改用定点运算。当变量的变化范围足够小时,定点运算比浮点运算更准确、更快。

一般来说,存储空间可以换取时间。您可以缓存经常使用的数据,而不是每次都重新计算或重新加载。例如,sin/cos 表,或者伪随机数表(如果你确实不需要随机数,可以在开始时计算 1,000,并在后续代码中重用它们)

尽可能少使用全局变量。

将文件中的变量声明为静态变量,除非有必要将其设为全局变量。

不要使用递归。递归可以让代码变得非常整洁、美观,但是会产生大量的函数调用和开销。

访问一维数组比访问多维数组更快

使用#define宏来替换常用的小函数。

参考文献/参考文献

你可能想看:
« 上一篇
下一篇 »

相关推荐

FreeBasic:强大免费开源的64位basic编译器及应用优势

2025年05月10日

83阅读

脊柱侧弯矫正方法大盘点:运动康复、支具矫正等多种方式

2025年05月09日

88阅读

如何在winForm标题栏去掉标题文字且任务栏图标正常显示

2025年05月09日

86阅读

十一黄金周促销广告词大全,国庆购物狂欢等你来

2025年05月09日

78阅读

关闭快速编辑模式及插入模式,设置控制台指定文字颜色举例

2025年05月09日

64阅读

黄金搭档低糖高钙饮品包装及成分功效全解析,适合人群要知晓

2025年05月09日

91阅读