c语言优化

转自http://just-study.blogbus.com/logs/37238127.html 代码有部分改动

介绍

本文档已经经过反复修改,最终包含大量的方法帮助优化c语言代码。编译器是很好的,但是它们并不能按照你的意愿做所有事情,因此,我希望帮助大家从自己的代码中获取最好的性能比。本教程面向的不是初学者,而是更有经验的程序员.

 

声明

取决于你的特殊硬件环境和编译环境,下面列出来的一些技巧实际上可能使得你的代码运行更慢。就像现代的编译器一样,使用或者没有使用这些优化技术在低层次上都可能运行得很好。改善整体的算法能比局部代码优化取得更好的结果。本文档最初只是作为个人的一个备用笔记,并没有考虑成为C语言优化专题方面的权威性的文章。文章中我也可能存在一些错误,如果你有任何建议或者只是一些建设性的批评,与我取得联系。

 

编码速度

如何从你的C语言代码中获取最后的一点T-states???考虑的出发点应该是最快的执行速度而不是代码的可读性。

文章列出的源码没有进行语法检测,只是就事论事。当应用达到低级别的例程的时候,你应该已经过滤掉任何不良数据。

 

*数组索引

*别名

*寄存器

*整型数据类型

*循环干扰

*动态循环展开

*更快的for循环

*switch语句

*常指针使用

*提早退出循环

*杂项

 

 

1.使用数组索引

如果你希望根据某个变量的值变化把一个变量设置为一个特殊的字符(译注:其他的数据也行),也许你会这样做:

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';
else
    letter = 'U';

然而一种更紧凑(并且更快)的方法是使用变量作为一个字符串数组的索引,上面的语句修改成这样的也许更好:

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

2.别名

考虑下面的语句:

void func1(int *data ){
    for(int i = 0; i < 10; ++i)
        somefunc2(*data, i);
}

尽管“*data”可能永远也不会改变,但是编译器不知道 somefunc2()没有改变它,因此,当该变量被使用的时候程序必须每次从内存中读取它它也可能是一些在其他地方改变的变量的别名。如果你知道该变量不会被改变,你能按照下面的方法编码:

void func1(int *data ){
    int localdata = *data;
    for(i = 0; i < 10; ++i)
    somefunc2(localdata, i);
}

这样做的话将为编译器提供更大的机会进行优化。

 

3.整形数据类型和寄存器变量使用

如果你知道整数变量在之后的使用过程中不可能出现负数,那么你可以考虑使用无符号整数替代有符号正数.相对于有符号正数,一些处理器对无符号正数的算术运算处理速度更快.

因此,对一个应用于循环计数器的整形变量来说,最好的声明应该如下:

register unsigned int var_name;

还有一点需要记住的,整型算术运算比浮点型算术运算要快很多.应为整形数据通常能被处理器直接处理,而浮点型数据需要依赖外部的FPUs单元或者外部浮点数学函数库.如果你的运算结果只需要精确到小数点后两位,运算前把所有的浮点型数据*100,到最后再转换成浮点型数据.(虽然并没有机制保证编译器不会忽略“register”关键字,”unsigned”可能对一些处理器而言不会产生任何区别,但这样做确实是一种好的方法)

 

4.循环干扰

当一个循环足够的时候不要使用2个循环:

for(int i = 0; i < 100; ++i)
{
    stuff();
}

for(int i = 0; i < 100; ++i)
{
    morestuff();
}

下面的这种方式会更好写(生产的代码也要短):

for(i = 0; i < 100; ++i)
{
    stuff();
    morestuff();
}

需要注意的是,然而,如果在循环中有大量的工作,它可能不能适合你的处理器指令缓存.在这种情况下,因为每一个循环能完全在缓存总运行,两个独立的循环实际上可能执行速度更快.

 

5. 循环展开和动态循环展开

众所皆知,展开循环能产生相当大的开销节省.例如:

for(int i = 0; i < 3; ++i)
    something(i);

上面语句的效率就比下面语句的效率要低一些:

something(0);
something(1);
something(2);

