可以转换为二进制并返回十进制而不损失重要性的最重要的十进制数字精度是 6 还是 7.225?

Is the most significant decimal digits precision that can be converted to binary and back to decimal without loss of significance 6 or 7.225?

我遇到了两个不同精度的浮点数公式。

⌊(N-1) log10(2)⌋ = 6 decimal digits (Single-precision)

N log10(2) ≈ 7.225 decimal digits (Single-precision)

其中 N = 24 有效位(单精度)

第一个公式位于“IEEE Standard 754 for Binary Floating-Point Arithmetic”第 4 页的顶部,作者是 W. Kahan 教授

第二个公式可在维基百科文章“Single-precision floating-point format”的第 IEEE 754 单精度二进制浮点格式下找到:binary32.

对于第一个公式,W. Kahan 教授说

If a decimal string with at most 6 sig. dec. is converted to Single and then converted back to the same number of sig. dec., then the final string should match the original.

对于第二个公式,维基百科说

...the total precision is 24 bits (equivalent to log10(224) ≈ 7.225 decimal digits).

两个公式(6 位和 7.225 位十进制数字)的结果不同,我希望它们是相同的,因为我假设它们都表示可以转换为浮点数的最重要的十进制数字二进制,然后转换回十进制,并使用与它开头相同的有效小数位数。

为什么这两个数字不同,可以转换为二进制并返回十进制而不丢失意义的最重要的十进制数字精度是多少?

请记住,它们是完全相同的公式。记住你的高中数学书身份:

    Log(x^y) == y * Log(x)

使用计算器实际计算 N = 24 的值很有帮助:

  Kahan's:      23 * Log(2) = 6.924
  Wikipedia's:   Log(2^24)  = 7.225

由于 floor(),Kahan 被迫将 6.924 截断为 6 位数,真可惜。唯一实际的区别是 Kahan 使用的精度少了 1 位。

很难猜出原因,教授可能依赖于旧笔记。在 IEEE-754 之前编写并且没有考虑到第 24 位精度是免费的。该格式使用了一个技巧,非 0 的浮点值的最高有效位始终为 1。因此不需要存储它。处理器在执行计算之前将其添加回去。将 23 位存储精度转换为 24 位有效精度。

或者他考虑到从十进制字符串到二进制浮点值的转换本身会产生错误。许多漂亮的圆十进制值,如 0.1,不能完美地转换为二进制。它有无穷多的数字,就像十进制的 1/3。然而,这会产生一个偏离 +/- 0.5 位的结果,这是通过简单的舍入实现的。所以结果精确到 23.5 * Log(2) = 7.074 十进制数字。如果他假设转换例程很笨拙并且没有正确舍入,那么结果可能会相差 +/-1 位并且 N-1 是合适的。他们并不笨拙。

或者他像典型的科学家或(天哪)会计师一样思考,并希望将计算结果也转换回十进制。例如,当您平凡地查找一个 7 位十进制数,其来回转换不会产生相同的数字时,您会得到这样的结果。是的,这又增加了 +/- 0.5 位错误,总共有 1 位错误。

但是永远,永远不要犯这样的错误,您总是必须 包括 在计算中处理数字时出现的任何错误。其中一些很快丢失有效数字,尤其是减法是非常危险的。

这些是在谈论两个略有不同的事情。

7.2251 位是数字可以内部存储的精度。例如,如果您使用双精度数进行计算(因此您从 15 位精度开始),然后将其四舍五入为单精度数,此时您剩下的精度将约为7 位数。

这 6 位数字谈论的是可以保持的精度 通过 从一串十进制数字到浮点数再返回到另一个的往返转换一串十进制数字。

