代码优化-之-优化浮点数取整
[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 秒
////////////////////////////////////////////////////////////////////////////////
提示:为了避免浮点数到整数的转换可以考虑用定点数来表示小数,从而在需要取整的时候可
以用一个快速的移位指令来实现