代码优化经验总结(4)

来源:互联网 发布:chrome for mac 翻墙 编辑:程序博客网 时间:2024/06/02 04:23


零散优化方法

1, 用inline或宏,可减少函数调用的开销(运行时的堆栈操作支持语句).另外在函数中的大量相似语句可以用

宏来实现(并不一定使变快,但源码可并小并更美观).

主意: (1),宏不做类型检查,仅仅是替换,在使用宏定义中的编排格式

(2), inline specifier仅仅是对编译器的建议,编译器有权利忽略这个建议。编译器根据函数体的大小,是否有局部对象被声明,函数的复杂性等等决定。


2,分清什么条件判断是运行时的,什么是编译时. 如果是在编译时可选择的代码段,可用宏实现.


3, 尽量将参数包成结构体指针或对象来传,因为这实际上就只是传个指针而已. 


4, 能用更小的数据结构尽量用更小的,如C中无Bool类型,那么如果要用尽量不要用int,而是用char.另外尽量在结构体中使用位域运算符和union来节省空间.(union使用时应该注意任何时侯只能有一个数据成员有效,另外它没有运行时负担, 而在编译的代码生成阶段优化)


5, 将条件判断合并到起来.常用技巧是擅用Bit来代替byte或int, 假如有几个bool变量,并且需要根据它们值进行不同的处理.那么可以用一个byte,并将它的每一位表示一个bool变量.这样的好处在于你可以用一个位操作符判断一组bool变量.(同时这样也节约了空间,当发生内存拷贝或涉及到分布式,那其实就是提高效率)


6, 用内存换性能是不变的秘诀.如用一个大数组来代替动态在堆上分配.用多维数组来代替结构体数组.当然在使用上有它的局限性和复杂性.(值得注意的是当访问数组时.指针的效率却比数组下标,原因在于一个是指针变量++,另一个是数组的访问下标++,再将这个值加到数组起始地址上)


7, 结构体成员的对齐, 很多编译器有“使结构体字,双字或四字对齐”的选项。但是,还是需要改善结构体成员的对齐,有些编译器可能分配给结构体成员空间的顺序与他们声明的不同。但是,有些编译器并不提供这些功能,或者效果不好。所以,要在付出最少代价的情况下实现最好的结构体和结构体成员对齐.

实现策略: (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;


8 提高CPU的并行性

(1)使用并行代码 

尽可能把长的有依赖的代码链分解成几个可以在流水线执行单元中并行执行的没有依赖的代码链。很多高级语言,包括C++,并不对产生的浮点表达式重新排序,因为那是一个相当复杂的过程。需要注意的是,重排序的代码和原来的代码在代码上一致并不等价于计算结果一致,因为浮点操作缺乏精确度。在一些情况下,这些优化可能导致意料之外的结果。幸运的是,在大部分情况下,最后结果可能只有最不重要的位(即最低位)是错误的。 

不好的代码: 

double a[100], sum; 

int i; 

sum = 0.0f; 

for (i=0; i<100; i++) 

sum += a[i]; 

推荐的代码: 

double a[100], sum1, sum2, sum3, sum4, sum; 

int i; 

sum1 = sum2 = sum3 = sum4 = 0.0; 

for (i = 0; i < 100; i += 4) 

  sum1 += a[i]; 

  sum2 += a[i+1]; 

  sum3 += a[i+2]; 

  sum4 += a[i+3]; 

sum = (sum4+sum3)+(sum1+sum2);  

  要注意的是:使用4 路分解是因为这样使用了4段流水线浮点加法,浮点加法的每一个段占用一个时钟周期,保证了最大的资源利用率。 

(2)避免没有必要的读写依赖 

当数据保存到内存时存在读写依赖,即数据必须在正确写入后才能再次读取。虽然AMD Athlon等CPU有加速读写依赖延迟的硬件,允许在要保存的数据被写入内存前读取出来,但是,如果避免了读写依赖并把数据保存在内部寄存器中,速度会更快。在一段很长的又互相依赖的代码链中,避免读写依赖显得尤其重要。如果读写依赖发生在操作数组时,许多编译器不能自动优化代码以避免读写依赖。所以推荐程序员手动去消除读写依赖,举例来说,引进一个可以保存在寄存器中的临时变量。这样可以有很大的性能提升。下面一段代码是一个例子: 

不好的代码: 

float x[VECLEN], y[VECLEN], z[VECLEN]; 

。。。。。。 

for (unsigned int k = 1; k < VECLEN; k ++) 

  x[k] = x[k-1] + y[k]; 

for (k = 1; k <VECLEN; k++) 

  x[k] = z[k] * (y[k] - x[k-1]); 

推荐的代码: 

float x[VECLEN], y[VECLEN], z[VECLEN]; 

。。。。。。 

float t(x[0]); 

for (unsigned int k = 1; k < VECLEN; k ++) 

  t = t + y[k]; 

  x[k] = t; 

t = x[0]; 

for (k = 1; k <; VECLEN; k ++) 

  t = z[k] * (y[k] - t); 

  x[k] = t; 

}  


9, 循环不变计算

对于一些不需要循环变量参加运算的计算任务可以把它们放到循环外面,现在许多编译器还是能自己干这件事,不过对于中间使用了变量的算式它们就不敢动了,所以很多情况下你还得自己干。对于那些在循环中调用的函数,凡是没必要执行多次的操作通通提出来,放到一个init函数里,循环前调用。另外尽量减少喂食次数,没必要的话尽量不给它传参,需要循环变量的话让它自己建立一个静态循环变量自己累加,速度会快一点。 

还有就是结构体访问,东楼的经验,凡是在循环里对一个结构体的两个以上的元素执行了访问,就有必要建立中间变量了(结构这样,那C++的对象呢?想想看),看下面的例子: 

旧代码: 

    total = 

    a->b->c[4]->aardvark + 

    a->b->c[4]->baboon + 

    a->b->c[4]->cheetah + 

    a->b->c[4]->dog; 

新代码: 

    struct animals * temp = a->b->c[4]; 

    total = 

    temp->aardvark + 

    temp->baboon + 

    temp->cheetah + 

    temp->dog; 

一些老的C语言编译器不做聚合优化,而符合ANSI规范的新的编译器可以自动完成这个优化,看例子: 

    float a, b, c, d, f, g; 

    。。。 

    a = b / c * d; 

    f = b * g / c; 

这种写法当然要得,但是没有优化 

    float a, b, c, d, f, g; 

    。。。 

    a = b / c * d; 

    f = b / c * g; 

如果这么写的话,一个符合ANSI规范的新的编译器可以只计算b/c一次,然后将结果代入第二个式子,节约了一次除法运算。


10,采用递归 

与LISP之类的语言不同,C语言一开始就病态地喜欢用重复代码循环,许多C程序员都是除非算法要求,坚决不用递归。事实上,C编译器们对优化递归调用一点都不反感,相反,它们还很喜欢干这件事。只有在递归函数需要传递大量参数,可能造成瓶颈的时候,才应该使用循环代码,其他时候,还是用递归好些。




三, 结构及算法优化.

1, 尽可能用大量的hash替代查找和搜索,常用的方法是用hash实现通过数组下标访问元素.当然如果数组中存放的是函数指针,那实际上就是快速动态改变代码执行流了.


2, 查表,原理就是先计算好,要使用时只需直接查表,而查表这个动作又可用hash.(不要在自己的主循环里搞什么运算工作,绝对是先计算好了,再到循环里查表。

原创粉丝点击