转载请注明来源:http://blog.csdn.net/letian0805/article/details/17191797
作为一个忠实的C语言程序员,经常要因为各种需要优化程序,比如:内存限制、CPU限制、网络限制、磁盘空间限制等。最近在优化公司的程序,顺便将一些优化心得总结出来和大家分享。
gcc 作为一款著名的C编译器,自带了一些优化选项。在gcc参数里加上-On (n为0、1、2、3、s)就可以开启gcc的优化选项。
-O0是gcc的默认选项,意思是不做任何优化。这个选项一般用不到,只有一种情况下:我们不希望gcc做任何优化的时候(比如我们的程序用于调试,需要保留调试信息),加上该选项关闭gcc的优化。
-O1允许gcc从运行效率和程序大小上做少量优化,不进行比较耗编译时间的优化。当程序源码比较多需要用很长时间来编译时,如果程序对优化要求不高,可以用该选项。该选项不会展开循环和内联函数。
-O2允许gcc进行比-O1选项更深一步的优化,进一步减少程序运行效率,但会增加编译时间。对程序运行效率以及大小都有严格要求的可以使用该选项。该选项是推荐选项。
-O3选项会打开所有-O2支持的选项,但-O3更注重优化运行效率,所以该选项会打开-finline-functions,-funswitch-loops以及-fgcse-after-reload 选项,将内联函数和循环展开,程序大小会有所增加。但该选项可能会因为循环的展开而导致一些问题,并不推荐对所有程序使用该选项。
测试结果:https://wiki.edubuntu.org/GccOptimizationTests
-Os选项是在-O2的基础上对程序大小的进一步优化,主要是关闭了-O2中的数据对齐。按理说速度会比-O2慢,但某些情况下速度却比使用-O2快(内存读写速度成为瓶颈时),比如:程序涉及到大量的内存读写,内存对齐后会增加数据量导致运行时间增加。所以,当数据量比较大时,推荐使用该选项。该选项除了取消数据对齐外,还会将部分函数合并为一个函数(比如该函数是个static函数而且只被调用了一次或者函数功能非常简单),会去掉没有被调用到的函数,甚至会去掉一些即使被调用了但基本没啥用的函数。通过对编译结果反汇编我们可以看到区别。
register关键字的作用是定义一个寄存器变量,对该变量的操作实际上是直接操作了某个寄存器。要注意的是需要有足够的寄存器,如果寄存器不够用了,或者被修饰的变量所占比特数大于CPU的位数时,register关键字将失效。那么register关键字将不起作用。register关键字一般用于修饰局部变量,因为局部变量通常要经过压栈的操作,每次对局部变量的操作都涉及到对内存的操作。使用register关键字后,省去了压栈的操作,节省了操作内存的时间。当函数需要频繁操作内存时,使用register关键字将会节省大量的时间。在优化公司的程序的过程中,通过测试发现使用register关键字可以将一个函数的效率提高至少4倍。
普通函数在函数调用时会设计到参数、函数地址的压栈、出栈操作,入栈、出栈操作就是对内存的操作。如果能减少对内存的操作,程序的效率会有所提升。减少压栈、出栈操作可以通过使用宏或者内联函数,但有时候我们需要将函数的参数限定为某一类型,所以我们会需要检测参数的类型,而宏有时候会有副作用(典型的例子是MAX宏的参数里使用了i++之类的),所以这个时候宏就不大适合了,那么我们就需要用内联函数。在static(如果有的话)和函数定义之间加入inline关键字则是定义了一个内联函数,内联函数需要配合gcc编译器的-finline-functions选项,否则编译器不会对内联函数进行展开。
CPU会对条件判断进行预测,如果预测失败,则会带来额外的开销。对于条件与,我们要将判假概率高的条件放在前面;对于条件或,我们要将判真概率高的条件放前面。另外,我们要将执行概率高的分支放到第一个分支的位置。在做这些之前,我们需要对我们写的程序有充分的了解,能够人为预测到大概的概率,也可以通过多次的测试来发现程序的运行规律。另外,如果知道条件是取固定的几个值,最好是选用switch...case而不是if...else。在循环中,最好是将循环因子和 0 比较。
循环中对循环因子的操作(自增、自减、比较)都需要花费一定的时间,减少循环次数可以提高程序的运行效率。当我们害怕编译器的自动展开带来的副作用时,我们可以手动展开循环。循环的展开一般适用于循环体比较简单的情况,若循环体本身就比较复杂,展开会降低代码的可读性。
动态内存的分配开销比较大,特别是内存碎片比较多的时候。因为动态分配内存时系统需要遍历内存链表,如果分配的次数比较多,会浪费很多时间。假如程序中分配内存的次数比较多,而且内存大小相差不大,可以将用完的内存用自己实现的栈管理起来,下次分配的时候直接从栈里获取内存地址。也就是说:释放内存的时候不马上释放,而是将内存地址压入栈中,如果超过栈空间,则释放内存;下次分配内存的时候直接从栈里获取内存地址。
在多线程编程时,难免会需要对某些资源的操作进行加锁。加锁时要注意这几点:1)不要在加锁后进行睡眠、阻塞等操作。2)不要对整个循环体加锁。3)当锁的时间比较长,使用互斥锁,锁的时间比较短时用自旋锁。合理使用锁,可以提高多线程效率,反之无法体现多线程的优点。
(未完待续)