【问题标题】:Double precision is different in different languages双精度在不同语言中是不同的
【发布时间】:2021-04-20 20:01:49
【问题描述】:

我正在尝试各种编程语言中双精度值的精度。

我的程序

main.c

#include <stdio.h>

int main() {
    for (double i = 0.0; i < 3; i = i + 0.1) {
        printf("%.17lf\n", i);
    }
    return 0;
}

main.cpp

#include <iostream>

using namespace std;

int main() {
    cout.precision(17);
    for (double i = 0.0; i < 3; i = i + 0.1) {
        cout << fixed << i << endl;
    }
    return 0;
}

main.py

i = 0.0
while i < 3:
    print(i)
    i = i + 0.1

Main.java

public class Main {
    public static void main(String[] args) {
        for (double i = 0.0; i < 3; i = i + 0.1) {
            System.out.println(i);
        }
    }
}

输出

main.c

0.00000000000000000
0.10000000000000001
0.20000000000000001
0.30000000000000004
0.40000000000000002
0.50000000000000000
0.59999999999999998
0.69999999999999996
0.79999999999999993
0.89999999999999991
0.99999999999999989
1.09999999999999990
1.20000000000000000
1.30000000000000000
1.40000000000000010
1.50000000000000020
1.60000000000000030
1.70000000000000040
1.80000000000000050
1.90000000000000060
2.00000000000000040
2.10000000000000050
2.20000000000000060
2.30000000000000070
2.40000000000000080
2.50000000000000090
2.60000000000000100
2.70000000000000110
2.80000000000000120
2.90000000000000120

main.cpp

0.00000000000000000
0.10000000000000001
0.20000000000000001
0.30000000000000004
0.40000000000000002
0.50000000000000000
0.59999999999999998
0.69999999999999996
0.79999999999999993
0.89999999999999991
0.99999999999999989
1.09999999999999987
1.19999999999999996
1.30000000000000004
1.40000000000000013
1.50000000000000022
1.60000000000000031
1.70000000000000040
1.80000000000000049
1.90000000000000058
2.00000000000000044
2.10000000000000053
2.20000000000000062
2.30000000000000071
2.40000000000000080
2.50000000000000089
2.60000000000000098
2.70000000000000107
2.80000000000000115
2.90000000000000124

main.py

0.0
0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999
1.0999999999999999
1.2
1.3
1.4000000000000001
1.5000000000000002
1.6000000000000003
1.7000000000000004
1.8000000000000005
1.9000000000000006
2.0000000000000004
2.1000000000000005
2.2000000000000006
2.3000000000000007
2.400000000000001
2.500000000000001
2.600000000000001
2.700000000000001
2.800000000000001
2.9000000000000012

Main.java

0.0
0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999
1.0999999999999999
1.2
1.3
1.4000000000000001
1.5000000000000002
1.6000000000000003
1.7000000000000004
1.8000000000000005
1.9000000000000006
2.0000000000000004
2.1000000000000005
2.2000000000000006
2.3000000000000007
2.400000000000001
2.500000000000001
2.600000000000001
2.700000000000001
2.800000000000001
2.9000000000000012

我的问题

我知道double 类型本身存在一些错误,我们可以从Why You Should Never Use Float and Double for Monetary CalculationsWhat Every Computer Scientist Should Know About Floating-Point Arithmetic 等博客中了解更多信息。

但这些错误不是随机的!每次错误都是相同的,因此我的问题是为什么这些对于不同的编程语言会有所不同?

其次,为什么 Java 和 Python 的精度误差是一样的? [Java 的 JVM 是用 C++ 编写的,而 python 解释器是用 C 编写的]

但令人惊讶的是,它们的错误是相同的,但与 C 和 C++ 中的错误不同。为什么会这样?

