浮点精度是可变的还是不变的?

Is floating point precision mutable or invariant?

关于浮点数(即 floatdoublelong double)是否只有一个精度值,或者是否有一个精度值,我一直得到混合的答案这可能会有所不同。

一个名为 float vs. double precision 的主题似乎暗示浮点精度是绝对的。

但是,另一个名为 Difference between float and double 的主题说,

In general a double has 15 to 16 decimal digits of precision

另一个 source 说,

Variables of type float typically have a precision of about 7 significant digits

Variables of type double typically have a precision of about 16 significant digits

如果我使用的是敏感代码,当我的值不准确时,这些代码很容易被破坏,我不喜欢参考上述近似值。所以让我们澄清一下吧。浮点精度是可变的还是不变的,为什么?

精度是固定的,对于双精度,正好是 53 个二进制数字(如果我们排除隐含的前导 1,则为 52)。结果是 大约 15 位小数


OP 要求我详细说明为什么恰好有 53 个二进制数字意味着 "about" 15 个十进制数字。

为了直观地理解这一点,让我们考虑一种不太精确的浮点格式:我们将使用 4 位尾数而不是像双精度数那样的 52 位尾数。

所以,每个数字看起来像:(-1)s × 2yyy × 1.xxxx (其中 s是符号位,yyy是指数,1.xxxx是归一化尾数)。为了立即讨论,我们将只关注尾数而不是符号或指数。

这是 table 所有 xxxx 值的 1.xxxx 的样子(所有舍入都是半对偶数,就像默认浮点舍入模式的工作方式一样):

  xxxx  |  1.xxxx  |  value   |  2dd  |  3dd  
--------+----------+----------+-------+--------
  0000  |  1.0000  |  1.0     |  1.0  |  1.00
  0001  |  1.0001  |  1.0625  |  1.1  |  1.06
  0010  |  1.0010  |  1.125   |  1.1  |  1.12
  0011  |  1.0011  |  1.1875  |  1.2  |  1.19
  0100  |  1.0100  |  1.25    |  1.2  |  1.25
  0101  |  1.0101  |  1.3125  |  1.3  |  1.31
  0110  |  1.0110  |  1.375   |  1.4  |  1.38
  0111  |  1.0111  |  1.4375  |  1.4  |  1.44
  1000  |  1.1000  |  1.5     |  1.5  |  1.50
  1001  |  1.1001  |  1.5625  |  1.6  |  1.56
  1010  |  1.1010  |  1.625   |  1.6  |  1.62
  1011  |  1.1011  |  1.6875  |  1.7  |  1.69
  1100  |  1.1100  |  1.75    |  1.8  |  1.75
  1101  |  1.1101  |  1.8125  |  1.8  |  1.81
  1110  |  1.1110  |  1.875   |  1.9  |  1.88
  1111  |  1.1111  |  1.9375  |  1.9  |  1.94

你说提供的小数位数是多少?你可以说 2,因为包含了两位小数范围内的每个值,尽管不是唯一的;或者您可以说 3,它涵盖所有唯一值,但不涵盖三位小数范围内的所有值。

为了论证,我们假设它有 2 个十进制数字:十进制精度将是可以表示这些十进制数字的所有值的位数。


好吧,如果我们将所有数字减半(所以我们使用 yyy = -1)会怎样?

  xxxx  |  1.xxxx  |  value    |  1dd  |  2dd  
--------+----------+-----------+-------+--------
  0000  |  1.0000  |  0.5      |  0.5  |  0.50
  0001  |  1.0001  |  0.53125  |  0.5  |  0.53
  0010  |  1.0010  |  0.5625   |  0.6  |  0.56
  0011  |  1.0011  |  0.59375  |  0.6  |  0.59
  0100  |  1.0100  |  0.625    |  0.6  |  0.62
  0101  |  1.0101  |  0.65625  |  0.7  |  0.66
  0110  |  1.0110  |  0.6875   |  0.7  |  0.69
  0111  |  1.0111  |  0.71875  |  0.7  |  0.72
  1000  |  1.1000  |  0.75     |  0.8  |  0.75
  1001  |  1.1001  |  0.78125  |  0.8  |  0.78
  1010  |  1.1010  |  0.8125   |  0.8  |  0.81
  1011  |  1.1011  |  0.84375  |  0.8  |  0.84
  1100  |  1.1100  |  0.875    |  0.9  |  0.88
  1101  |  1.1101  |  0.90625  |  0.9  |  0.91
  1110  |  1.1110  |  0.9375   |  0.9  |  0.94
  1111  |  1.1111  |  0.96875  |  1.   |  0.97

