代碼優化-之-優化浮點數取整
[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 <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的取整方式的代碼放到循環體外面從而加快了速度
{
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向零取整
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
}
}
////////////////////////////////////////////////////////////////////////////////
//速度測試:
//==============================================================================
// ftol_test_fpu 0.407 秒
////////////////////////////////////////////////////////////////////////////////
SSE3增加了一條FPU取整指令fisttp,和fistp指令功能幾乎相同(我的電腦上經過測試速度也相同),但默認向0取整,和RC場設置無關,所以使用fisttp的代碼就可以不管RC場了,有利於簡化代碼和優化性能;
C:利用浮點數的編碼格式來“手工”處理浮點數到整數的轉換(利用了IEEE浮點編碼格式)
{
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 秒
{
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浮點編碼格式)
{
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指令
{
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 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 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 <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 秒
////////////////////////////////////////////////////////////////////////////////
提示:爲了避免浮點數到整數的轉換可以考慮用定點數來表示小數,從而在需要取整的時候可
以用一個快速的移位指令來實現