• 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

  • 相关阅读:
    mysql备份
    Linux 配置参考
    oracle补丁安装
    多智能体城市交通计算综合应用
    基于纳什均衡的多智能体强化学习交通信号控制
    多智能体强化学习在城市交通信号控制中的研究与应用- 笔记
    Sarsa与Q-learning
    Pandas笔记
    Python学习笔记
    Game Theory and Multi-agent Reinforcement Learning笔记 下
  • 原文地址:https://www.cnblogs.com/burellow/p/2375476.html
Copyright © 2020-2023  润新知