打印
[经验分享]

C语言代码优化方法

[复制链接]
3749|49
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
lzbf|  楼主 | 2024-4-27 10:59 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
1、选择合适的算法和数据结构
选择一种合适的数据结构很重要,如果在一堆随机存放的数中使用了大量的插入和删除指令,那使用链表要快得多。数组与指针语句具有十分密切的关系,一般来说,指针比较灵活简洁,而数组则比较直观,容易理解。对于大部分的编译器,使用指针比使用数组生成的代码更短,执行效率更高。

在许多种情况下,可以用指针运算代替数组索引,这样做常常能产生又快又短的代码。与数组索引相比,指针一般能使代码速度更快,占用空间更少。使用多维数组时差异更明显。下面的代码作用是相同的,但是效率不一样。

    数组索引                指针运算

    For(;;){               p=array

    A=array[t++];          for(;;){

                                a=*(p++);

    。。。。。。。。。。。。。。。

    }                      }
指针方法的优点是,array的地址每次装入地址p后,在每次循环中只需对p增量操作。在数组索引方法中,每次循环中都必须根据t值求数组下标的复杂运算。

2、使用尽量小的数据类型
能够使用字符型(char)定义的变量,就不要使用整型(int)变量来定义;能够使用整型变量定义的变量就不要用长整型(long int),能不使用浮点型(float)变量就不要使用浮点型变量。当然,在定义变量后不要超过变量的作用范围,如果超过变量的范围赋值,C编译器并不报错,但程序运行结果却错了,而且这样的错误很难发现。

在ICCAVR中,可以在Options中设定使用printf参数,尽量使用基本型参数(%c、%d、%x、%X、%u和%s格式说明符),少用长整型参数(%ld、%lu、%lx和%lX格式说明符),至于浮点型的参数(%f)则尽量不要使用,其它C编译器也一样。在其它条件不变的情况下,使用%f参数,会使生成的代码的数量增加很多,执行速度降低。

3、减少运算的强度
(1)、查表(游戏程序员必修课)
一个聪明的游戏大虾,基本上不会在自己的主循环里搞什么运算工作,绝对是先计算好了,再到循环里查表。看下面的例子:

旧代码:

long factorial(int i)
{
    if (i == 0)
      return 1;
    else
      return i * factorial(i - 1);
}
新代码:

static long factorial_table[] = {1, 1, 2, 6, 24, 120, 720  /* etc */ };
long factorial(int i)
{
    return factorial_table[i];
}
如果表很大,不好写,就写一个init函数,在循环外临时生成表格。

(2)求余运算
a=a%8;
可以改为:

a=a&7;
说明:位操作只需一个指令周期即可完成,而大部分的C编译器的“%”运算均是调用子程序来完成,代码长、执行速度慢。通常,只要求是求2n方的余数,均可使用位操作的方法来代替。

(3)平方运算
a=pow(a, 2.0);
可以改为:

a=a*a;
说明:在有内置硬件乘法器的单片机中(如51系列),乘法运算比求平方运算快得多,因为浮点数的求平方是通过调用子程序来实现的,在自带硬件乘法器的AVR单片机中,如ATMega163中,乘法运算只需2个时钟周期就可以完成。既使是在没有内置硬件乘法器的AVR单片机中,乘法运算的子程序比平方运算的子程序代码短,执行速度快。

如果是求3次方,如:

a=pow(a,3.0);
更改为:

a=a*a*a;
则效率的改善更明显。

(4)用移位实现乘除法运算
a=a*4;
b=b/4;
可以改为:

a=a<<2;
b=b>>2;
通常如果需要乘以或除以2n,都可以用移位的方法代替。在ICCAVR中,如果乘以2n,都可以生成左移的代码,而乘以其它的整数或除以任何数,均调用乘除法子程序。用移位的方法得到代码比调用乘除法子程序生成的代码效率高。实际上,只要是乘以或除以一个整数,均可以用移位的方法得到结果,如:

a=a*9
可以改为:

a=(a<<3)+a
采用运算量更小的表达式替换原来的表达式,下面是一个经典例子:

旧代码:

x = w % 8;
y = pow(x, 2.0);
z = y * 33;
for (i = 0;i < MAX;i++)
{
    h = 14 * i;
    printf("%d", h);
}
新代码:

x = w & 7;                 /* 位操作比求余运算快*/
y = x * x;                 /* 乘法比平方运算快*/
z = (y << 5) + y;          /* 位移乘法比乘法快 */
for (i = h = 0; i < MAX; i++)
{
    h += 14;               /* 加法比乘法快 */
    printf("%d",h);
}
(5)避免不必要的整数除法
整数除法是整数运算中最慢的,所以应该尽可能避免。一种可能减少整数除法的地方是连除,这里除法可以由乘法代替。这个替换的副作用是有可能在算乘积时会溢出,所以只能在一定范围的除法中使用。

