【问题标题】:Why do C and Java round floats differently?为什么 C 和 Java 对浮点数进行舍入不同?
【发布时间】:2020-01-06 18:30:57
【问题描述】:

考虑浮点数 0.644696875。让我们使用 Java 和 C 将其转换为具有八位小数的字符串:

Java

import java.lang.Math;
public class RoundExample{
     public static void main(String[] args){
        System.out.println(String.format("%10.8f",0.644696875));
     }
}

结果:0.64469688

自己试试吧:http://tpcg.io/oszC0w

C

#include <stdio.h>

int main()
{
    printf("%10.8f", 0.644696875); //double to string
    return 0;
}

结果:0.64469687

自己试试吧:http://tpcg.io/fQqSRF

问题

为什么最后一位数字不同?

背景

数字 0.644696875 不能完全表示为机器编号。它表示为分数 2903456606016923 / 4503599627370496,其值为 0.6446968749999999

这无疑是一个边缘案例。但我真的很好奇差异的来源。

相关:https://mathematica.stackexchange.com/questions/204359/is-numberform-double-rounding-numbers

【问题讨论】:

  • 请注意,这个问题可能与二进制表示无关,而是printf 函数在每种语言中的工作方式。
  • 默认的舍入行为是一个有点随意的选择。 Java seems to use banker's rounding,GNU libc seems to round to the nearest integer by default
  • 要点是:Java 将使用具有最少位数的值,这将唯一标识该值。这是0.644696875,将四舍五入为0.64469688。相反,C 将使用精确的 double 值,大约是 0.64469687499999999997363220316515253....;这是四舍五入向下0.64469687
  • @Ctx - 什么,不,你说的都不对。 Java 和 C 都使用 uhh 52 位整数乘以 2 的 11 位整数指数的幂。仅当将此表示形式转换为供人类使用的字符串时,才会出现十进制数字,除非您使用的是任意精度的类,否则内部不涉及十进制数字。
  • @Ctx:是的,使用四舍五入的 C 实现(C 标准比 Java 标准更宽松,不需要这样做)比 Java 实现更准确,因为根据我的在下面回答,Java 在格式化时需要 两次 舍入操作,这会增加错误。

标签: java c floating-point printf


【解决方案1】:

结论

在这种情况下,Java 规范需要麻烦的双舍入。数字 0.6446968749999999470645661858725361526012420654296875 首先转换为 0.644696875,然后四舍五入为 0.64469688。

相比之下,C 实现只是将 0.6446968749999999470645661858725361526012420654296875 直接四舍五入为八位,生成 0.64469687。

预赛

对于Double,Java 使用 IEEE-754 基本 64 位二进制浮点。在这种格式中,最接近源文本中数字 0.644696875 的值是 0.6446968749999999470645661858725361526012420654296875,我相信这是要格式化为 String.format("%10.8f",0.644696875) 的实际值。1

Java 规范是怎么说的

The documentation for formatting with the Double type and f format 说:

... 如果精度小于Float.toString(float)Double.toString(double) 分别返回的字符串中小数点后出现的位数,则该值将使用四舍五入算法进行四舍五入。否则,可能会附加零以达到精度……

让我们考虑“…Double.toString(double) 返回的字符串”。对于数字 0.6446968749999999470645661858725361526012420654296875,此字符串为“0.644696875”。这是因为 Java 规范规定 toString produces just enough decimal digits to uniquely distinguish the number within the set of Double values,而“0.644696875”在这种情况下只有足够的数字。2

这个数字在小数点后有九位,"%10.8f" 要求八位,所以上面引用的段落说“值”是四舍五入的。它是指哪个值——format 的实际操作数,即 0.6446968749999999470645661858725361526012420654296875,还是它提到的字符串“0.644696875”?由于后者不是一个数值,我本来希望“值”指的是前者。但是,第二句说“否则 [即,如果请求更多数字],可能会附加零……”如果我们使用 format 的实际操作数,我们将显示它的数字,而不是使用零。但是,如果我们将字符串作为数值,那么它的十进制表示在其中显示的数字之后将只有零。因此,这似乎是预期的解释,Java 实现似乎符合这一点。

因此,要使用"%10.8f" 格式化此数字,我们首先将其转换为 0.644696875,然后使用四舍五入规则对其进行四舍五入,得到 0.64469688。

这是一个糟糕的规范,因为:

  • 需要两次舍入,会增加误差。
  • 四舍五入发生在难以预测和难以控制的地方。某些值将在小数点后两位四舍五入。有些会在 13 之后四舍五入。程序无法轻易预测或调整。

(另外,很遗憾他们写了“可能”附加的零。为什么不“否则,零被附加以达到精确度”?对于“可能”,似乎它们是给实现一个选择,虽然我怀疑他们的意思是“可能”是基于是否需要零来达到精度,而不是实现者是否选择附加它们。)

脚注

1 当源文本中的0.644696875 转换为Double 时,我相信结果应该是Double 格式中可表示的最接近的值。 (我没有在 Java 文档中找到它,但它符合 Java 要求实现行为相同的哲学,我怀疑转换是按照 Double.valueOf(String s) 完成的,does require this。)最近的 Double 到0.644696875 是 0.6446968749999999470645661858725361526012420654296875。

2 由于位数较少,七位数的 0.64469687 是不够的,因为最接近它的 Double 值是 0.6446968699999999774519210404832847416400909423828125。所以需要八位数字才能唯一区分0.6446968749999999470645661858725361526012420654296875

【讨论】:

  • “应该”怎么样?这更符合当今的标准写作。
【解决方案2】:

这里发生的可能是他们使用稍微不同的方法将数字转换为字符串,这会引入舍入错误。也有可能在编译期间将字符串转换为浮点数的方法在它们之间有所不同,这同样会由于舍入而给出稍微不同的值。

但请记住,float 的分数有 24 位精度,大约为 7.22 位十进制数字 [log10(2)*24],前 7 位数字在它们之间是一致的,所以它只是最后几个最不重要的数字不同的位。

欢迎来到有趣的浮点数学世界,2+2 并不总是等于 4。

【讨论】:

  • 十进制数字的 .22 是什么?
  • @JL2210 在浮点术语中,一个非常不准确的表示会在大约 2.2 个不同的值之间摆动,而不是正常的 10,或者换句话说,它只有 22% 的准确率,而不是 100%。跨度>
  • 不涉及float(32 位二进制浮点)。如果有,则值为 0.644696891307830810546875,我们会看到“0.64469689”——以 9 结尾,而不是 8 或 7。
  • 2+2 在 Java 使用的所有浮点格式中始终为 4。添加两个不完全是 2 的值有时可能不会产生正好 4,但这适用于大多数数值系统。如果你看到两种可能,但都是迷信的层面,不回答也没关系(其实Java转十进制和大部分C编译器不同(1),转十进制和大多数 C 编译器 (1))。
  • (1) 大多数 C 编译器,包括 tpcg.io 上的那个
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-07
  • 2010-09-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多