代码优化-之-优化浮点数取整

                           代码优化-之-优化浮点数取整
                        [email protected]  2007.05.19

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 秒
////////////////////////////////////////////////////////////////////////////////

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


 

你可能感兴趣的:(优化,算法,测试,float,Delphi,编译器)