目录
float的不精确表示
+0.5的舍入方法
该方法的漏洞
0.4999997f舍入的结果错误
以+0.4999997f改进舍入方法
可以用0.49999996、0.49999998或者0.49999999替换0.49999997吗?
在做舍入函数研究时,发现函数中实现四舍五入的trunc函数大概采用的逻辑是floor(x+0.4999997),而不是floor(x+0.5),下面对这个数值进行研究说明。
float的不精确表示
我们都知道二进制在有限的位上表示浮点值并不是每个都精确的,就像在有限的位数上,十进制不能准确地表示1/3。二进制在实数上能表示的数据分布大概规律如下:
越接近0的值表示得越精确,越大的数表示得越不精确,这是前提。
+0.5的舍入方法
舍入函数中truncate实现数学上的四舍五入,广泛被接受的方法是将x+0.5后并向下舍入(向下舍入对于非负数,小数置0;对于负数,小数置0后减1),这种做法对计算机来说,比判断x是否大于等于【小于等于x的最大整数与大于等于x的最大整数的中值】这个判断要简单得多。
该方法的漏洞
因为浮点表示不精确的特性,当2^22 < |x|时,单精度float浮点的IEEE 745(目前计算机普遍使用的标识浮点的格式)能表示的只有整数,即两个可表示的浮点之间间隔最小为1,并且间隔会随x的增大而增大。
在2^22 < |x| < 2^23时,间隔正好为1。
此时,假设有float值x=2^22+1.0,对其进行四舍五入的舍入预期值为x本身,但实际舍入会执行floor(x+0.5),但x+0.5这个值并不能用float精确表示,系统会对其进行舍入后再表示,此处的舍入是中值向偶数舍入的就近舍入,x+0.5正好是2^22+1.0与2^22+2.0的中值,向偶舍入则会舍入为2^22+2.0,对2^22+2.0执行floor依然是2^22+2.0,得到的结果与预期值不符。这种错误的结果会在2^22 < |x| < 2^23范围内所有的奇数值上发生(但对于更大绝对值的x则不会发生,因为最小间隔大于1,任何数字+0.5后依然达不到与上一个可精确表示数字的中值,所以x+0.5被实际表示为x,floor(x)=x,结果正确)。
0.4999997f舍入的结果错误
如何处理这个范围内的错误结果呢?
让我们考虑一个特殊的值x=0.4999997f,对x执行truncate,发现结果=1.0而不是0.0,但x明显更接近1.0。
这个错误同样是因为浮点表示不精确引起的,float以 0.4999999701976776123046875来不精确地表示0.4999997:
当这个值加上0.5时,数学上是0.9999999701976776123046875,但float同样不能精确地表示这个数,它能精确表示的下一个数是:
能精确表示的上一个数是1:
这个值正好是两者的中值:(0.9999999701976776123046875+1)/2=0.4999999701976776123046875
根据向偶舍入的就近舍入原则,系统以1.0来不精确地表示这个数:
所以就是floor(1.0)结果为1.0。
你可能会想,为什么float能精确表示0.4999999701976776123046875却不能精确表示0.9999999701976776123046875呢?
因为上面提到的,越靠近0表达地越精确。
为什么0.9999999701976776123046875正好落在能精确表达的两个数的中间呢?
因为float能精确表示的数是有规律的,假设0.49999997附近的最小间隔为a,比0.49999997大的数间隔会慢慢变成2a,4a……,0.99999997附近的最小间隔就是2a,效果如下:
所以对0.4999997执行四舍五入的结果为1.0。
以+0.4999997f改进舍入方法
这个数字却可以用来避免+0.5的舍入方法的漏洞,因为当2^22 < |x| < 2^23时,x+0.49999997达不到与上一个可精确表示数的中值(即x+0.5),所以x+0.49999997会就近舍入到x,floor(x)=x,结果正确,不会有+0.5的漏洞。
可以用0.49999996、0.49999998或者0.49999999替换0.49999997吗?
首先,0.49999996与0.49999998在float中的表示与0.49999997一样,所以在表示这一步就与0.49999997一样了,即可以用96与98替代97的。而0.49999999表示为0.5,显然会存在和+0.5的舍入方式一样的漏洞。
参考:
IEEE-754 Floating Point Converter