因为第一种做法在每一次循环中都要检测循环条件和循环计数器i的自加.当循环中给出固定的循环次数,编译器常常能够自动展开第一种形式的循环成第二种形式.但是像下面的语句:

for(int i = 0; i < limit; ++i)
    something();

就不太可能由编译器自动展开,因为我们并不知道循环迭代多少次.然而,展开这种循环是可能的,并从中节省一些时间开销.“Graphic Gems”系列的书中就有这样的例子.这种方式不仅能加速图形渲染中行扫描的像素显示,而且可以适用于涉及到大量数据处理且操作方式相同的任何情况下.

下面的代码(列表1)的规模比一个简单的循环要大很多 ,但是却执行效率更高.

8为块大小只是用来作为演示目的任意合适的块大小都行你只需要重复循环中的内容同样多次数.

在下面的例子中,循环条件被设置为每8次重复调用然后才迭代执行一次,而不是函数调用一次迭代一次.

假如你知道你要处理的数组的大小,你可以设置块大小为数组的大小(或者能够被数组大小所整除).

然而我发现以8为块大小是一个很合适的大小我在Sun Sparcstation上使用该优化技术进行一些性能测试块大小为8或者16的时候效果很好但是当我把块大小提高到32的时候执行效率却下降了.(我认为这与机器的缓存大小有很大关系)

为对一张很大的单色位图(大概3000*2000像素点)进行求反操作,我在图像处理应用中使用的类似的代码,结果显示执行速度比使用简单的for()循环有显著的提高.因为处理器主要是专注于数据处理的工作,而不是循环次数的自加和循环条件的判断.

列表

#include<stdio.h>
#define BLOCKSIZE (8)

int main(void)
{
    int limit = 35; /* could be anything */
    int i = 0;
    int blocklimit = (limit / BLOCKSIZE) * BLOCKSIZE;

    while (i < blocklimit)
    {
        printf(“process(%d)n”, i++);
        printf(“process(%d)n”, i++);
        printf(“process(%d)n”, i++);
        printf(“process(%d)n”, i++);
        printf(“process(%d)n”, i++);
        printf(“process(%d)n”, i++);
        printf(“process(%d)n”, i++);
        printf(“process(%d)n”, i++);
    }

    if (i < limit)
    {
        switch (limit – i)
        {
            case 7:
                printf(“process(%d)n”, i++);
            case 6:
                printf(“process(%d)n”, i++);
            case 5:
                printf(“process(%d)n”, i++);
            case 4:
                printf(“process(%d)n”, i++);
            case 3:
                printf(“process(%d)n”, i++);
            case 2:
                printf(“process(%d)n”, i++);
            case 1:
                printf(“process(%d)n”, i);
        }
    }
    return 0;
}

6.更快的for循环

一般来说你会按照下面的方式来编写一个简单的for循环:

