浮点数运算时出现的精度损失问题

浮点数运算时出现的精度损失问题

2019, Jan 19    

缘起

最近在做一个投资组合系统。由于系统里面涉及到大量金额的存储。
所以对保存金额的字段的类型就要符合两个要求:一是能存储足够大的金额,二是保证精度不损失。
不过和我合作的同事不同意,坚持用数值*100换成整数,保持精度。他的看法是,数值在网络传输的过程中会出现精度损失。
据说他们之前出现过用grpc之类的获取数据库字段的时候,出现精度损失的情况。
例如本来应该是58,获取到本地后就变成了57.9999999999999。
但是我觉得调用金额的地方众多,每次计算都要乘100,除100,非常麻烦。
如果一不小心,某个地方很可能就漏掉了,那么就出错了,而且这种地方很不好排查。
另外如果精度到时候提高到小数点后4位,那么所有乘除100的地方都要改成10000,那不麻烦了去嘛。何必自寻烦恼。
不过为了说服他,这里我们就要知道,为什么好好的58,会变成57.9999999999999呢?这真的是由于网络传输引起的吗?

原理

所以带着疑问,我们先Google下看,是不是别人也会有这种问题。果然,找到了一个帖子。
https://www.v2ex.com/t/92674
仔细看完帖子后,发现原来这是由于浮点数存储小数引起的。
里面贴了一个关于鸟哥的解答,大致内容就是讲我们要搞清楚浮点数的表示(IEEE 754), 有穷的小数,根据大部分计算机实现的IEEE 浮点数运算,
用二进制存储,按照浮点数的表示的方法,可能会是无穷数。

浮点数, 以64位的长度(双精度)为例, 会采用1位符号位(E), 11指数位(Q), 52位尾数(M)表示(一共64位).

符号位:最高位表示数据的正负,0表示正数,1表示负数。

指数位:表示数据以2为底的幂,指数采用偏移码表示

尾数:表示数据小数点后的有效数字.

这里的关键点就在于, 小数在二进制的表示, 关于小数如何用二进制表示, 大家可以百度一下, 我这里就不再赘述, 我们关键的要了解,
0.58 对于二进制表示来说, 是无限长的值(下面的数字省掉了隐含的1)..

0.58的二进制表示基本上(52位)是: 0010100011110101110000101000111101011100001010001111
0.57的二进制表示基本上(52位)是: 0010001111010111000010100011110101110000101000111101
而两者的二进制, 如果只是通过这52位计算的话,分别是:

0.58 -> 0.57999999999999996
0.57 -> 0.56999999999999995
至于0.58 * 100的具体浮点数乘法, 我们不考虑那么细, 有兴趣的可以看(Floating point), 我们就模糊的以心算来看… 0.58 * 100 = 57.999999999

那你intval一下, 自然就是57了….

可见, 这个问题的关键点就是: “你看似有穷的小数, 在计算机的二进制表示里却是无穷的”

小数的二进制表示,大家也可以去了解一下。 算法大致就是小数部分也会统一成 二进制数 * 2的n次方。 整数部分转成二进制基本上不会出错,但小数转的话呢,你不能保证每个数都乘2都能得到

实例: 22.625

               22 = (10110)2

               0.625 = (.101)2

               10110.101 = 1.0110101 x 2^4 (标准值)

符号位 = 0 (正数)

指数位 = 4 + 127  = 131 = 10000011

有效位 = 0110101[16个零]

短浮点数 = 0 10000011 0110101[16个零]  

如果将一个短浮点数值转换成十进制数值,应用如等式1-1。

数值 = (-1)^符号位x [1 + (有效位/2^23)] x 2^(指数位 –127)

解决

那么破案了。就是由于小数的存储方式,导致了精度的损失。那么有什么解决的办法呢? 针对当前这个案例,还是有办法的。数据库里存储的是decimal的类型, python拿到后,返回的就不是double类型了,而是 decimal 的类。<class ‘decimal.Decimal’> decimal是什么类型呢? https://docs.python.org/2/library/decimal.html
我猜应该是通过字段串格式去存储带小数的数字(入参需要是str或者int,不能是float)。 然后重载了运算符,如果用字符串,那么就不存在精度损失了。而且decimal类型跟python的普通number进行加减乘除,
得到的还是一个decimal类型。所以这应该能比较好的解决这个问题。

例如: python3:

>>> 0.58*100
57.99999999999999
    
    
from  decimal import Decimal
from  decimal import getcontext

d_context = getcontext()
d_context.prec = 6
print(d_context)

d = Decimal(0.58) * 100
print(type(d), d)
<class 'decimal.Decimal'> 58.000000