现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

点滴经验:提高代码的运行效率

2016-06-21 06:13 工业·编程 ⁄ 共 6835字 ⁄ 字号 暂无评论
文章目录

(1) 用for(; ;) 代替while(1)

(2) 循环的时候首先进行内部数据的循环,然后进行外部数据的循环

(3) 同一层循环之内,尽量只安排同一数据的相关操作

(4) 编译的时候头文件不要相互包含,尽量简单

(5) 尽量不要使用乘除, 多用加减和移位操作

(6) 部分复制、计算操作可以用SIMD命令代替,比如 SSE命令等等

(7) 如果是服务器软件或者是游戏客户端软件,请多使用查询,少计算

(8) if() ...else()的时候,把最长出现的代码放在前面,不经常出现的结果放在后面

(9) 使用数组的时候,多使用int* p = &value[0]; p ++;迭代的形式, 这样可以减少数据的计算

(10) 优化算法,发挥当前CPU多核的优势,最大限速地发挥CPU的特性。

以下详细解读:

1、为什么需要用for(; ;)代替while(1)?

5:        int m = 0;
00401038   mov         dword ptr [ebp-4],0
6:
7:        while(1)
0040103F   mov         eax,1
00401044   test        eax,eax
00401046   je          test+32h (00401052)
8:        {
9:            if( m == 0)
00401048   cmp         dword ptr [ebp-4],0
0040104C   jne         test+30h (00401050)
10:               break;
0040104E   jmp         test+32h (00401052)
11:       }
00401050   jmp         test+1Fh (0040103f)
12:
13:      for(;;)
14:       {
15:           if( m == 0)
00401052   cmp         dword ptr [ebp-4],0
00401056   jne         test+3Ah (0040105a)
16:               break;
00401058   jmp         test+3Ch (0040105c)
17:       }
0040105A   jmp         test+32h (00401052)

可以很清楚地看出,while(1)被翻译成了三个命令,而for(;;)却没有。很多同学可能认为,这只是三条指令而已,没有什么大惊小怪的,但是我们要知道,很多循环都是上百万次的进行的,如果一般的函数都注意这个问题,那么一天节省下来的CPU时间是相当可观的。

2、先进行循环内部的计算,然后进行循环外面的计算

void loop_analyse()
{
    int m = GetTickCount();
    int inner = 0;
    int outer = 0;
    for(outer = 0; outer < 1000; outer ++)
    {
        for (inner = 0; inner < 1000; inner ++)
        {
            a[outer][inner] = inner;
        }

    } 
    printf("%d\n", GetTickCount() - m);

    m = 0;
    for(inner = 0; inner < 1000; inner ++)
    {
        for (outer = 0; outer < 1000; outer ++)
        {
            data[outer][inner] = inner;
        }

    }
    printf("%d\n", GetTickCount() - m);
}

    我们在VC6.0上面的测试结果是31、64。原因就是我们的数据在内存中间是按照先内存,然后再按照外层的顺序排列的。如果在计算的时候,我们首先使用了内部的数据,那么在cpu cache命中率上就会很高。相反,如果按照outer进行数据的遍历的话,那么就需要进行数据的不停跳转,在cpu cache上面也需要不停地进行刷新。在一旦cpu的cache命中率下降,就会重新将数据从内存加载到cpu的cache上面。等到循环得到一定的积累之后,就会在时间的运算上面发生很大的变化。两者之间的运行效率差异就会变得非常明显。

3、尽可能在循环的时候只运行本层的数据

int data1[10000000] = {0,1};
int data2[10000000] = {0,1};

void loop_layer_test()
{
     int m = GetTickCount();
     int outer = 0;
     int inner = 0;
     for(outer = 0; outer < 10000000; outer ++)
     {
          data1[outer] = outer;
          data2[outer] = outer;
     }
     printf("%d\n", GetTickCount() - m);

     m = GetTickCount();;
     for(inner = 0; inner < 10000000; inner ++)
     {
         data1[inner] = inner;
     }

     for(outer = 0; outer < 10000000; outer ++)
     {
          data2[outer] = outer;
     }
     printf("%d\n", GetTickCount() - m);
}

    在我的VC6.0测试的时候,两者的运行差别时间还是挺大的,有兴趣的朋友可以在自己的机器上面好好试一下,看看是不是效果显著。其实道理和上面的准则是差不多的,只不过我们这一次涉及的单层循环的东西,不过在本质上还是差别不是很大。

