zoukankan      html  css  js  c++  java
  • C/C++性能优化 ZZ

    任务:把一个小头(little endian)的整型(32bit)转化为大头(big endian)。


    我们需要这样一个函数 void foo(unsigned int &u); 用来颠倒整数u的字节序。类似于socket函数htonl()或者ntohl()。也就是说,在以某个整数u为参数调用foo以后,u小头变大头,或者反过来。这无所谓,因为小头和大头是对称的。

    我发现对这个简单的任务,采用不同的作法,效率能差到很多,这两天研究了一下,写一点心得出来与同好分享。


    第一种作法:
    extern "C"  void f1(unsigned int &u)
    {
           unsigned int v = u;
           char *src = ((char *)&v + 3);
           char *dst = (char *)&u;

           *dst ++ = *src --;
           *dst ++ = *src --;
           *dst ++ = *src --;
           *dst ++ = *src --;
    }
    这是我最早想到的一种作法,也是最直观的作法。我当时的考虑是这样只有简单的赋值操作,避免了移位(>>或者<<),效率*应该*会比较高。但是测试的结果令人沮丧, 执行一千万次所需要的时间平均下来有390毫秒。

    为何如此?经过一番思索,我认为一定和内存访问有关。要知道v是一个局部变量,本来一个优化的编译器完全可以把v放入某个寄存器中,那么后续对v值的引用就无需再访问内存,但是注意到在上面的代码中,有一个对v求地址的操作: char *src = ((char *)&v + 3); 而寄存器是没有地址的,所以编译器只能选择为此生成效率较低的代码,也就是,把v放入堆栈。

    在优化打开的情况下,编译器会把src和dst放入寄存器而不是堆栈,所以这样一来,对于语句:
    *dst ++ = *src --;
    来说,需要访问两次内存。其中*src需要访问一次,得到其所指地址的值,然后再把这个值写回到*dst所指向的内存又是一次。。反汇编得到的代码也验证了这一点:

           pushl   %ebp
           movl    %esp, %ebp
           subl    $4, %esp
           movl    8(%ebp), %edx
           movl    (%edx), %eax
           movl    %eax, -4(%ebp)
           movzbl  -1(%ebp), %eax
           movb    %al, (%edx)
           movzbl  -2(%ebp), %eax
           movb    %al, 1(%edx)
           movzbl  -3(%ebp), %eax
           movb    %al, 2(%edx)
           movzbl  -4(%ebp), %eax
           movb    %al, 3(%edx)
           leave
           ret

    一共有13次内存访问的指令。

    这时候,我考虑如何让编译器把v变量放到寄存器里。根据上面的分析,很显然,办法是不要有对v求地址的操作,那么为了得到v各个byte的值,要执行移位动作就不可避免了。但是考虑到v在寄存器里,那么对它的移位操作也不过就是一条指令而已,比访问内存要快的多了。这样我就得到了第二种做法:

    第二种做法:
    extern "C"  void f2(unsigned int &u)
    {
           unsigned int v = u;
           char *dst = (char *)&u;

           *dst ++ = (v >> 24);
           *dst ++ = ((v >> 16) & 0xFF);
           *dst ++ = ((v >> 8) & 0xFF);
           *dst ++ = (v & 0xFF);
    }

    那么现在让我们假定v是某个寄存器,对于上面的4条赋值语句,每一条都只需要访问内存一次,看看反汇编生成的代码(v相当于ecx,而保存v移位生成的临时变量用的是eax):

           pushl   %ebp
           movl    %esp, %ebp
           movl    8(%ebp), %edx
           movl    (%edx), %ecx
           movl    %ecx, %eax
           shrl    $24, %eax
           movb    %al, (%edx)
           movl    %ecx, %eax
           shrl    $16, %eax
           movb    %al, 1(%edx)
           movl    %ecx, %eax
           shrl    $8, %eax
           movb    %al, 2(%edx)
           movb    %cl, 3(%edx)
           popl    %ebp
           ret

    只需要访问8次内存。测试的结果是喜人的,现在执行一千万次该函数调用,只需要200毫秒,效率几乎提高了一倍。看来消除访问内存的努力确实有效果。这时候代码中的dst指针又变成了目标,如果消除掉它改成寄存器访问,我们又可以减少4次内存引用,减去一次把寄存器内容写回u的访存指令,一共就可以减少3次内存访问。这样我就得到了第三个版本:



    第三种做法:
    extern "C" void f3(unsigned int &u)
    {
           unsigned int v = u;

           u = ((v >> 24) |
                   (((v >> 16) & 0xFF) << 8) |
                   (((v >> 8) & 0xFF) << 16) |
                   (v << 24));
    }

    首先反汇编:

           pushl   %ebp
           movl    %esp, %ebp
           pushl   %ebx
           movl    8(%ebp), %ebx
           movl    (%ebx), %ecx
           movl    %ecx, %eax
           movl    %ecx, %edx
           shrl    $8, %eax
           andl    $65280, %eax
           shrl    $24, %edx
           orl     %eax, %edx
           movzbl  %ch, %eax
           sall    $16, %eax
           orl     %eax, %edx
           sall    $24, %ecx
           orl     %ecx, %edx
           movl    %edx, (%ebx)
           popl    %ebx
           popl    %ebp
           ret

    因为有太多的临时变量,寄存器已经不够用了,编译器必须使用ebx,而ebx不属于“调用者保存”的寄存器。所以如果函数内部要使用它,必须自己保存再恢复,这样就多了两条push ebx和pop ebx的指令,那么这个函数需要访问内存7次,看上去不是很理想。不过测试结果却更加喜人,简直是令人惊异。一千万次函数调用,现在竟然只需要80毫秒!效率提高了一倍有余。我这里只能猜测第二种做法里面大量的movb,在32位的机器上,可能比movl要慢很多。否则这个现象很难解释。


    80毫秒的测试结果令我非常满意,因为最简单的函数:
    void simple(unsigned int &u)
    {
           ++ u;
    }
    调用一千万次都需要40几毫秒,我认为几乎已经是极限了,但是...事实显然并非如此。


    我们还有第四种做法:
    extern "C" void f4(unsigned int &u)
    {
           __asm__("bswap %0" : "=r" (u) : "0" (u));
    }

    从80486开始,为了方便网络程序的处理,主要就是htonl()和ntohl()啦,Intel特意添加了一条专门用来转换大头小头的指令,也就是 BSWAP ,它可以在一条指令中,完成上面我辛辛苦苦实现出来的全部功能,而且速度,你可以想象,应该和上面那个void simple(unsigned int &u)相当。事实也是如此,一千万次对f4()的调用,确实只需要40几毫秒。

    不过对我的需求来说,80毫秒的战绩已经很足够了。而引入内嵌汇编 BSWAP 来实现,有两个麻烦处,最主要的是不同的编译器,有不同的内嵌汇编格式,我主要用gcc和vc,维护两份汇编码太累,而且今后如果要和别的编译器兼容,也很讨厌。其二是这个指令只在80486以后才有,虽然我可以断定我的代码绝对不会运行在386上面:-),但是对于追求“形式完美”的程序员,比如说鄙人,来说,是不太能接受的:-)


    两个结论:
    1. 尽量以一种方便编译器优化的方式使用局部变量,比如说,不要对局部变量求地址。
    2. 尽量定义和机器字长相同的变量,正如上面所猜测的,movb比movl要慢很多。


    注1:测试结果中具体的的数值,会根据机器性能的不同而不同。但是在不同的机器上,4种方法所消耗时间的比例,应该大体上是一致的。
    注2:第四种方法来自于参考linux kernel中对htonl()函数的实现。


    附测试代码test.cpp,请使用gcc编译,带-O2选项:

    #include <iostream>
    #include <windows.h>

    extern "C"  void f1(unsigned int &u)
    {
           unsigned int v = u;
           char *src = ((char *)&v + 3);
           char *dst = (char *)&u;

           *dst ++ = *src --;
           *dst ++ = *src --;
           *dst ++ = *src --;
           *dst ++ = *src --;
    }

    extern "C"  void f2(unsigned int &u)
    {
           unsigned int v = u;
           char *dst = (char *)&u;

           *dst ++ = (v >> 24);
           *dst ++ = ((v >> 16) & 0xFF);
           *dst ++ = ((v >> 8) & 0xFF);
           *dst ++ = (v & 0xFF);
    }

    extern "C" void f3(unsigned int &u)
    {
           unsigned int v = u;

           u = ((v >> 24) |
                   (((v >> 16) & 0xFF) << 8) |
                   (((v >> 8) & 0xFF) << 16) |
                   (v << 24));
    }

    extern "C" void f4(unsigned int &u)
    {
           __asm__("bswap %0" : "=r" (u) : "0" (u));
    }

    int main()
    {
           using std::cout;
           using std::endl;

           const unsigned cnt = 100 * 100 * 100 * 10;
           unsigned int u = 1024;

           unsigned int tk = GetTickCount();
           for (unsigned i = 0; i < cnt; ++ i)
                   f1(u);
           tk = GetTickCount() - tk;
           cout << cnt << " times f1() cost " << tk << " ms" << endl;


           tk = GetTickCount();
           for (unsigned i = 0; i < cnt; ++ i)
                   f2(u);
           tk = GetTickCount() - tk;
           cout << cnt << " times f2() cost " << tk << " ms" << endl;


           tk = GetTickCount();
           for (unsigned i = 0; i < cnt; ++ i)
                   f3(u);
           tk = GetTickCount() - tk;
           cout << cnt << " times f3() cost " << tk << " ms" << endl;


           tk = GetTickCount();
           for (unsigned i = 0; i < cnt; ++ i)
                   f4(u);
           tk = GetTickCount() - tk;
           cout << cnt << " times f4() cost " << tk << " ms" << endl;

           return 0;
    }

    原帖:http://general.blog.51cto.com/927298/345740

  • 相关阅读:
    java 在线网络考试系统源码 springboot mybaits vue.js 前后分离跨域
    springboot 整合flowable 项目源码 mybiats vue.js 前后分离 跨域
    flowable Springboot vue.js 前后分离 跨域 有代码生成器 工作流
    Flowable 工作流 Springboot vue.js 前后分离 跨域 有代码生成器
    java 企业 网站源码 后台 springmvc SSM 前台 静态化 代码生成器
    java 进销存 商户管理 系统 管理 库存管理 销售报表springmvc SSM项目
    基于FPGA的电子计算器设计(中)
    基于FPGA的电子计算器设计(上)
    FPGA零基础学习:SPI 协议驱动设计
    Signal tap 逻辑分析仪使用教程
  • 原文地址:https://www.cnblogs.com/burellow/p/2375476.html
Copyright © 2011-2022 走看看