执行数值精度转换的机器指令

来源:互联网 发布:取消trunk端口 编辑:程序博客网 时间:2024/05/03 10:17
    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         4
    4. 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]
 };