4、编译的时候头文件不要相互包含,尽量简单

    我们习惯于在头文件中包含很多其他的文件,不管他们对我们是有用还是没有用,殊不知这样会带来很大的麻烦。一方面,它会在我们修改头文件的时候造成麻烦,另外一方面会给我们的编译带来很多的麻烦。我们知道,一般的工程项目都有少则上百个,多则上千个上万个文件。如果在每一个文件上面节约一点编译的时间,那么久整个项目或者工程来说也是相当可观的。

5、尽量不要使用乘除,多使用移位操作

#define LOOP_MAX_NUMBER 10000000L
int data[LOOP_MAX_NUMBER] = {0, 1};
void test10()
{
    int m = GetTickCount();
    int inner = 0;
    int value = 10;
    for(inner = 0; inner < LOOP_MAX_NUMBER; inner ++)
    {
        data[inner] = inner / 16 + inner / 32 * 8;
    } 
    printf("%d\n", GetTickCount() - m);

    m = GetTickCount();
    for(inner = 0; inner < LOOP_MAX_NUMBER; inner ++)
    {
        data[inner] = inner >> 4 + (inner >> 5) << 3;
    }
    printf("%d\n", GetTickCount() - m);
}

6、所谓SIMD指令就是指用一条指令,完成多个字节数据的操作

static void mmx_memcopy(void* dst, void* src, int len)
{
     __asm
     {
         mov ecx, [len]
         test ecx, ecx
         mov eax, 63
         mov esi, [src]
         jle over

         and eax, ecx
         mov edi, [dst]
         sub ecx, eax
        jz  mov_remain

mov64bytes:
       add esi, 64
       add edi, 64
       sub ecx, 64
       movq mm0, [esi -64]
       movq mm1, [esi -64 + 8]
       movq [edi - 64], mm0
       movq [edi - 64 + 8], mm1
       movq mm2, [esi -64 + 16]
       movq mm3, [esi -64 + 24]
       movq [edi - 64 + 16], mm2
       movq [edi - 64 + 24], mm3
       movq mm4, [esi -64 + 32]
       movq mm5, [esi -64 + 40]
      movq [edi - 64 + 32], mm4
      movq [edi - 64 + 40], mm5
      movq mm6, [esi -64 + 48]
      movq mm7, [esi -64 + 56]
      movq [edi - 64 + 48], mm6
      movq [edi - 64 + 56], mm7
      ja mov64bytes

mov_remain:
      test eax, eax
      mov ecx, eax
      je over

      shr eax, 3
      cmp eax, 7
      je mov_remain56

      cmp eax, 6
      je mov_remain48

      cmp eax, 5
      je mov_remain40

      cmp eax, 4
      je mov_remain32

      cmp eax, 3
      je mov_remain24

      cmp eax, 2
      je mov_remain16

      cmp eax, 1
      je mov_remain8

      jmp mov_remain7

mov_remain56:
     movq mm0, [esi]
     sub ecx,8
     add esi,8
     movq [edi],mm0
     add edi, 8

mov_remain48:
     movq mm0, [esi]
     sub ecx,8
     add esi,8
     movq [edi],mm0
     add edi, 8

mov_remain40:
     movq mm0, [esi]
     sub ecx,8
     add esi,8
     movq [edi],mm0
     add edi, 8

mov_remain32:
     movq mm0, [esi]
     sub ecx,8
     add esi,8
     movq [edi],mm0
     add edi, 8

mov_remain24:
     movq mm0, [esi]
     sub ecx,8
     add esi,8
     movq [edi],mm0
     add edi, 8

mov_remain16:
     movq mm0, [esi]
     sub ecx,8
     add esi,8
     movq [edi],mm0
     add edi, 8

mov_remain8:
     sub ecx, 8
     movq mm0, [esi]
     movq [edi], mm0

     je over

     add esi, 8
     add edi, 8

mov_remain7:
     rep movsb

over:
     emms
}
}

     这是一个简单的内存拷贝代码,它和我们通常意义上的C拷贝代码还是有很大的不同的,因为是8个字节一起复制的,所以它使用了mm0~mm7共8个寄存器,对于剩下来的余数字节又是分别进行处理的,所以一旦拷贝的数据量很大,效果还是相当明显的。

