为什么 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_f64
、3.141592653589793_f64
或 3.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" }
您可以看到 B
、C
和 D
将相等,尽管传递给编译器的字面值有很大不同。您还可以看到像 (1/3 + 1/3 + 1/3) 这样的算术如何 return 0.999999999
而不是 1
,因为根本没有办法表示任何超过最后一个原始数字的精度。
我有以下 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_f64
、3.141592653589793_f64
或 3.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" }
您可以看到 B
、C
和 D
将相等,尽管传递给编译器的字面值有很大不同。您还可以看到像 (1/3 + 1/3 + 1/3) 这样的算术如何 return 0.999999999
而不是 1
,因为根本没有办法表示任何超过最后一个原始数字的精度。