因此,假设我以 1.23456789 之类的数字作为字符串开始,然后将其转换为 float32,然后将结果转换回字符串。完成此操作后,我可以期望 6 位数字完全匹配。第七位数字可能会四舍五入,所以我不一定期望它匹配(尽管它可能是原始字符串的 +/- 1。

例如,考虑以下代码:

#include <iostream>
#include <iomanip>

int main() {
    double init = 987.23456789;
    for (int i = 0; i < 100; i++) {
        float f = init + i / 100.0;
        std::cout << std::setprecision(10) << std::setw(20) << f;
    }
}

这会生成如下所示的 table:

     987.2345581         987.2445679         987.2545776         987.2645874
     987.2745972         987.2845459         987.2945557         987.3045654
     987.3145752          987.324585         987.3345947         987.3445435
     987.3545532          987.364563         987.3745728         987.3845825
     987.3945923          987.404541         987.4145508         987.4245605
     987.4345703         987.4445801         987.4545898         987.4645386
     987.4745483         987.4845581         987.4945679         987.5045776
     987.5145874         987.5245972         987.5345459         987.5445557
     987.5545654         987.5645752          987.574585         987.5845947
     987.5945435         987.6045532          987.614563         987.6245728
     987.6345825         987.6445923          987.654541         987.6645508
     987.6745605         987.6845703         987.6945801         987.7045898
     987.7145386         987.7245483         987.7345581         987.7445679
     987.7545776         987.7645874         987.7745972         987.7845459
     987.7945557         987.8045654         987.8145752          987.824585
     987.8345947         987.8445435         987.8545532          987.864563
     987.8745728         987.8845825         987.8945923          987.904541
     987.9145508         987.9245605         987.9345703         987.9445801
     987.9545898         987.9645386         987.9745483         987.9845581
     987.9945679         988.0045776         988.0145874         988.0245972
     988.0345459         988.0445557         988.0545654         988.0645752
      988.074585         988.0845947         988.0945435         988.1045532
      988.114563         988.1245728         988.1345825         988.1445923
      988.154541         988.1645508         988.1745605         988.1845703
     988.1945801         988.2045898         988.2145386         988.2245483

如果我们看一下这个,我们可以看到前六位有效数字总是精确地遵循模式(即,每个结果都比它的前一个结果大 0.01)。可以看到原来double的值其实是98x.xx456——但是当我们把单精度浮点数转成十进制的时候,可以看到第7th 数字经常不会被正确读回——因为后续数字大于 5,它应该四舍五入到 98x.xx46,但有些值不会(例如,第一个中的倒数第二个项目)列是 988.154541,它将向下舍入而不是向上舍入,所以我们最终得到 98x.xx45 而不是 46。因此,即使值(存储的)精确到 7 位(加上一点),当我们通过转换为十进制并返回来往返该值时,我们不能再依赖于第七位数字的精确匹配(即使有足够的精度它会比不是)。


1. 这基本上意味着 7 位数字,第 8th 位比没有更准确,但不是很多——例如,如果我们从双倍的1.2345678.225 位精度意味着最后一位数字大约是开始时的 +/- .775(而没有 .225 位精度,它会基本上是开始时的 +/- 1)。

what is the most significant decimal digits precision that can be converted to binary and back to decimal without loss of significance?

可以在不损失重要性的情况下(对于单精度浮点数或 24 位)可以转换为二进制并返回十进制的最高有效十进制数字精度为 6 位十进制数字。


Why do these two numbers differ...

数字 6 和 7.225 不同,因为它们定义了两个不同的事物。 6 是可以往返的最多的十进制数字。 7.225 是 24 位二进制整数的近似十进制位数精度,因为 24 位二进制整数可以有 7 位或 8 位小数位,具体取决于其特定值。

7.225 是使用特定的二进制整数公式找到的。

dspec = b·log10(2)             (dspec = specific decimal digits, b = bits)

但是,您通常需要知道的是 b 位整数的最小和最大十进制数字。以下公式用于查找特定二进制整数的最小和最大十进制数字(24 位分别为 7 和 8)。

dmin = ⌈(b-1)·log10(2)⌉    (dmin = min decimal digits, b = bits, ⌈x⌉ = smallest integer ≥ x)

dmax = ⌈b·log10(2)⌉         (dmax = max decimal digits, b = bits, ⌈x⌉ = smallest integer ≥ x)

要详细了解这些公式的推导方式,请阅读 Rick Regan 撰写的 Number of Decimal Digits In a Binary Integer

这一切都很好,但是你可能会问,如果你说一个24位数字的小数位数跨度是7到8,为什么来回转换的小数位数最多是6?

答案是——因为上面的公式只适用于整数而不适用于浮点数!

每个十进制整数都有一个精确的二进制值。但是,不能对每个十进制浮点数都这样说。以 .1 为例。 .1 在二进制中是数字 0.000110011001100...,这是一个重复或循环的二进制。这会产生舍入误差。

此外,表示一个十进制浮点数比表示一个同等重要的十进制整数多需要一位。这是因为浮点数越接近 0 越精确,越远离 0 越不精确。正因为如此,许多接近最小值和最大值范围的浮点数 (e min = -126 和 emax = +127 对于单精度)由于舍入误差而丢失 1 位精度。要直观地了解这一点,请查看 Josh Haberman 撰写的 What every computer programmer should know about floating point, part 1

此外,至少有784,757个正七位十进制数来回转换后不能保留原值。 8.589973e9 是无法在往返过程中幸存下来的此类数字的一个示例。这是不保留其原始值的最小正数。

这是您应该用于浮点数精度的公式,它将为您提供 6 位十进制数字以进行往返转换。

dmax = ⌊(b-1)·log10(2)⌋    (dmax = max decimal digits, b = bits, ⌊x⌋ = largest integer ≤ x)

要详细了解此公式的推导方式,请阅读同样由 Rick Regan 撰写的 Number of Digits Required For Round-Trip Conversions。 Rick 出色地展示了公式的推导并参考了严格的证明。


因此,您可以以建设性的方式利用上述公式;如果您了解它们的工作原理,则可以将它们应用于任何使用浮点数据类型的编程语言。您所需要知道的只是您的浮点数据类型所具有的有效位数,并且您可以找到它们各自的小数位数,您可以指望这些位数在往返转换后不会丢失任何重要性。

2017 年 6 月 18 日更新: 我想在 Rick Regan 的新文章中包含一篇 link,这篇文章更详细,我认为比此处提供的任何答案。他的文章是“Decimal Precision of Binary Floating-Point Numbers" and can be found on his website www.exploringbinary.com.