不好的代码:

int i, j, k, m;
m = i / j / k;
推荐的代码:

int i, j, k, m;
m = i / (j * k);
(6)使用增量和减量操作符
在使用到加一和减一操作时尽量使用增量和减量操作符,因为增量符语句比赋值语句更快,原因在于对大多数CPU来说,对内存字的增、减量操作不必明显地使用取内存和写内存的指令,比如下面这条语句:

x=x+1;
模仿大多数微机汇编语言为例,产生的代码类似于:

move A,x      ;把x从内存取出存入累加器A
add A,1       ;累加器A加1
store x        ;把新值存回x
如果使用增量操作符,生成的代码如下:

incr x           ;x加1
显然,不用取指令和存指令,增、减量操作执行的速度加快,同时长度也缩短了。

(7)使用复合赋值表达式
复合赋值表达式(如a-=1及a+=1等)都能够生成高质量的程序代码。

(8)提取公共的子表达式
在某些情况下,C++编译器不能从浮点表达式中提出公共的子表达式,因为这意味着相当于对表达式重新排序。

需要特别指出的是,编译器在提取公共子表达式前不能按照代数的等价关系重新安排表达式。这时,程序员要手动地提出公共的子表达式(在VC.NET里有一项“全局优化”选项可以完成此工作,但效果就不得而知了)。

不好的代码:

float a, b, c, d, e, f;
。。。
e = b * c / d;
f = b / d * a;
推荐的代码:

float a, b, c, d, e, f;
。。。
const float t(b / d);
e = c * t;
f = a * t;
不好的代码:

float a, b, c, e, f;
。。。
e = a / c;
f = b / c;
推荐的代码:

float a, b, c, e, f;
。。。
const float t(1.0f / c);
e = a * t;
f = b * t;
4、结构体成员的布局
很多编译器有“使结构体字,双字或四字对齐”的选项。但是,还是需要改善结构体成员的对齐,有些编译器可能分配给结构体成员空间的顺序与他们声明的不同。但是,有些编译器并不提供这些功能,或者效果不好。

所以,要在付出最少代价的情况下实现最好的结构体和结构体成员对齐,建议采取下列方法:

(1)按数据类型的长度排序
把结构体的成员按照它们的类型长度排序,声明成员时把长的类型放在短的前面。编译器要求把长型数据类型存放在偶数地址边界。

在申明一个复杂的数据类型 (既有多字节数据又有单字节数据) 时,应该首先存放多字节数据,然后再存放单字节数据,这样可以避免内存的空洞。编译器自动地把结构的实例对齐在内存的偶数边界。

(2)把结构体填充成最长类型长度的整倍数
把结构体填充成最长类型长度的整倍数。照这样,如果结构体的第一个成员对齐了,所有整个结构体自然也就对齐了。下面的例子演示了如何对结构体成员进行重新排序:

不好的代码,普通顺序:

struct
{
  char a[5];
  long k;
  double x;
} baz;
推荐的代码,新的顺序并手动填充了几个字节:

struct
{
  double x;
  long k;
  char a[5];
  char pad[7];
} baz;
这个规则同样适用于类的成员的布局。

(3)按数据类型的长度排序本地变量
当编译器分配给本地变量空间时,它们的顺序和它们在源代码中声明的顺序一样,和上一条规则一样,应该把长的变量放在短的变量前面。如果第一个变量对齐了,其它变量就会连续的存放,而且不用填充字节自然就会对齐。有些编译器在分配变量时不会自动改变变量顺序,有些编译器不能产生4字节对齐的栈,所以4字节可能不对齐。

下面这个例子演示了本地变量声明的重新排序:

不好的代码,普通顺序

short ga, gu, gi;
long foo, bar;
double x, y, z[3];
char a, b;
float baz;
推荐的代码,改进的顺序

double z[3];
double x, y;
long foo, bar;
float baz;
short ga, gu, gi;
(4)把频繁使用的指针型参数拷贝到本地变量
避免在函数中频繁使用指针型参数指向的值。因为编译器不知道指针之间是否存在冲突,所以指针型参数往往不能被编译器优化。这样数据不能被存放在寄存器中,而且明显地占用了内存带宽。

注意,很多编译器有“假设不冲突”优化开关(在VC里必须手动添加编译器命令行/Oa或/Ow),这允许编译器假设两个不同的指针总是有不同的内容,这样就不用把指针型参数保存到本地变量。否则,请在函数一开始把指针指向的数据保存到本地变量。如果需要的话,在函数结束前拷贝回去。

