zoukankan      html  css  js  c++  java
  • 代码优化优化浮点数取整

    原文: http://blog.csdn.net/housisong/article/details/1616026

    tag: 浮点数转换为整数,fpu,sse,sse2,读缓冲区优化,代码优化,ftol,取整,f2l,ftoi,f2i,floattoint 
    摘要: 本文首先给出一个浮点数取整的需求,并使用默认的取整方式,然后通过尝试各种方法来优化它的速度;
      最终的浮点数取整实现速度甚至达到了初始代码的5倍(是vc6代码的18倍)!

    (注意: 文章中的测试结果在不同的CPU和系统环境下可能有不同的结果,数据仅作参考)

    (2007.06.08更新: 补充SSE3新增的FPU取整指令fisttp的说明)
    (2007.06.04更新: 一些修正、补充double取整、补充FPU的RC场说明)


    正文:
      为了便于讨论,这里代码使用C++,涉及到汇编优化的时候假定为x86平台;使用的编译器为vc2005;
      测试使用的CPU为AMD64x2 4200+,测试时使用的单线程执行;
      为了代码的可读性,没有加入异常处理代码;


    A: 需要优化的原始代码(使用了大量的浮点数到整数的转换)

    #include <stdio.h>
    #include 
    <stdlib.h>
    #include 
    <time.h>

    volatile long testResult; //使用一个全局域的volatile变量以避免编译器把需要测试的代码优化掉
    const long    testDataCount=10000000;
    const long    testCount=20;
    float         fSrc[testDataCount];
    #define asm __asm

    void ftol_test_0()
    {
        
    long tmp=0;
        
    for (long i = 0; i < testDataCount; ++i) 
            tmp 
    +=(long)fSrc[i];  //需要优化的浮点数取整
        testResult=tmp;
    }

    int main()
    {
        
    //inti
        for (long i=0;i<testDataCount;++i)
            fSrc[i]
    =(float)(rand()*(1.0/RAND_MAX)*(rand()-(RAND_MAX>>1))*rand()*(1.0/RAND_MAX));

        
    //test
        double start0=(double)clock();    
        
    for (long c=0;c<testCount;++c)
            ftol_test_0();
        start0
    =((double)clock()-start0)*(1.0/CLOCKS_PER_SEC);

        
    //out
        printf ("  Result = %ud   Seconds = %8.5f ",testResult,start0);

        
    return 0;
    }

    ////////////////////////////////////////////////////////////////////////////////
    //速度测试:        
    //==============================================================================
    // ftol_test_0                        1.047 秒  (VC6编译 3.64 秒):        
    //          (使用vc2005的SSE编译选项  “/arch:SSE” 0.437 秒)
    ////////////////////////////////////////////////////////////////////////////////

       一般编译器生成的浮点数转换为整数的指令序列都比预想的速度慢很多,它的性能代价很容易被人忽略;
    在VC6编译器下上面的代码需要运行3.64秒,代码先修改FPU的取整模式(RC场),完成取整后在恢复RC场;
    VC2006生成的代码在CPU支持SSE的时候会调用使用cvttsd2si指令实现的版本,从而加快了取整的速度,
    达到了1.047秒,快了很多!
    让我们来尝试继续优化这个含有大量取整操作的函数ftol_test_0;

    B: 最容易想到的就是用浮点协处理器(FPU)(也可以称作x87)来优化取整
    将设置FPU取整方式和恢复FPU的取整方式的代码放到循环体外面从而加快了速度

    void ftol_test_fpu()
    {
        unsigned 
    short RC_Old;
        unsigned 
    short RC_Edit;
        
    long isrc;
        asm
        {
            
    //设置FPU的取整方式  为了直接使用fistp浮点指令
            FNSTCW  RC_Old             // 保存协处理器控制字,用来恢复
            FNSTCW  RC_Edit            // 保存协处理器控制字,用来修改
            FWAIT
            OR      RC_Edit, 
    0x0F00    // 改为 RC=11  使FPU向零取整
            FLDCW   RC_Edit            // 载入协处理器控制字,RC场已经修改

            mov     ecx,testDataCount
            xor     eax,eax
            test    ecx,ecx
            jle     EndLoop

            lea     edx,[fSrc
    +ecx*4]
            neg     ecx
          StartLoop:
                fld     dword ptr [edx
    +ecx*4]
                fistp   isrc
                add     eax,isrc

              inc     ecx
              jnz     StartLoop

          EndLoop:
            
            mov  testResult,eax;
        
            
    //恢复FPU的取整方式
            FWAIT
            FLDCW   RC_Old 
        }
     
       //RC场占用第11、10bit位 用于控制舍入方向
       // RC=00 向最近(或偶数)舍入   RC=01 向下(负无穷大)舍入          
       // RC=10 向上(正无穷大)舍入   RC=11 向零舍入      
       //提示:一般的编程语言环境中,RC场都会设置为一个默认值(一般为RC=00),
       //  语言就可能利用这一点做快速的取整(比如Delphi中的round函数),但某些引入的
       //  第三方库或代码可能会修改该默认值,从而造成以前运行正确的程序出现异常情况

    ////////////////////////////////////////////////////////////////////////////////
    //速度测试:        
    //==============================================================================
    // ftol_test_fpu                      0.407 秒
    ////////////////////////////////////////////////////////////////////////////////

    SSE3增加了一条FPU取整指令fisttp,和fistp指令功能几乎相同(我的电脑上经过测试速度也相同),但默认向0取整,和RC场设置无关,所以使用fisttp的代码就可以不管RC场了,有利于简化代码和优化性能; 

    C:利用浮点数的编码格式来“手工”处理浮点数到整数的转换(利用了IEEE浮点编码格式)

        inline long _ftol_ieee(float f)
        { 
            
    long a         = *(long*)(&f);
            unsigned 
    long mantissa  = (a&((1<<23)-1))|(1<<23); //不支持非规格化浮点数
            long exponent  = ((a&0x7fffffff)>>23);
            
    long r         = (mantissa<<8>> (31+127-exponent);
            
    long sign      = (a>>31); 
            
    return ((r ^ (sign)) - sign ) &~ ((exponent-127)>>31);
        }

    void ftol_test_ieee()
    {
        
    long tmp=0;
        
    for (long i = 0; i < testDataCount; ++i) 
            tmp 
    +=_ftol_ieee(fSrc[i]);  
        testResult
    =tmp;
    }

    ////////////////////////////////////////////////////////////////////////////////
    //速度测试:        
    //==============================================================================
    // ftol_test_ieee                     0.828 秒
    ////////////////////////////////////////////////////////////////////////////////
     
    手工实现居然超过了VC2005的SSE实现(主要是VC2005的实现函数调用开销太大);

    如果能够允许存在误差的话,还有一个快速的取整算法(注意,该函数的结果和标准不完全相同):// ftol_test_ieee_M                   0.438 秒

    inline long ftol_ieee_M(float x) 

        
    static const float magic_f = (3<<21);
        
    static const long magic_i = 0x4ac00000;
        
    float ftmp=x+magic_f;
        
    return  (*((long*)&ftmp)-magic_i) >> 1
    }


    D:对于Double到整数的转换有一个超强的算法 (利用了IEEE浮点编码格式)

        inline long _ftol_ieee_MagicNumber(double x)  
        { 
            
    static const double magic = 6755399441055744.0// (1<<51) | (1<<52)
            double tmp = x;
            tmp 
    += (x > 0? -0.499999999999 : +0.499999999999//如果需要4舍5入取整就去掉这一行
            tmp += magic;
            
    return *(long*)&tmp;
        }
    void ftol_test_ieee_MagicNumber()
    {
        
    long tmp=0;
        
    for (long i = 0; i < testDataCount; ++i) 
            tmp 
    +=_ftol_ieee_MagicNumber(fSrc[i]);  
        testResult
    =tmp;
    }

    (警告:该算法要求FPU的计算精度为高精度模式,某些程序可能为了速度而将FPU改成了低精度模式,
    比如在D3D中会默认调整该设置)

    ////////////////////////////////////////////////////////////////////////////////
    //速度测试:        
    //==============================================================================
    // ftol_test_ieee_MagicNumber         1.813 秒
    ////////////////////////////////////////////////////////////////////////////////
    如果需要4舍5入取整,速度就能快出很多,降低到0.407秒


    ( ftol_test_ieee,ftol_test_ieee_MagicNumber的实现主要参考了:  云风的《_ftol 的优化》:

    http://blog.codingnow.com/2005/12/_ftol_opt.html
    http://www.flipcode.com/cgi-bin/fcarticles.cgi?show=64008    这里有改动)


    E:借鉴vc2005的SSE实现使用cvttss2si指令

    void ftol_test_sse()
    {
        asm
        {
            mov     ecx,testDataCount
            xor     eax,eax
            test    ecx,ecx
            jle     EndLoop

            lea     edx,[fSrc
    +ecx*4]
            neg     ecx
          StartLoop:
                cvttss2si   ebx,dword ptr [edx
    +ecx*4]
                add     eax,ebx

              inc     ecx
              jnz     StartLoop

          EndLoop:
            
            mov  testResult,eax;
        }
    }

    ////////////////////////////////////////////////////////////////////////////////
    //速度测试:        
    //==============================================================================
    // ftol_test_sse                      0.422 秒
    ////////////////////////////////////////////////////////////////////////////////

    F: cvttss2si是一个单指令单数据流的指令,我们可以使用它的单指令多数据流的版本:
    cvttps2dq指令;它能同时将4个float取整!

    long ftol_sse_expand16(float* psrc,long count16) 
    {
        
    long result;
        asm
        {
            mov     ecx,count16
            test    ecx,ecx
            jle     EndLoop
            
            pxor    xmm0,xmm0
            pxor    xmm1,xmm1
            mov     ecx,count16
            mov     edx,psrc
            lea     edx,[edx+ecx*4]
            neg     ecx
          StartLoop:  
    //一次循环处理16个float
                cvttps2dq   xmm2,xmmword ptr [edx+ecx*4]
                cvttps2dq   xmm3,xmmword ptr [edx
    +ecx*4+16]
                cvttps2dq   xmm4,xmmword ptr [edx
    +ecx*4+16*2]
                cvttps2dq   xmm5,xmmword ptr [edx
    +ecx*4+16*3]
                paddd       xmm2,xmm3
                paddd       xmm4,xmm5
                add     ecx,
    16
                paddd       xmm0,xmm2
                paddd       xmm1,xmm4

              jnz     StartLoop

          EndLoop:
            paddd       xmm0,xmm1

            movaps      xmm1,xmm0 
            movhlps     xmm1,xmm0
            paddd       xmm0,xmm1
            movaps      xmm2,xmm0 
            shufps      xmm2,xmm0,
    1
            paddd       xmm0,xmm2            
            
            movd       eax,xmm0
            mov        result,eax
        }
        
    return  result;
    }
    void ftol_test_sse_expand16()
    {
        
    long tmp=0;
        
    for (long i = 0; i < testDataCount; i+=2000)  
        {
            tmp
    +=ftol_sse_expand16(&fSrc[i],2000);//2000=16*125
        }
        
    //todo: 因为testDataCount是2000的倍数,所以这里不用处理边界了
        testResult=tmp;
    }

    ////////////////////////////////////////////////////////////////////////////////
    //速度测试:        
    //==============================================================================
    // ftol_test_sse_expand16             0.281 秒
    ////////////////////////////////////////////////////////////////////////////////

    G: 由于函数需要读取大量的数据来处理,所以可以考虑优化读缓冲区(也可以考虑使用显式预读指令)

    long ftol_sse_expand16_prefetch(float* psrc,long count16)
    {
        
    long result;
        asm
        {
            mov     ecx,count16
            test    ecx,ecx
            jle     EndLoop
            
            
    //预读
            mov     edx,psrc 
            lea     edx,[edx
    +ecx*4]
            neg     ecx
          ReadStartLoop:
                mov     eax,dword ptr [edx
    +ecx*4]
                add     ecx,
    16
              jnz     ReadStartLoop

            pxor    xmm0,xmm0
            pxor    xmm1,xmm1
            mov     ecx,count16
            neg     ecx
          StartLoop:
                cvttps2dq   xmm2,xmmword ptr [edx+ecx*4]
                cvttps2dq   xmm3,xmmword ptr [edx+ecx*4+16]
                cvttps2dq   xmm4,xmmword ptr [edx+ecx*4+16*2]
                cvttps2dq   xmm5,xmmword ptr [edx+ecx*4+16*3]
                paddd       xmm2,xmm3
                paddd       xmm4,xmm5
                add     ecx,16
                paddd       xmm0,xmm2
                paddd       xmm1,xmm4

              jnz     StartLoop

          EndLoop:
            paddd       xmm0,xmm1

            movaps      xmm1,xmm0 
            movhlps     xmm1,xmm0
            paddd       xmm0,xmm1
            movaps      xmm2,xmm0 
            shufps      xmm2,xmm0,
    1
            paddd       xmm0,xmm2            
            
            movd       eax,xmm0
            mov        result,eax
        }
        
    return  result;
    }
    void ftol_test_sse_expand16_prefetch()
    {
        
    long tmp=0;
        
    for (long i = 0; i < testDataCount; i+=2000
        {
            tmp
    +=ftol_sse_expand16_prefetch(&fSrc[i],2000);
        }
       testResult
    =tmp;
    }

    ////////////////////////////////////////////////////////////////////////////////
    //速度测试:        
    //==============================================================================
    // ftol_test_sse_expand16_prefetch    0.219 秒
    ////////////////////////////////////////////////////////////////////////////////

    H:补充Double的取整,完整测试源代码

    #include <stdio.h>
    #include 
    <stdlib.h>
    #include 
    <time.h>

    volatile long testResult; //使用一个全局域的volatile变量以避免编译器把需要测试的代码优化掉
    const long    testDataCount=10000000;
    const long    testCount=20;
    double         fSrc[testDataCount];
    #define asm __asm

    void dftol_test_0()
    {
        
    long tmp=0;
        
    for (long i = 0; i < testDataCount; ++i) 
        {
            tmp 
    +=(long)fSrc[i];  //需要优化的浮点取整
        }
        testResult
    =tmp;
    }


    void dftol_test_fpu()
    {
        unsigned 
    short RC_Old;
        unsigned 
    short RC_Edit;
        
    long isrc;
        asm  
    //设置FPU的取整方式  为了直接使用fistp浮点指令
        {
            FNSTCW  RC_Old             
    // 保存协处理器控制字,用来恢复
            FNSTCW  RC_Edit            // 保存协处理器控制字,用来修改
            FWAIT
            OR      RC_Edit, 
    0x0F00    // 改为 RC=11  使FPU向零取整     
            FLDCW   RC_Edit            // 载入协处理器控制字,RC场已经修改
        
    //}
        
    //asm
        
    //{
            mov     ecx,testDataCount
            xor     eax,eax
            test    ecx,ecx
            jle     EndLoop

            lea     edx,[fSrc
    +ecx*8]
            neg     ecx
          StartLoop:
                fld     qword ptr [edx
    +ecx*8]
                fistp   isrc
                add     eax,isrc

              inc     ecx
              jnz     StartLoop

          EndLoop:
            
            mov  testResult,eax;
        
    //}
        
    //asm  //恢复FPU的取整方式
        
    //{
            FWAIT
            FLDCW   RC_Old 
        }
    }

    inline 
    long dftol_ieee_MagicNumber(double x)  

        
    static const double magic = 6755399441055744.0// (1<<51) | (1<<52)
        double tmp = x;
        tmp 
    += (x > 0? -0.499999999999 : +0.499999999999//如果需要4舍5入取整就去掉这一行
        tmp += magic;
        
    return *(long*)&tmp;
    }


    void dftol_test_ieee_MagicNumber()
    {
        
    long tmp=0;
        
    for (long i = 0; i < testDataCount; ++i) 
            tmp 
    +=dftol_ieee_MagicNumber(fSrc[i]);  
        testResult
    =tmp;
    }



    void dftol_test_sse2()
    {
        asm
        {
            mov     ecx,testDataCount
            xor     eax,eax
            test    ecx,ecx
            jle     EndLoop

            lea     edx,[fSrc
    +ecx*8]
            neg     ecx
          StartLoop:
                cvttsd2si   ebx,qword ptr [edx
    +ecx*8]
                add     eax,ebx

              inc     ecx
              jnz     StartLoop

          EndLoop:
            
            mov  testResult,eax;
        }
    }

    long dftol_sse2_expand8(double* psrc,long count8)
    {
        
    long result;
        asm
        {
            mov     ecx,count8
            test    ecx,ecx
            jle     EndLoop
            
            pxor    xmm0,xmm0
            pxor    xmm1,xmm1
            mov     edx,psrc 
            lea     edx,[edx
    +ecx*8]
            neg     ecx
          StartLoop:
    //一次循环处理8个double
                cvttpd2dq   xmm2,xmmword ptr [edx+ecx*8]
                cvttpd2dq   xmm3,xmmword ptr [edx
    +ecx*8+16]
                cvttpd2dq   xmm4,xmmword ptr [edx
    +ecx*8+16*2]
                cvttpd2dq   xmm5,xmmword ptr [edx
    +ecx*8+16*3]
                paddd       xmm2,xmm3
                paddd       xmm4,xmm5
                add     ecx,
    8
                paddd       xmm0,xmm2
                paddd       xmm1,xmm4

              jnz     StartLoop

          EndLoop:
            paddd       xmm0,xmm1

            movaps      xmm1,xmm0 
            shufps      xmm1,xmm0,
    1
            paddd       xmm0,xmm1            

            movd       eax,xmm0
            mov        result,eax
        }
        
    return  result;
    }
    void dftol_test_sse2_expand8()
    {
        
    long tmp=0;
        
    for (long i = 0; i < testDataCount; i+=2000)  
        {
            tmp
    +=dftol_sse2_expand8(&fSrc[i],2000);//2000=8*256
        }
        
    //todo: 因为testDataCount是2000的倍数,所以这里不用处理边界了
        testResult=tmp;
    }


    long dftol_sse2_expand8_prefetch(double* psrc,long count8)
    {
        
    long result;
        asm
        {
            mov     ecx,count8
            test    ecx,ecx
            jle     EndLoop
            
            
    //预读
            mov     edx,psrc 
            lea     edx,[edx
    +ecx*8]
            neg     ecx
          ReadStartLoop:
                mov     eax,dword ptr [edx
    +ecx*8]
                add     ecx,
    8
              jnz     ReadStartLoop

            pxor    xmm0,xmm0
            pxor    xmm1,xmm1
            mov     ecx,count8
            neg     ecx
          StartLoop:
                cvttpd2dq   xmm2,xmmword ptr [edx
    +ecx*8]
                cvttpd2dq   xmm3,xmmword ptr [edx
    +ecx*8+16]
                cvttpd2dq   xmm4,xmmword ptr [edx
    +ecx*8+16*2]
                cvttpd2dq   xmm5,xmmword ptr [edx
    +ecx*8+16*3]
                paddd       xmm2,xmm3
                paddd       xmm4,xmm5
                add     ecx,
    8
                paddd       xmm0,xmm2
                paddd       xmm1,xmm4

              jnz     StartLoop

          EndLoop:
            paddd       xmm0,xmm1

            movaps      xmm2,xmm0 
            shufps      xmm2,xmm0,
    1
            paddd       xmm0,xmm2            
            
            movd       eax,xmm0
            mov        result,eax
        }
        
    return  result;
    }
    void dftol_test_sse2_expand8_prefetch()
    {
        
    long tmp=0;
        
    for (long i = 0; i < testDataCount; i+=2000
        {
            tmp
    +=dftol_sse2_expand8_prefetch(&fSrc[i],2000);
        }
       testResult
    =tmp;
    }

    int main()
    {
        
    //inti
        for (long i=0;i<testDataCount;++i)
            fSrc[i]
    =(float)(rand()*(1.0/RAND_MAX)*(rand()-(RAND_MAX>>1))*rand()*(1.0/RAND_MAX));

        
    //test
        double start0=(double)clock();    
        
    for (long c=0;c<testCount;++c)
            
    //dftol_test_0();   
            
    //dftol_test_fpu(); 
            
    //dftol_test_ieee_MagicNumber();  
            
    //dftol_test_sse2(); 
            
    //dftol_test_sse2_expand8(); 
            dftol_test_sse2_expand8_prefetch(); 
        start0
    =((double)clock()-start0)*(1.0/CLOCKS_PER_SEC);

        
    //out
        printf ("  Result = %ud   Seconds = %8.5f ",testResult,start0);

        
    return 0;
    }

    H:把测试结果放在一起

    ////////////////////////////////////////////////////////////////////////////////
    //速度测试:  编译器vc2005 CPU为AMD64x2 4200+ 单线程      
    //==============================================================================
    // ftol_test_0                        1.047 秒  (“/arch:SSE”0.437秒、VC6编译3.64秒)
    // ftol_test_fpu                      0.407 秒
    // ftol_test_ieee                     0.828 秒
    // ftol_test_ieee_MagicNumber         1.813 秒  (4舍5入取整0.407 秒)
    // ftol_test_sse                      0.422 秒
    // ftol_test_sse_expand16             0.281 秒
    // ftol_test_sse_expand16_prefetch    0.219 秒
    //==============================================================================秒
    //补充double的取整
    // dftol_test_0                       1.141 秒  (“/arch:SSE2”0.734秒、VC6编译3.675秒)
    // dftol_test_fpu                     0.719 秒
    // dftol_test_ieee_MagicNumber        1.688 秒  (4舍5入取整0.703 秒)
    // dftol_test_sse2                    0.734 秒
    // dftol_test_sse2_expand8            0.609 秒
    // dftol_test_sse2_expand8_prefetch   0.516 秒
    ////////////////////////////////////////////////////////////////////////////////

    提示:为了避免浮点数到整数的转换可以考虑用定点数来表示小数,从而在需要取整的时候可
    以用一个快速的移位指令来实现

     

  • 相关阅读:
    IDEA开发 Scala 项目
    mvn编译时绕过本地jar去maven仓库下载问题
    三角化(转载)
    分布式文件服务器介绍(转载)
    VSCode 设置侧边栏字体大小
    libLas编译
    OSG编译
    vcpkg.exe安装与应用
    OpenCASCADE编译
    gl2ps编译
  • 原文地址:https://www.cnblogs.com/nsnow/p/2104107.html
Copyright © 2011-2022 走看看