按照与以前相同的标准,我们现在处理的是 1 位小数。所以你可以看到,根据指数,你可以有更多或更少的小数位,因为 二进制和十进制浮点数不能完全相互映射.

同样的论点适用于双精度浮点数(带有 52 位尾数),只是在这种情况下,您将获得 15 位或 16 位十进制数字,具体取决于指数。

好吧,这个问题的答案很简单但也很复杂。这些数字以二进制形式存储。根据它是浮点数还是双精度数,计算机使用不同数量的二进制来存储数字。您获得的精度取决于您的二进制文件。如果您不知道二进制数是如何工作的,那么查找它是个好主意。但简单地说,有些数字比其他数字需要更多的 1 和 0。

所以精度是固定的(相同数量的二进制数字),但您获得的实际精度取决于您使用的数字。

浮点变量的类型定义了值的范围和可以表示的小数位 (!) 的数量。由于小数和二进制小数没有整数关系,小数实际上是一个近似值。

其次:还有一个问题就是运算的精度。想想 1.0/3.0 或 PI。这些值不能用有限数量的数字表示——既不是十进制也不是二进制。所以这些值必须四舍五入以适应给定的 space。小数位数越多,精度越高。

现在考虑应用多个这样的操作,例如PI/3.0 。这将需要进行两次舍入:PI 本身不准确,结果也不准确。这将失去两次精度,如果重复它会变得更糟。

所以,回到 floatdoublefloat 根据标准(C11,附件 F,其余部分)可用的位数较少,因此 roundig 将是不如 double 精确。试想一下,小数点有 2 个小数位(m.ff,称之为浮点数)和一个有 4 个小数位(m.ffff,称之为双精度)。如果所有计算都使用 double,那么您可以进行更多操作,直到您的结果只有 2 个正确的小数位,而不是如果您已经从 float 开始,即使 float 结果就足够了。

请注意,在某些(嵌入式)CPU(如 ARM Cortex-M4F)上,硬件 FPU 仅支持 folat(单精度),因此双精度运算的成本会更高。其他的MCU根本就没有硬件浮点计算器,只好用我的软件来模拟(很费钱)。在大多数 GPU 上,float 的执行成本也比 double 低得多,有时是 10 倍以上。

我将在此处添加另类答案,并说明由于您已将此问题标记为 C++,因此无法保证浮点数据的精度。绝大多数实现在实现其浮点类型时都使用 IEEE-754 ,但这不是必需的。 C++ 语言唯一需要的是(C++ 规范§3.9.1.8):

There are three floating point types: float, double, and long double. The type double provides at least as much precision as float, and the type long double provides at least as much precision as double. The set of values of the type float is a subset of the set of values of the type double; the set of values of the type double is a subset of the set of values of the type long double. The value representation of floating-point types is implementation-defined. Integral and floating types are collectively called arithmetic types. Specializations of the standard template std::numeric_limits (18.3) shall specify the maximum and minimum values of each arithmetic type for an implementation.

所有现代计算机都使用二进制浮点运算。这意味着我们有一个二进制尾数,它通常有 24 位单精度、53 位双精度和 64 位扩展精度。 (扩展精度在 x86 处理器上可用,但在 ARM 或其他类型的处理器上不可用。)

24、53 和 64 位尾数表示对于介于 2k 和 2k+1 之间的浮点数,下一个更大的数字分别是 2k-23、2k-52 和 2k-63。这就是决议。每次浮点运算的舍入误差最多为其一半。

那么如何将其转化为十进制数? 这取决于

取k = 0 且1 ≤ x < 2。分辨率为2-23, 2-52, 2-63约等于1.19×10-7、2.2×10-16、1.08×10 -19分别。这比 7、16 和 19 位小数少了一点。然后取 k = 3 和
8 ≤ x < 16。两个浮点数之间的差现在大 8 倍。对于 8 ≤ x < 10,您分别得到略高于 6、小于 15 和略高于 18 位小数。但是对于 10 ≤ x < 16 你多了一位小数!

