代碼優化-之-優化浮點數取整

                           代碼優化-之-優化浮點數取整
                       
[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 秒
////////////////////////////////////////////////////////////////////////////////

提示:爲了避免浮點數到整數的轉換可以考慮用定點數來表示小數,從而在需要取整的時候可
以用一個快速的移位指令來實現


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章