浮点数的精度和转换
float(24bits,有效位数,不包括指数部分和符号位,下同)和double(53bits)类型,指的是浮点数在内存中存储精度。而在FPU中,却存在着三种运算精度:single precision(24bits),double precision(53bits),double extended precision(64bits)。FPU的默认精度是53bits的double precision。D3D的CreateDevice函数会将FPU的运算精度改成24bits,除非指定了D3DCREATE_FPU_PRESERVE参数。
除了运算精度之外,FPU的Control Word中还有一个叫做RC的字段,控制浮点转整型的转换方式,共有四种转换方式:
1. round to nearest(even) 取整后的差值最小,即四舍五入方式,如前后的差值相等则取向偶数,如3.5则取得偶数4,2.5则取得偶数2。这是FPU的默认取整方式。
2. round down 向负无穷大方向取整(ceil函数),如3.5取得3,-3.5取得-4
3. round up 向正无穷大方向取整(floor函数),如3.5取得4,-3.5取得-3
4. round toward zero(truncate) 向零的方向取整(浮点数转整数),如3.5取得3 ,-3.5取得-3
一个32位的整数,如果转成float型存储在内存中,就有可能导致误差,因为float只有24位有效位;而转成double型存储在内存中,则不会导致误差,但是当FPU的运算精度为single precision时,一旦通过FPU进行了运算,就有可能导致误差。64位的int在double precision时也有类似的问题存在,因此猜想64位CPU上FPU的默认运算精度应该是double extended precision才对。
在游戏中,由于考虑到D3D的性能,我们的浮点运算精度是24bits的。同时,Lua的内部没有区分整型和浮点型,而统一采用了double类型来存储数值。如果仅仅是存储32bits的整型数据,那么不会造成任何问题。但是,一旦需要进行运算,则支持的整数范围其实已经降到了-2^24至2^24。
接下来分析以下6种转换过程:
1. dword->int 直接复制内存
2. int->dword 直接复制内存 3. dword->double mov eax,dword ptr [dwNumber]
mov dword ptr [ebp-134h],eax
mov dword ptr [ebp-130h],0
fild qword ptr [ebp-134h]
fstp qword ptr [fNumber]
4. double->dword
fld qword ptr [fNumber]
fnstcw word ptr [ebp-12Eh] //*
movzx eax,word ptr [ebp-12Eh]
or eax,0C00h
mov dword ptr [ebp-134h],eax
fldcw word ptr [ebp-134h] //*这段将取整方式切换为向零取整
fistp qword ptr [ebp-13Ch]
fldcw word ptr [ebp-12Eh]
mov eax,dword ptr [ebp-13Ch]
mov dword ptr [dwNumber],eax
5. int->double
fild dword ptr [nNumber]
fstp qword ptr [fNumber]
6. double->int
fld qword ptr [fNumber]
call @ILT+215(__ftol2_sse) (4110DCh)
mov dword ptr [dwNumber],eax
fld指令从内存中将一个4字节(single)、8字节(double)、10字节(double extended)的浮点数压入FPU的浮点寄存器栈中
fstp指令从FPU的浮点寄存器栈中弹出一个浮点数,依据目标内存空间的大小转换成对应的精度,存入指定的内存地址
fild指令从内存中将一个2字节(word)、4字节(dword)或8字节(qword)带符号整数转换成一个浮点数并压入FPU的浮点寄存器栈
fistp指令从FPU的浮点寄存器栈中弹出一个浮点数,并转换成2字节(word)、4字节(dword)或8字节(qword)带符号整数存入指定的内存地址;在这个转换过程中,当浮点数过大时,如果control word中的invalid operation位被置0,则触发异常(Unhandled exception at 0x00411799 in test.exe: 0xC0000090: Floating-point invalid operation.),且不会向目标内存中存入任何数值;如果被置1(默认为1),则屏蔽异常,同时向目标内存中存入一个表示无限大的整数值。例如当目标内存为dword时,过大的负数会被转化成-2^31,而过大的整数会被转换成2^31。
__ftol2_sse函数在pentium机器上,会利用sse中的cvttsd2si指令,将一个double precsion的浮点数转换成一个带符号的32位整数,采用向零取整(truncate)方式。
由于浮点数操作指令中的整型数值都是带符号的,因此3号和4号转换中的无符号整数需要特殊处理。
如dword->int->double->dword的转换不会导致任何问题,而dword->double->int->dword的转换中double->int就会因为数值过大而触发异常,或者存入了无限大整数值而导致错误。
再来看看6种转换的效率:
1. dword->int 1(CPU周期,下同)
2. int->dword 1 3. dword->double 44. double->dword 56
5. int->double 0
6. double->int 26
测试环境:迅驰1.7G,VS2005,RDSTC空转耗时234个周期,以上数字已经减去该时间
由于流水线存在并行计算,所以5号转换的时间没有表现出来,而且所有周期数并不是实际消耗的周期数,但是反应出的消耗大小关系应该是正确的。
运算速度的测试结果:
float(24) float(53) double(24) double(53)
+ 5 5 5 5
- 5 5 5 5
* 5 5 8 8
/ 17 31 17 31
sin 805 610 390 170
sqrt 720 510 290 80
double类型加上默认运算精度,是FPU运算速度最快的组合。至于为什么D3D要求float(24)的组合,可能和显卡的带宽有关系,毕竟会使得带宽占用减少一半,而这很可能是显卡的主要性能瓶颈。不知道咋测试显卡相关的性能指标,随便猜测一下罢了。
附:切换control word的代码
WORD temp;
__asm //打开invalid operation异常
{
fnstcw word ptr [temp]
mov ax,word ptr [temp]
and ax,0FCFEh
or ax,3Eh
and ax,0F3FEh
mov word ptr [temp],ax
fldcw word ptr [temp]
};
__asm //切换到single precision运算精度 { fnstcw word ptr [temp] mov ax,word ptr [temp] and ax,0FCFFh or ax,3Fh and ax,0F3FFh mov word ptr [temp],ax fldcw word ptr [temp] };