for(i = 0; i < 10; ++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–作为判断条件能够得到更快的处理.i是非零是的话,递减,继续循环.

而对于原始代码(第一种方式),处理器必须首先进行10i进行相减然后判断结果是否为非零,是的话,递增,继续循环(比第二种情况多一条相减运算语句).在一些规模小的循环中,这会产生很大的区别.

第二种语法看起来有点奇怪,但是这是合法的.循环中的第3个语句是可选的(无限循环可以写成“for( ; ; )”).下面的代码能产生同样的效果:

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

或者进一步扩展为:

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

唯一需要时刻注意的是循环终止条件是计数器值最终为0(如果你的循环计数变量值是从80递减50,这种方式是不会生效的),并且循环计数器是递减的.如果代码依赖于递增循环,这种方式也是不会起作用的.

 

7.使用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-else判断语句块考虑把最可能发生的情况放在最前面.if语句块中,如果最后一种情况需要执行,所有的先前条件都会测试一次.switch帮我们节省了这些额外的工作.

 

8.指针

无论什么时候通过引用的方式传结构体(例如,传递结构体指针), 否则的话整个结构体内容将要拷贝到栈中作为值传递这样将会放慢执行速度(我曾经见过有程序员把好几Mb的结构体内容的按值传递给调用函数,然而,事实上只需要通过一个简单的指针就能做同样一件事情.)

如果接受结构体指针参数的函数体不会改变结构体的内容,那么应该把该参数申明为指向const内容的指针.

void print_data(const bigstruct *data_pointer)
{
    print_contents_of_struct();
}

这个示例告诉编译器函数体不改变外部结构体的内容,不需要在每次访问结构体变量的时候重读结构体内容.同时,这样也可以防止你的代码意外的修改只读结构体内容.

9.提前退出循环

完整地执行循环体常常是一种多次一举,反而会降低执行效率.例如,如果你在一个大数组中查找一个特殊的数据项,一旦你找到你需要的数据项,尽快退出循环吧.

例如下面的语句用于在一个大小为10000个数组中看是否有-99这个数:

#define FALSE (0)
#define TRUE (1)

int found = FALSE;
for(int i = 0; i < 10000; ++i)
{
    if(-99 == list[i])
        found = TRUE;
    if(found)
        printf("Yes, there is a -99. Hooray!n");
}

这种方式能够得到想要的结果,但是不管查找到的数据在数组的位置而执行了整个for循环.

int found = FALSE;
for(int i = 0; i < 10000; ++i)
{
    if(-99 == list[i])
    {
        found = TRUE;
        break;
    }
}

if(found)
{
    printf("Yes, there is a -99. Hooray!n");
}

如果我们要查找的-99这个数据在数组的第23号位置,循环应该在第23次循环的时候终止,而不去执行剩下的9977次迭代.

 

10.杂项

一般来说,可以通过增加内存的使用来提高程序执行速度.例如,如果你能够缓存经常需要的数据而不是重复地计算或者加载它,这种方法就能生效.这样的例子很多,例如sine/cosine ,伪随机数表(在开始的时候一次性计算1000,然后重用它)

避免在循环表达式中使用++或者等之类有二义性的语句.例如while(n–){};这种方式很难被编译器优化.

尽可能少的使用全局变量.

声明文件内的函数或者变量等为static,除非你打算将它用于全局范围内.

如果可以的话使用word-size大小的变量,因为处理器能更好的处理这些数据(而不是char,short,double,bitfields等数据类型)

不要使用递归.递归函数看起来很华丽和紧凑,但是它会使用很多函数调用,而这些函数调用将占据很大的开销.

避免在循环中使用sqrt函数求平方根计算平方根很耗CPU.计算量大,函数执行周期长.

单维数组比多维数组执行速度更快.

编译器常常能优化整个文件避免分隔一些相关性高的函数到不同的文件中.如果编译器能发现两个相关性高的单元在一块,优化的机会更大(例如,它可能把代码设置为内联).

10 单精度数学运算比双精度运算速度更快通常编译器有针对于这个的一个开关.

11 浮点型数据乘法运算常常比除法运算更快推荐使用val*0.5替代val/2.0

12 加法运算比乘法运算要快推荐使用val+val+val替代val*3.

13 puts函数虽然灵活性差点,但是性能比printf函数要高.

14 使用#define宏来替代一些通用的小函数.—有时候,在一个规模小的循环中,大量CPU的使用被转移到成千上万次小的外部函数的调用中.使用宏替代函数来执行相同的工作将节省所有函数的调用开销,这样做同时还能使编译器更进一步优化.

15 二进制/无格式文件的访问比文本文件的访问要块.因为机器没必要在人易读的ASCII和机器易读的二进制之间进行转换.如果你实际上不需要读文件的数据,考虑把它写成二进制文件.

16 如果你的库支持mallopt()函数,使用它吧.MAXFAST设置能够使代码执行效率有显著的提高.如果一个特殊的结构体在短时间内反复创建和销毁,设置mallopt选项使它能够效率更好.

17 最后一点,但不是最不重要的一点,打开编译器优化.因为等到最后总是急冲冲将产品及时交给客户,这样做虽然显而易见,但是经常被遗忘.相对于在源码级执行的优化,编译器在更低一级能更进一步优化,并且能够针对目标处理器执行更具体的优化.

 

最后推荐再推荐一篇文章程序优化虽然是基于ARM处理器的但其大部分内容不失通用性.