【问题讨论】:

  • 您应该确保打印相同的位数以进行公平比较。 0.100000000000000010.100000 可以表示相同的值,如果第二种情况是用更少的数字打印的。
  • 没有“错误”。十进制数的二进制表示需要近似值。这一切都是确定性和理解的。
  • “为什么会这样?” - TBH,一个很好的答案是“为什么不呢?”
  • 当然,这些差异是由于这些语言打印值的方式不同,而不是因为存在不同的值,还是我遗漏了什么?
  • @JaysmitoMukherjee 您一直假设打印值的差异意味着底层表示的差异,但事实并非如此。正如我之前所说,为了证明您的论文,您需要查看双精度的二进制表示,而不是打印的十进制表示。

标签: java python c++ c precision


【解决方案1】:

输出的差异是由于将浮点数转换为数字的差异。 (通过数字,我的意思是字符串或其他代表数字的文本。“20”、“20.0”、“2e+1”和“2•102”是相同数字的不同数字。)

作为参考,我在下面的注释中显示了i 的确切值。

在 C 中,您使用的 %.17lf 转换规范要求小数点后 17 位,因此产生小数点后 17 位。然而,C 标准允许在这方面有所松懈。它只需要计算出足够多的数字来区分实际的内部值。1 其余的可以用零(或其他“不正确”的数字)填充。您使用的 C 标准库似乎仅完全计算 17 位有效数字,并用零填充您请求的其余部分。这解释了为什么你得到“2.90000000000000120”而不是“2.90000000000000124”。 (注意“2.90000000000000120”有18位:小数点前1位,后16位有效位,1位无意义的“0”。“0.10000000000000001”小数点前有审美的“0”,后有17位有效位. 17位有效数字的要求是为什么“0.10000000000000001”结尾必须有“1”,而“2.90000000000000120”可能有“0”。)

相比之下,您的 C++ 标准库似乎完成了全部计算,或者至少完成了更多计算(这可能是由于 C++ 标准中的一条规则2),因此您得到“2.90000000000000124”。

Python 3.1 added an algorithm 转换为与 Java 相同的结果(见下文)。在此之前,对于显示转换是松懈的。 (据我所知,在算术运算中使用的浮点格式和对 IEEE-754 的一致性仍然松懈;具体的 Python 实现可能在行为上有所不同。)

Java 要求从double 到字符串的默认转换产生just as many digits as are required to distinguish the number from neighboring double values(也是here)。所以它产生“.2”而不是“0.20000000000000001”,因为最接近 .2 的双精度值是 i 在该迭代中的值。相比之下,在下一次迭代中,算术中的舍入误差使i 的值与最接近 0.3 的双精度值略有不同,因此 Java 为其生成了“0.30000000000000004”。在下一次迭代中,新的舍入误差正好部分抵消了累积误差,所以又回到了“0.4”。

注意事项

当使用 IEEE-754 binary64 时,i 的确切值是:

0 0.10000000000000000055511151231257827021181583404541015625 0.2000000000000000011102230246251565404236316680908203125 0.30000000000000000444089209850062616169452667236328125 0.400000000000000002220446049250313080847263336181640625 0.5 0.59999999999999997779553950749686919152736663818359375 0.6999999999999999555910790149937383830547332763671875 0.79999999999999993338661852249060757458209991455078125 0.899999999999999911182158029987476766109466552734375 0.99999999999999988897769753748434595763683319091796875 1.0999999999999998667732370449812151491641998291015625 1.1999999999999999555910790149937383830547332763671875 1.3000000000000000444089209850062616169452667236328125 1.4000000000000001332267629550187848508358001708984375 1.5000000000000002220446049250313080847263336181640625 1.6000000000000003108624468950438313186168670654296875 1.7000000000000003996802888650563545525074005126953125 1.8000000000000004884981308350688777863979339599609375 1.9000000000000005773159728050814010202884674072265625 2.000000000000000444089209850062616169452667236328125 2.10000000000000053290705182007513940334320068359375 2.200000000000000621724893790087662637233734130859375 2.300000000000000710542735760100185871124267578125 2.400000000000000799360577730112709105014801025390625 2.50000000000000088817841970012523233890533447265625 2.600000000000000976996261670137755572795867919921875 2.7000000000000010658141036401502788066864013671875 2.800000000000001154631945610162802040576934814453125 2.90000000000000124344978758017532527446746826171875

