浮点数简介
32位浮点数都按照 IEEE 规则定义:
- 首位是符号位,零为正,一为负
- 8位指数位,真实指数加127为存储位
- 23位小数位
浮点数表示为$m \times b^e$,$m$ 称作为有效位,或尾数。$b$ 是基数,$e$ 是指数。
浮点数范围
32位浮点数指数位为8位,由于 0
和 11111111
指数位有特殊作用,所以只有 [1,254]
范围可以用。因此 $m \times b^e$中,$b$ 为 $2$,$e\in[-126,127]$。$m\in[1, 2-{2^{-23}}]$。所以最大值为:
$max = ({2-{2^{-23}}})\times{2^{127}}$
$min = -({2-{2^{-23}}})\times{2^{127}}$
浮点数精度
浮点数的小数位是归一化的,归一化的意思是小数位只存储自小数点后面的数。32位浮点数的最小精度为 $2^{-23} = 1.192\times{10^{-7}}$。七位有效数字,但随着整数位的增大,小数位的精度下降。比如整数位变为 $2^{23}=8388068$ 则小数位的精度变为 $1$,这时如果在做浮点数和小数的加法,则小数直接被忽略。
浮点数近似值
Intel 8086 处理器被设计用来处理整数运算。这对于使用浮点运算的图形和运算密集型软件是个问题。通过软件模拟浮点运算是可行的,但性能损失很严重。类似于软件AutoCad需要更加强有力的方式来进行浮点数学运算。Intel推出了单独浮点协处理芯片8087,并且随着每代处理器一起升级。直到Intel 486的发布,浮点硬件集成到了主CPU被称作FPU。
浮点数在寄存器里使用IEEE 10-byte 扩展实数格式。当FPU将运算结果放入到内存操作数时,它将结果转换为其中一种格式:整数,长整数,单精度浮点数,双精度浮点数,或者BCD。
浮点目标操作数有效位是有限的,当浮点计算有效位结果长度大于目标操作数有效位长度时,FPU 有以下四种逼近方式:
- 向最近的偶数舍入:舍入结果最接近无限精确结果。如果两个值非常接近,最终结果以偶数为准(最后一位为零)
- 向下舍入方向 -∞:舍入结果小于或等于无限精确结果
- 向上舍入方向 +∞:舍入结果大于或等于无限精确结果
- 向零舍入(将要舍去的位截断):舍入结果的绝对值小于或等于无限精确结果
FPU控制字包含两位称作RC 字段来标识那种舍入方法。字段值说明如下:
- 00 二进制:向最近的偶数舍入(默认)
- 01 二进制:向-∞方向舍入
- 10 二进制:向+∞方向舍入
- 11 二进制:向零舍入(截断)
向最近的偶数舍入是默认的舍入方法,此方法被认为是最精确以及被大多数程序实现的方式。
游戏中的浮点问题
游戏中要实现录像,或者网络同步功能,需要在不同的平台运行结果完全一致。如果所有平台使用相同的语言,以及相同的编译器,那浮点运算结果应给一致。但实际上每个平台都有自己的编译器,在每个平台上的CPU的浮点单元运算结果向CPU内存值转换时,存在取舍。IEEE 浮点规则只保证了浮点数的定义,但并有说明转换时的取舍问题。这导致不同平台之间浮点运算结果的差异。总结下来造成相同的代码,执行结果不一致的原因如下:
- 编译结果,不同平台编译结果如果不一致,不能保证运算结果一致,比如:
a + b + c
和a + (b + c)
的结果不一定一致 - 硬件平台,不同厂商的FPU运算寄存器长度不一致,这导致运算精度不可控
- 取舍问题,IEEE没有定义浮点取舍的标准,所以每家语言,平台都有自己的实现接口,这里不能保证统一
游戏开发中为了完成所有平台运算结果一致的问题,使用定点数代替浮点数来保证运算结果一致。
浮点数转换为字符串
游戏中经常需要显示浮点值,显示的内容为字符串。使用有效位123456
和1.
组合出一个值 1.123456
,然后再加上 $10^n$,最后结果为$1.123456\times 10^5$。如果指数位为零,直接使用结果$1.123456$。
参考
Demystifying Floating Point Precision
how-to-round-binary-fractions