7、尽量采用查询的方式,少采用计算的方式,注意分析经验数据

     做过服务器侧软件的朋友都知道,单个socket的响应时间要尽可能的短,因为这有这样我们才能在短时间内响应更多的用户。如果把宝贵的时间放在一些临时性的计算上面,那么就不大合算了。历史的经验运行数据对于我们进行数据分析是很重要。举一个例子来说:我们知道操作系统分配内存的时候,刚开始的访问要求都是混乱无章的。等到运行一段时间之后,我们的内存就会充斥着更多的内存碎片。此时此刻,即使我们需要获取一块较大的内存都是比较困难的。为了获得我们需要的内存,操作系统不得不把操作系统中暂时使用不到的内存临时放到硬盘上面,等到重新需要的时候再次加载进来,这在无形之中就增加了运行时间,降低了运行效率。如果我们能够从经验数据中得到启发,知道应该在机器开机启动的时候就知道怎样进行数据的分配,短时间内实现机器的最优状态,就可以节省时间,还提高了用户的满意程度。
  

8、if-else语句的合理使用

    比如说存在下面一个语句
[cpp] view plain copy
if(a && b) 

     /* 代码处理 */ 
}else{ 
     /* 代码处理 */ 

   我们想要处理a && b都为真的情况比较复杂一些。只有a和b都为真的时候,我们才能进入到相应的模块进行处理。但是一般来说,a和b出现的频率是不一样的。此时此刻,我们一般出现频率较高的a放在前面。因为a一旦判断失败,我们就不再需要判断,b的结果对我们此时来说是没有意义的。因此合理调整a和b的顺序也会在无形之中提高我们的运行效率。和这段代码类似的另外一段代码是:
[cpp] view plain copy
if(a || b) 

     /* 代码处理 */ 
}else{ 
     /* 代码处理 */ 

    上面这段代码的基本原理和前一段代码是一样的。我们需要把经常出现的情况放在前面,因为一旦a成立,b条件的判断就变得无足轻重了。

9、多使用指针

    指针是一把双刃剑,用好了可以事半功倍,我们可以做一个小的测试:
[cpp] view plain copy
char* strncpy(char* dst, const char* src, size_t size) 

    char* pAddress = dst; 
    size_t count = 0; 
    if(NULL == dst || NULL == src || 0 == size) 
    { 
        return NULL; 
    } 
 
    while(count < size) 
    { 
        dst[count] = src[count]; 
        count++; 
    } 
 
     return pAddress; 
     

    上面的这段话,我们完全可以写成这样的形式:
[cpp] view plain copy
char* strncpy(char* dst, const char* src, size_t size) 

    char* pAddress = dst; 
    if(NULL == dst || NULL == src || 0 == size) 
    { 
        return NULL; 
    } 
 
    while(size-- > 0) 
    { 
        *dst ++ = *src ++; 
    } 
 
     return pAddress; 

    因为不管是什么样的数组,都要经过起始地址、偏移值、取值三个步骤。而指针是比较直接的,通过不断的指针递增,可以省略中间取首地址、偏移数值的过程,快速实现数值的复制。

10、优化算法是提高代码效率很重要的一个环节

    我们都知道快速排序、堆排序是解决排序问题的重要方法,但是它需要很大的内存,主要来自于堆栈的不确定性。所以如果开发排序功能且数据比较庞大的话,我们完全有机会实现这样的优化。另外一个算法的优化就是我们可以把平时运行过程中不是特别紧急的任务降低运行的优先级,比如说可以干一会休息一会,10秒钟就sleep一下,这样就可以把宝贵的时间留给业务的处理。当然,这种优化与其说是算法的优化,不如说是流程的优化。

后注:
    优化是以牺牲代码的可移植性、降低代码的阅读性作为代码的。代码的优化也不仅仅是以上的提到的几种方法。就我个人的经验而言,我觉得可以从下面几个方面综合进行考虑:a)提高硬件的配置。只要银子允许,我们就可以在短期内实现效率的大幅提升,当然它的提高也是有局限性的。b)优化流程,我们的代码经常需要添加新的功能,就会在不知不觉当中添加很多判断、申请很多资源,甚至重复做一些无意义的操作,这在很大程度上降低了我们代码的运行效率,如果把握的好,重新对项目或者版本进行功能上的梳理,随之而来的效率提升甚至不亚于第一种方法,这是一种降低成本的好方法,当然需要软件架构师的精确把握和控制。c)提高代码编写技巧,优化代码,这方面需要时间,也需要进行总结。做得好效果显而易见,做得不好反而降低了原来的工作效率。

作者:平凡的程序员

给我留言

留言无头像?