这些值与将 0、.1、.2、.3、... 2.9 从十进制转换为二进制 64 得到的值并不完全相同,因为它们是通过算术产生的,因此初始转换存在多个舍入错误,并且连续添加。

脚注

1 C 2018 7.21.6.1 仅要求生成的数字在指定意义上精确到 DECIMAL_DIG 数字。 DECIMAL_DIG 是这样的位数,对于实现中任何浮点格式的任何数字,将其转换为具有DECIMAL_DIG 有效数字的十进制数,然后返回浮点数会产生原始值。如果 IEEE-754 binary64 是您的实现支持的最精确格式,那么其DECIMAL_DIG 至少为 17。

2 除了合并 C 标准之外,我在 C++ 标准中没有看到这样的规则,因此您的 C++ 库可能只是使用了与 C 库不同的方法选择题。

【讨论】:

  • “20”、“20.0”不是同一个数字。
  • @nicomp:当然不是同一个数字,因为“20”和“20.0”是字符串,不是数字。它们是代表同一个数字二十的数字。或者您可能将2020.0 视为源代码中编程语言属性类型的文本。在这种情况下,它们表示编程语言中具有不同类型的概念元素,但这些元素仍然表示相同的数字,即 20。
  • 它们不代表相同的数字。 20 是整数,20.0 是浮点数,小数点后有一位精度。
  • 我相信用于浮点表示的 CPython 规则是“往返”的“最佳”(最少数字),意思是 float(str(x)) == x,这相当于 Eric 引用的 Java 规则。
  • FWIW 是 Python 3.1 改变了浮点表示算法,这记录在 docs.python.org/3/whatsnew/3.1.html#other-language-changes
【解决方案2】:

您看到的差异在于打印数据的方式,而不是数据本身。

在我看来,这里有两个问题。一个是您在以每种语言打印数据时并没有始终如一地指定相同的精度。

第二个是您将数据打印到 17 位精度,但至少与通常实现的一样(double 是具有 53 位有效位的 64 位数字)double 实际上只有大约 15 位小数的精度。

因此,虽然(例如)C 和 C++ 都要求您的结果“正确”四舍五入,但一旦超出了它应该支持的精度限制,它们就不能保证每次都能产生真正相同的结果可能的情况。

但这只会影响打印出来的结果看起来,而不影响它在内部的实际存储方式。

【讨论】:

    【解决方案3】:

    我不了解 Python 或 Java,但 C 和 C++ 都没有坚持要求双精度值的打印的十进制表示尽可能精确或简洁。因此,比较打印的十进制表示并不能告诉您有关正在打印的实际值的所有信息。两个值在二进制表示中可能相同,但在不同语言(或同一语言的不同实现)中仍然可以合法地打印为不同的十进制字符串。

    因此,您的打印值列表并没有告诉您发生了任何异常情况。

    您应该做的是打印您的双精度值的确切二进制表示。

    一些有用的读物​​。 https://www.exploringbinary.com/

    【讨论】:

      【解决方案4】:

      但这些错误不是随机的!

      正确。这应该是意料之中的。

      为什么不同的编程语言会有不同?

      因为您对输出的格式设置不同。

      为什么Java和Python的错误是一样的?

      它们似乎具有相同或足够相似的默认格式。

      【讨论】:

      • 现在所有格式都相似但仍然不同
      • @JaysmitoMukherjee now all are formatted similarly 如果是这种情况,请将代码放入问题中。
      • 里面都打印到17位
      • @JaysmitoMukherjee 为什么你认为 System.out.println(i);print(i) 打印 17 位数字?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-05-23
      • 2020-05-28
      • 1970-01-01
      相关资源
      最近更新 更多