使用特权

评论回复
沙发
alvpeg| | 2024-5-1 20:30 | 只看该作者
避免重复计算相同的值,特别是那些计算代价高昂的操作,如幂运算、高复杂度的数学函数等。可以通过将这些值缓存起来重复使用来优化。

使用特权

评论回复
板凳
phoenixwhite| | 2024-5-1 23:00 | 只看该作者
大多数现代编译器提供了多种优化选项,如-O2或-O3,这些选项可以自动应用许多优化技术。

使用特权

评论回复
地板
robertesth| | 2024-5-2 17:11 | 只看该作者
除非必要(如与硬件直接交互的场合),否则避免在C代码中使用汇编语言。汇编语言虽然可以提供更高的性能,但会降低代码的可读性和可移植性。

使用特权

评论回复
5
houjiakai| | 2024-5-2 17:14 | 只看该作者
函数调用会有额外的开销,包括保存和恢复调用者的状态,考虑是否可以合并一些函数调用或者将一些小函数内联。

使用特权

评论回复
6
bestwell| | 2024-5-2 17:21 | 只看该作者
全局变量会影响优化,因为它可能会被任何函数修改,这使得编译器无法进行某些优化。

使用特权

评论回复
7
gygp| | 2024-5-2 17:25 | 只看该作者
利用编译器的特定功能,如内联函数、限制指针别名(restrict)等,以提高代码性能。

使用特权

评论回复
8
vivilyly| | 2024-5-2 17:29 | 只看该作者
内存分配和释放会增加程序的执行开销。尽量减少内存分配和释放,可以通过使用静态分配(static allocation)或者对象池(object pools)实现。

使用特权

评论回复
9
biechedan| | 2024-5-2 20:09 | 只看该作者
尽量减少内存访问次数,特别是随机内存访问。利用缓存友好性(cache-friendly)的数据布局和访问模式。

使用特权

评论回复
10
hudi008| | 2024-5-2 20:14 | 只看该作者
选择合适的算法和数据结构对程序的性能有巨大影响。例如,使用哈希表可以加快查找速度,而使用适当的排序算法可以提升整体效率。

使用特权

评论回复
11
tifmill| | 2024-5-2 20:18 | 只看该作者
优化算法和数据结构通常比优化代码本身更有效。选择更高效的算法或调整数据结构以减少内存访问次数和计算复杂度。

使用特权

评论回复
12
pentruman| | 2024-5-2 20:22 | 只看该作者
位操作通常比其对应的数**算更快,因此,在处理标志位或进行位级操作时,使用位操作可以提高效率。

使用特权

评论回复
13
primojones| | 2024-5-2 20:26 | 只看该作者
现代编译器通常已经内置了许多优化技术,如循环展开、常量折叠、死代码删除等。在手动优化代码之前,先了解并评估编译器的优化能力是很重要的。

使用特权

评论回复
14
rosemoore| | 2024-5-2 20:30 | 只看该作者
避免重复计算相同的值,特别是那些计算代价高昂的操作,如幂运算、高复杂度的数学函数等。可以通过将这些值缓存起来重复使用来优化。

使用特权

评论回复
15
jtracy3| | 2024-5-2 21:48 | 只看该作者
函数调用会产生额外的开销,尤其是在嵌入式系统中。如果可以的话,尽量将一些简单的函数内联,或者在循环内部避免过多的函数调用。

使用特权

评论回复
16
uytyu| | 2024-5-2 21:57 | 只看该作者
使用合适的数据类型可以减少内存占用和提高程序执行速度。例如,当需要存储一个范围较小的整数时,可以使用short或char类型,而不是int类型。

使用特权

评论回复
17
uiint| | 2024-5-2 22:07 | 只看该作者
位操作通常比其他操作更快,因为它们直接在寄存器上进行。尽量使用位操作代替其他操作,例如使用按位与(bitwise AND)代替乘法,使用按位或(bitwise OR)代替加法。

使用特权

评论回复
18
modesty3jonah| | 2024-5-2 22:14 | 只看该作者
在循环中,尽量避免在每次迭代中都进行相同的计算,可以将这些计算移到循环外部。

使用特权

评论回复
19
belindagraham| | 2024-5-2 22:18 | 只看该作者
函数调用会增加程序的执行开销,因为需要保存和恢复寄存器状态。尽量减少函数调用,可以通过内联函数(inline functions)或宏(macros)实现。

使用特权

评论回复
20
earlmax| | 2024-5-4 12:05 | 只看该作者
优化条件语句以减少分支预测错误。例如,重新排序条件语句、使用查表法代替复杂的条件判断等。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

124

主题

5199

帖子

3

粉丝