双精度在不同的语言中是不同的

Double precision is different in different languages

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

我的程序

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 Calculations and What Every Computer Scientist Should Know About Floating-Point Arithmetic.

等博客中了解更多信息

但这些错误不是随机的!每次错误都是一样的,因此我的问题是为什么不同的编程语言这些错误是不同的?

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

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

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

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

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

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

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

输出的差异是由于将浮点数转换为数字的差异。 (numeral,我指的是表示数字的字符串或其他文本。“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 到字符串的默认转换产生 (also )。所以它产生“.2”而不是“0.20000000000000001”,因为最接近的 .2 是 i 在该迭代中的值。相反,在下一次迭代中,算术中的舍入误差给了 i 一个与最接近的 .3 略有不同的值,因此 Java 为它产生了“0.30000000000000004”。在下一次迭代中,新的舍入误差正好抵消了部分累积误差,所以又回到了“0.4”。

备注

使用 IEEE-754 binary64 时 i 的确切值为:

0
0.1000000000000000055511151231257827021181583404541015625
0.200000000000000011102230246251565404236316680908203125
0.3000000000000000444089209850062616169452667236328125
0.40000000000000002220446049250313080847263336181640625
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 库中选择。

But these errors are not random!

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

why are these different for different programming language?

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

Why are the errors in Java and Python same?

它们似乎具有相同或非常相似的默认格式。

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

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

您应该做的是打印双精度值的精确 binary 表示。

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