首页 > 解决方案 > 如何处理浮点数的精度问题?

问题描述

我正在使用 Firebird 3.0.4(在 Windows 和 Linux 中)并且我有以下过程清楚地展示了我的浮点数问题,并且还展示了一种可能的解决方法:

create or alter procedure test_float returns (res double precision,
  res1 double precision,
  res2 double precision)
as

declare variable z1 double precision;
declare variable z2 double precision;
declare variable z3 double precision;

begin

  z1=15;
  z2=1.1;
  z3=0.49;
  res=z1*z2*z3; /* one expects res to be 8.085, but internally, inside the procedure
                   it is represented as 8.084999999999.
                   The procedure-internal representation is repaired when then
                   res is sent to the output of the procedure, but the procedure-internal
                   representation (which is worng) impacts the further calculations */
  res1=round(res, 2);
  res2=round(round(res, 8), 2);

  suspend;

end

On 可以看到程序的结果:

  select proc.res, proc.res1, proc.res2
  from test_float proc

结果是

RES     RES1    RES2
8,085   8,08    8,09

但是可以预期 RES2 应该是 8.09。

可以清楚地看到 res 的内部表示包含 8.0849999(例如可以将 res 分配给异常消息,然后引发此异常),它在输出期间被修复,但在进一步使用该变量时导致计算失败计算。

RES2演示修复:我可以随时申请ROUND(..., 8)修复内部表示。我已准备好采用此解决方案,但我的问题是 - 它是可接受的解决方法(当外部 ROUND 严格小于 5 位小数时)还是有更好的解决方法。

我的所有测试都通过这种解决方法通过,但感觉很糟糕。

当然,我知道每个程序员都应该知道的关于浮点数的最低限度(有一篇关于它的文章),而且我知道不应该使用 double 进行业务计算。

标签: sqlfloating-pointroundingfirebirdfirebird-3.0

解决方案


这是使用浮点数计算的固有问题,并非 Firebird 特有的。问题是15 * 1.1 * 0.49使用双精度数的计算并不完全是 8.085。事实上,如果你这样做8.085 - RES,你会得到一个(大约)的值1.776356839400251e-015(尽管你的客户可能只是将它显示为0.00000000)。

你会在不同的语言中得到类似的结果。例如,在 Java 中

DecimalFormat df = new DecimalFormat("#.00");
df.format(15 * 1.1 * 0.49);

也将8.08出于完全相同的原因产生。

此外,如果您更改操作顺序,您会得到不同的结果。例如 using15 * 0.49 * 1.1将产生8.085并舍入到8.09,因此实际结果将符合您的期望。

鉴于round本身也返回双精度,这并不是在 SQL 代码中处理此问题的好方法,因为具有更多小数位数的舍入值可能仍会产生略低于您预期的值,因为如何浮点数有效,因此即使您的客户端中的演示“看起来”正确,某些数字的双轮仍可能失败。

如果您纯粹出于演示目的而希望这样做,那么在您的前端执行此操作可能会更好,但您也可以尝试添加一个小值和强制转换为 的技巧decimal,例如:

cast(RES + 1e-10 as decimal(18,2))

然而,这仍然存在舍入问题,因为无法区分真正为 8.08499999999 的值(应向下舍入为 8.08)和计算结果恰好是浮点数为 8.08499999999 的值,而它将是 8.085精确数字(因此需要四舍五入到 8.09)。

以类似的方式,您可以尝试使用双重转换为decimal(例如cast(cast(res as decimal(18,3)) as decimal(18,2))),或转换decimal然后四舍五入(例如round(cast(res as decimal(18,3)), 2)。这将比双重四舍五入更一致,因为第一次转换将转换为精确的数字,但这又一次与上述类似的缺点。

虽然你不想听到这个答案,但如果你想要精确的数字语义,你不应该使用浮点类型。


推荐阅读