为什么 Rust 只使用 16 位有效数字进行 f64 相等性检查?

Why does Rust only use 16 significant digits for f64 equality checks?

我有以下 Rust 代码:

use std::f64::consts as f64;

fn main() {
    println!("Checking f64 PI...");
    // f64::PI definition: https://github.com/rust-lang/rust/blob/e1fc9ff4a794fb069d670dded1a66f05c86f3555/library/core/src/num/f64.rs#L240
    println!("Definition: pub const PI: f64 = 3.14159265358979323846264338327950288_f64;");
    println!("Print it:                       {:.35}", f64::PI);
    println!("Different after 16 significant digits ----------|                         ");
    println!("##############################################################################");
    println!("Question 1: Why do the digits differ after 16 significant digits when printed?");
    println!("##############################################################################");

    println!("PERFORM ASSERTIONS..."); 
    assert_eq!(f64::PI, 3.14159265358979323846264338327950288_f64); // 36 significant digits definition
    assert_eq!(f64::PI, 3.141592653589793_f64); // 16 significant digits (less then the 36 in definition)
    // compares up to here -------------|
    assert_eq!(f64::PI, 3.14159265358979300000000000000000000_f64); // 36 significant digits (16 used in equality comparison)
    assert_ne!(f64::PI, 3.14159265358979_f64); // 15 significant digits (not equal)

    println!("PERFORM EQUALITY CHECK..."); 
    if 3.14159265358979323846264338327950288_f64 == 3.14159265358979300000000000000000000_f64 {
        println!("BAD: floats considered equal even when they differ past 16 significant digits");
        println!("######################################################################");
        println!("Question 2: Why does equality checking use only 16 significant digits?");
        println!("They are defined using 36 significant digits so why can't we perform");
        println!("an equality check with this accuracy?");
        println!("######################################################################");
    } else {
        println!("GOOD: floats considered different when they differ past 16 significant digits");
        println!("NOTE: This block won't execute :(");
    }
}

我知道浮点运算可能很棘手,但想知道这种棘手是否也会影响 f64 的打印和执行相等性检查。这是上述代码的输出:

Checking f64 PI...
Definition: pub const PI: f64 = 3.14159265358979323846264338327950288_f64;
Print it:                       3.14159265358979311599796346854418516
Different after 16 significant digits ----------|                         
##############################################################################
Question 1: Why do the digits differ after 16 significant digits when printed?
##############################################################################
PERFORM ASSERTIONS...
PERFORM EQUALITY CHECK...
BAD: floats considered equal even when they differ past 16 significant digits
######################################################################
Question 2: Why does equality checking use only 16 significant digits?
They are defined using 36 significant digits so why can't we perform
an equality check with this accuracy?
######################################################################

一个f64,顾名思义,是以64位存储的。在这个固定数量的存储中,我们只能编码固定数量的数字(具体来说,其中 52 位专用于尾数)。如果您在浮点文字中使用更多数字,则存储在 f64 变量中的数字将四舍五入到可用位数中可表示的最接近的数字。对于 f64 这意味着我们总是可以精确地表示 15 位十进制数字,有时是 16 位。这解释了为什么有时数字看起来相等,即使您在源代码中使用了不同的浮点文字:这是因为在四舍五入到最接近的可表示数字之后, 他们是一样的。

打印不同数字的原因是一样的。该数字在存储时四舍五入为最接近的可表示数字,并在打印时再次转换回十进制。额外的数字源自二进制到十进制的转换,但小数点后 15 位或 16 位的数字大多没有意义——它们不携带有关所表示数字的任何附加信息。

请注意,其中 none 特定于 Rust。大多数现代编程语言都使用 IEEE 754-1985 标准来表示浮点数,因此它们的行为是相同的。如果你想要任意精度的算法,你通常需要使用一些库,例如rug crate.

您首先假设传递所有这些双文字 3.14159265358979323846264338327950288_f643.141592653589793_f643.14159265358979_f64 实际上会将这些精确值分配给您的变量。这个假设是不正确的。

尽管 rust 源代码的作者使用数学常数的前 36 位实际数字来定义 f64::PI,但使用 IEEE 754 浮点格式存储的实际 64 位值是不同的。最接近的 IEEE 754 64 位浮点值 according to the online converter will be 0x400921FB54442D18, which can be approximated using the number 3.1415926535897931159979634685 when converted back to decimal。当您将 IEEE 754 值 0x400921FB54442D18 转换为十进制数时,您会得到相同的值。

换句话说:

What we wanted to store: 3.14159265358979323846264338327950288 
What is actually stored: 3.14159265358979311599796346854...

也许更简单的可视化方法是想象有一个虚构的数据类型,它可以存储从 0 到 1 的正实数,并且在内部使用字符串(字符数组)表示,最大长度为 12 个字符。所以,你采用这个奇怪的 96 位类型并创建 5 个变量:

strdouble A = 0.333333;       // internally stored as x = { .raw = "0.333333000" }
strdouble B = 0.333333333;    // internally stored as x = { .raw = "0.333333333" }
strdouble C = 0.333333333333; // internally stored as x = { .raw = "0.333333333" }
strdouble D = 0.333333333444; // internally stored as x = { .raw = "0.333333333" }
strdouble E = 0.333333333555; // internally stored as x = { .raw = "0.333333334" }

您可以看到 BCD 将相等,尽管传递给编译器的字面值有很大不同。您还可以看到像 (1/3 + 1/3 + 1/3) 这样的算术如何 return 0.999999999 而不是 1,因为根本没有办法表示任何超过最后一个原始数字的精度。