计算机浮点数处理带来的问题

JerryZhang 2016/07/28

业务场景:

  1. 用户用 10 元 购买三张券,每一个张券可以直接抵一款产品,平均下来一张券的价值为: 10 * 3 = 3.333333333...

  2. 假设用户购买产品 A 用了两张劵,A 的进价为 2 元。那么 A 的利润为 3.3333333... * 2 - 2 = 4.66666666666...

  3. 实际应用过程中,券不止可以抵同一款产品,所以服务器没有办法判断某一张券抵了那一款产品,必须要前端(App/Web) 手动填充产品的等价售价

这种无线循环的小数,客户端是没有办法填充的,只能精确到「分」,即 3.33。这样在利润统计的过程中就会少了 0.00666666666...,系统运行时间一长,就会导致财务对账对不上。

为了解决 10 * 3 = 3.333333333 这种高精度运算引发的订单无法结算的问题,决定手动进行截断,即: 10 * 3 = [3.3, 3.3, 3.4]。但实际程序处理时发现 10 - 3.3 - 3.3 = 3.4000000000000004 而不是 3.4。本以为这是 Python 语言的问题,因为之前用 C++/Java/C# 这几类语言都没有遇到过这个问题。

经过一番研究后才想明白,这是计算机二进制存储本身的问题,和语言无关。** 二进制无法精确的存储浮点数,所以浮点数才有「浮点数精度」这样的概念。比如 0.3 这样的数值,计算机实际存储的可能是 0.3 无限近似的值,近似的能力取决于计算机本身,而取出来的值到底是多少取决于编程语言的浮点数精度 ** 。因为 Python 的 float 精度高,所以 10 - 3.3 - 3.3 = 3.4000000000000004。C++ 的 float 精度是 6 位,自然而然 3.4000000000000004 被取值为 3.4,Java/C# 原因类似,所以我们看到"没问题"的假象。

这同时也解释了,为什么我们系统中很多没有除法运算的浮点数也有很长的尾序列。

既然如此,浮点数的带来的误差是没有办法解决的了,我们只能降低误差。我想的解决方案是:

  1. 对于后端: 所有与钱相关的,只保留两位有效数字,再后面的小数,计算机和语言爱怎么处理怎么处理,我们都不关心。还是按照我们现在的思路: 10 * 3 = [3.33, 3.33, 3.34],对所有实际处理的结果做「分」的四舍五入。
  2. 对于前端(App/Web): 同样保留两位有效数字(判等的时候也需要考虑),后端传给前端的数值假定为: 3.4000000000000004,前端按照 3.40 来处理就可以了。

0.01 这样的误差是不可接受的,累加成百上千次账目上就表现的很明显了。而 0.0000000000000001 这样的误差是可以接受的,这要累加到门店倒闭估计都不会出问题。