如果 x 只小于 2k+1 且只大于 10n[=,你得到的小数位数最多46=],例如 1000 ≤ x < 1024。如果 x 仅比 2k 高一点且比 10n,例如 11024 ≤ x < 11000。相同的二进制精度可以产生最多相差 1.3 位或 log10 (2×10).

的十进制精度

当然,你可以只看文章“What every computer scientist should know about floating-point arithmetic”。

Is floating point precision mutable or invariant, and why?

通常,给定相同 2 的幂范围内的任何数字,浮点精度是不变的 - 一个固定值。绝对精度随着每个 2 的幂步长而变化。在整个 FP 范围内,精度大约与幅度有关。将此相对二进制精度与十进制精度相关联会导致 摆动 DBL_DIGDBL_DECIMAL_DIG 十进制数字之间变化 - 通常为 15 到 17。


什么是精度?对于 FP,讨论 相对 精度最有意义。

浮点数的形式为:

Sign * Significand * pow(base,exponent)

它们服从对数分布。 100.0 和 3000.0(30 倍范围)之间的不同浮点数 about 与 2.0 和 60.0 之间的浮点数一样多。无论底层存储表示如何,都是如此。

1.23456789e100 具有与 1.23456789e-100.

大致相同的 relative 精度

大多数计算机将 double 实现为 binary64。此格式具有 53 位 binary 精度。

