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

2022-09-02 03:28:10

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

我的程序

main.c

#include <stdio.h>

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

主要.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

主要.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

主要.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

主要.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

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

其次,为什么Java和Python中的精度错误是相同的?[Java的JVM是用C++编写的,而python解释器是用C语言编写的]

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


答案 1

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

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

在 C 中,您使用的转换规范要求在小数点后 17 位,因此生成小数点后 17 位。但是,C标准允许在这方面有一些松弛。它只需要计算足够的数字,就可以区分实际的内部值。1 其余的可以用零(或其他“不正确”的数字)填充。您使用的 C 标准库似乎仅完全计算 17 位有效数字,并用零填充您请求的其余部分。这就解释了为什么你得到“2.900000000000000120”而不是“2.900000000000000124”。(请注意,“2.9000000000000000120”有 18 位数字:小数点前 1 位,小数点后 16 位有效数字,1 位非有效值“0”。“0.1000000000000000001”在小数点前有一个美学上的“0”,在小数点后有一个17位有效数字。对 17 位有效数字的要求就是为什么“”0.1000000000000000001“末尾必须有”1“,而”2.90000000000000000120“可能有”0”。%.17lf

相比之下,看起来您的C++标准库执行完整计算,或者至少执行更多计算(这可能是由于C++标准2中的规则),因此您将获得“2.900000000000000124”。

Python 3.1添加了一个算法来转换与Java相同的结果(见下文)。在此之前,对显示的转换很松懈。(据我所知,在算术运算中使用的浮点格式和符合IEEE-754方面仍然松懈;特定的Python实现可能在行为上有所不同。

Java 要求从 到字符串的默认转换生成与区分数字和相邻双精度值所需的数字数一样多(也在此处)。因此,它生成“.2”而不是“0.2000000000000000001”,因为最接近的双精度 .2 是该迭代中的值。相反,在下一次迭代中,算术中的舍入错误给出的值与最接近的双精度.3的值略有不同,因此Java为其生成了“0.300000000000000000004”。在下一次迭代中,新的舍入错误碰巧部分取消了累积的错误,因此它又回到了“0.4”。doubleii

笔记

使用 IEEE-754 二进制 64 时的确切值为: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 仅要求生成的数字在指定意义上精确到数字。 是位数,对于实现中任何浮点格式的任何数字,将其转换为具有有效数字的十进制数,然后返回浮点数将生成原始值。如果 IEEE-754 binary64 是您的实现支持的最精确的格式,则它至少为 17。DECIMAL_DIGDECIMAL_DIGDECIMAL_DIGDECIMAL_DIG

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


答案 2

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

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

第二种是,您将数据打印到17位精度,但至少按照通常的实现(即具有53位有效值的64位数字)实际上只有大约15位小数位的精度。doubledouble

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

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