为什么 C 和 Java 轮轮浮动方式不同?爪哇岛C问题背景

2022-09-02 13:08:29

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

爪哇岛

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


答案 1

结论

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

相反,C 实现只是将 0.6446968749999999470645661858725361526012420654296875 直接舍入为八位数字,生成 0.64469687。

预赛

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

Java规范是怎么说的

使用 Double 类型和 f 格式进行格式化的文档说:

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

让我们考虑“返回的字符串...".对于数字 0.6446968749999999470645661858725361526012420654296875,此字符串为“0.644696875”。这是因为 Java 规范规定 toString 生成的十进制数字刚好足以唯一区分 Double 值集中的数字,而“0.644696875”在这种情况下只有足够的数字。阿拉伯数字Double.toString(double)

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

因此,要将此数字的格式设置为 ,我们首先将其转换为 0.644696875,然后使用舍入半向上规则将其舍入,该规则产生 0.64469688。"%10.8f"

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

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

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

脚注

1.当在源文本转换为时,我认为结果应该是最接近的格式中可表示的值。(我没有在Java文档中找到它,但它符合Java的理念,即要求实现具有相同的行为,我怀疑转换是根据 完成的,这确实需要这样做。最接近 0.644696875 的是 0.6446968749999999470645661858725361526012420654296875。0.644696875DoubleDoubleDouble.valueOf(String s)Double

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


答案 2

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

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

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