1.0和2.0之间的n个数在((2.0-1.0)/pow(2,52).
中的1部分绝对精度相同 64.0 和 128.0 之间的数字,也就是 n,在 ((128.0-64.0)/pow(2,52).

中具有相同的 1 部分的绝对精度

即使是 2 的幂之间的一组数字,也具有相同的绝对精度。

在 FP 数的整个正常范围内,这近似于统一的相对精度。

当这些数字表示为十进制时,精度波动:数字1.0 到2.0 的绝对精度比数字2.0 到4.0 多1 位。比 4.0 多 2 位到 8.0 等

C 提供了 DBL_DIGDBL_DECIMAL_DIG 以及对应的 floatlong doubleDBL_DIG表示最小相对小数精度。 DBL_DECIMAL_DIG可以认为是最大相对小数精度。

通常这意味着给定的 double 将具有 15 到 17 位小数的精度。

考虑 1.0 及其下一个可表示的 double,数字在第 17 个有效十进制数字之前不会改变。接下来的每个 double 相隔 pow(2,-52) 或大约 2.2204e-16

/*
1 234567890123456789 */
1.000000000000000000...
1.000000000000000222...

现在考虑 "8.521812787393891" 及其下一个可表示的数字作为使用 16 位有效小数位的十进制字符串。这两个转换为 double 的字符串是 相同的 8.521812787393891142073699...,即使它们在第 16 位不同。说这个 double 有 16 位精度是言过其实了。

/*
1 234567890123456789 */
8.521812787393891
8.521812787393891142073699...
8.521812787393892

80x86 代码使用其硬件协处理器(最初是 8087)提供三个级别的精度:32 位、64 位和 80 位。那些非常密切地关注 IEEE-754 standard of 1985. The recent standard specifies a 128-bit format。浮点格式有 24、53、65 和 113 个尾数位,分别对应 7.22、15.95、19.57 和 34.02 位十进制精度。

The formula is mantissa_bits / log_2 10 where the log base two of ten is 3.321928095.

虽然任何特定实现的精度不会变化,但浮点值转换为十进制时可能会出现变化。请注意,值 0.1 没有精确的二进制表示。它是一个重复的位模式 (0.0001100110011001100110011001100...) 就像我们习惯用十进制表示 0.3333333333333 大约 1/3。

许多语言通常不支持 80 位格式。某些 C 编译器可能会提供 long double,它使用 80 位浮点数或 128 位浮点数。 las,它也可能使用 64 位浮点数,具体取决于实现。

NPU 有 80 位寄存器,并使用完整的 80 位结果执行所有操作。在 NPU 堆栈中计算的代码受益于这种额外的精度。不幸的是,糟糕的代码生成(或编写糟糕的代码)可能会通过将中间计算存储在 32 位或 64 位变量中来截断或舍入中间计算。

正如其他答案所解释的那样,存储具有精确的二进制数字计数。

需要知道的一件事是,CPU 可以在内部以不同的精度进行 运行 运算,例如 80 位。这意味着这样的代码可以触发:

void Kaboom( float a, float b, float c ) // same is true for other floating point types.
{
    float sum1 = a+b+c;
    float sum2 = a+b;
    sum2 += c; // let's assume that the compiler did not keep sum2 in a register and the value was write to memory then load again.
    if (sum1 !=sum2)
        throw "kaboom"; // this can happen.
}

越复杂的计算越有可能。

存储 float 所需的 space 数量将保持不变,同样 double;相对而言,有用的精度通常会有所不同,但是,在 223 的一部分和 224 的一部分之间 float,或 252 和 253 中的一部分 double。非常接近零的精度不是那么好,第二小的正值是最小的两倍,而后者将无限大于零。然而,在大部分范围内,精度会如上所述发生变化。

请注意,虽然在其整个范围内拥有相对精度变化小于两倍的类型通常是不切实际的,但精度的变化有时会导致计算产生的计算结果比看起来要低得多他们应该。例如,考虑 16777215.0f + 4.0f - 4.0f。所有的值都可以使用相同的比例精确表示为 float,最接近大值的值是 16,777,215 中的 +/- 一部分,但第一次加法产生的结果是 [=10 的一部分=] 范围,其中值仅以 8,388,610 中的一个部分分隔,导致结果四舍五入为 16,777,220。因此,减去 4 得到 16,777,216 而不是 16,777,215。对于 16777216 附近的 float 的大多数值,添加 4.0f 并减去 4.0f 将产生原始值不变,但恰好在转折点处变化的精度会导致结果在最低的地方多一点。

不,它是可变的。起点是非常弱的 IEEE-754 标准,它只确定了浮点数存储在内存中的格式。您可以指望单精度的 7 位精度,双精度的 15 位精度。

但该标准的一个主要缺陷是它没有指定如何执行计算。麻烦来了,尤其是Intel 8087浮点处理器让程序员睡不着觉。该芯片的一个重大设计缺陷是它存储的浮点值比内存格式多 位。 80 位而不是 32 位或 64 位。该设计选择背后的理论是,这允许中间计算更精确并导致更小的舍入误差。

听起来是个好主意,但实际效果并不理想。编译器编写者会尝试生成尽可能长地保留 FPU 中存储的中间值的代码。对代码速度很重要,将值存储回内存是昂贵的。麻烦的是,他经常 必须 将值存储回去,FPU 中的寄存器数量有限,代码可能会跨越函数边界。在这一点上,该值被截断并失去了很多精度。对源代码的小改动现在可以产生截然不同的值。此外,程序的非优化构建会产生与优化构建不同的结果。以一种完全无法诊断的方式,您必须查看机器代码才能知道为什么结果不同。

Intel 重新设计了他们的处理器来解决这个问题,SSE 指令集计算与内存格式相同的位数。然而,重新设计编译器的代码生成器和优化器是一项重大投资,因此流行起来很慢。三大 C++ 编译器都已切换。但是例如 .NET Framework 中的 x86 抖动仍然会生成 FPU 代码,它总是会。


然后是系统错误,精度下降是转换和计算不可避免的副作用。首先转换,人类使用以 10 为基数的数字,但处理器使用以 2 为基数的数字。我们使用的漂亮的整数,如 0.1 不能在处理器上转换为漂亮的整数。 0.1 作为 10 的幂的总和是完美的,但是没有产生相同值的有限的 2 的幂和。转换它会产生无限数量的 1 和 0,就像你不能完美地写下 10 / 3 一样。所以它需要被截断以适应处理器,并且产生的值偏离 +/- 0.5 位十进制值。

并且计算产生错误。乘法或除法将结果中的位数加倍,将其四舍五入以适应存储值会产生 +/- 0.5 位错误。减法是最危险的操作,可能会导致丢失 lot 的有效数字。例如,如果您计算 1.234567f - 1.234566f,那么结果只剩下一位有效数字。那是一个垃圾结果。将具有几乎相同值的数字之间的差异求和在数值算法中很常见。

获得过多的系统误差最终是数学模型中的一个缺陷。举个例子,你永远不要用高斯消元法,它对精度很不友好。并且始终考虑替代方法,LU 分解是一种出色的方法。然而,数学家参与构建模型并考虑结果的预期精度并不常见。像 Numerical Recipes 这样的普通书籍也没有对精度给予足够的重视,尽管它通过提出更好的模型来间接引导你远离糟糕的模型。最后,程序员经常被问题困住。好吧,这很容易,然后任何人都可以做到,而且我会失去一份高薪工作:)