关于二进制:浮点精度是可变的还是不变的?

Is floating point precision mutable or invariant?

对于浮点数(即floatdoublelong double是否有一个且只有一个精度值,或是否有一个可以改变的精度值,我一直得到各种各样的答案。

一个称为浮点与双精度的主题似乎意味着浮点精度是绝对的。

然而,另一个称为float和double之间的差异的主题说,

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

另一个消息来源说,

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位小数。

操作要求我详细说明为什么53位二进制数字意味着"大约"15位十进制数字。

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

因此,每个数字看起来像:(-1)s×;2yyy×;1.XXXX(其中s是符号位,yyy是指数,1.xxxx是标准化的尾数)。对于直接讨论,我们只关注尾数,而不关注符号或指数。

下面是一个表,列出了所有xxxx值的1.xxxx的外观(所有舍入都是从一半到偶数的,就像默认的浮点舍入模式的工作方式一样):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  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,它包含所有唯一的值,但不提供三个十进制数字范围内的所有值的覆盖范围。

为了便于论证,我们说它有两个十进制数字:十进制精度是可以表示这些十进制数字的所有值的位数。

好吧,那么,如果我们把所有的数字减半会发生什么(所以我们使用的是yyy=-1)?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  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位小数。


所有现代计算机都使用二进制浮点运算。这意味着我们有一个二进制尾数,它通常有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(例如1000≤x<1024),则得到最大的十进制数。如果x略高于2k且小于10n,则得到最小的十进制数,例如1&frasl;1024≤x<1&frasl;1000。相同的二进制精度可以产生最多变化1.3位的十进制精度或log10(2×;10)。

当然,你可以直接阅读这篇文章"每个计算机科学家都应该知道什么是浮点运算"。


80x86代码使用其硬件协处理器(最初是8087)提供三个精度级别:32位、64位和80位。这些都非常接近1985年的IEEE-754标准。最新标准规定了128位格式。浮点格式有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.000110011001100110011001100…),就像我们用十进制表示0.333333333333 33 33的值,大约是1/3。

许多语言通常不支持80位格式。有些C编译器可以提供使用80位浮点或128位浮点的long double。唉,它也可能使用64位浮点,这取决于实现。

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


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倍的范围)之间有大约和2.0和60.0之间一样多的不同浮点数。不管底层存储表示是什么,这都是正确的。

1.23456789e100的相对精度与1.23456789e-100差不多。

大多数计算机将double作为二进制64实现。这种格式有53位二进制精度。

介于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到8.0等2位。

C提供DBL_DIGDBL_DECIMAL_DIG及其对应的floatlong double版本。DBL_DIG表示最小相对小数精度。DBL_DECIMAL_DIG可以看作是最大的相对小数精度。

一般来说,这意味着给定的double将具有15到17位小数的精度。

1.0及其下一个可代表的double为例,数字直到第17位有效的十进制数字才改变。下一个doublepow(2,-52)或约2.2204e-16分开。

1
2
3
4
/*
1 234567890123456789 */
1.000000000000000000...
1.000000000000000222...

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

1
2
3
4
5
/*
1 234567890123456789 */
8.521812787393891
8.521812787393891142073699...
8.521812787393892


不,它是可变的。起始点是非常弱的IEEE-754标准,它只确定了浮动指针数字的格式,因为它们存储在内存中。单精度可计算7位精度,双精度可计算15位精度。

但该标准的一个主要缺陷是没有规定如何进行计算。但问题是,特别是Intel8087浮点处理器已经让程序员们一夜未眠。该芯片的一个显著设计缺陷是,它存储的浮点值比内存格式多。80位而不是32或64位。这种设计选择背后的理论是,这使得中间计算更精确,并减少舍入误差。

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

为了解决这个问题,英特尔重新设计了处理器,SSE指令集使用与内存格式相同的位数进行计算。然而,重新设计编译器的代码生成器和优化器是一项巨大的投资。三大C++编译器都已切换。但是,例如.NET框架中的x86抖动仍然会生成fpu代码,它总是会。

其次是系统误差,由于换算和计算不可避免的副作用而失去精度。首先,人类以10为基数工作,但处理器使用2为基数。我们使用的整数,如0.1,无法在处理器上转换为整数。0.1是10次幂的和,但没有2次幂的有限和产生相同值。转换它会产生无穷多的1和0,就像你不能完美地写下10/3那样。所以它需要被截断以适合处理器,并产生一个与十进制值相差+/-0.5位的值。

计算会产生误差。乘法或除法使结果中的位数加倍,四舍五入以使其恢复到存储值,会产生+/-0.5位错误。减法是最危险的操作,会导致大量有效数字的丢失。例如,如果计算1.234567F-1.234566F,则结果只剩下1个有效数字。这是一个垃圾结果。在数字算法中,求和具有几乎相同值的数字之间的差是非常常见的。

获得过多的系统误差最终是数学模型的一个缺陷。就像一个例子,你永远不想使用高斯消元,它对精度非常不友好。并且总是考虑一种替代方法,LU分解是一种很好的方法。然而,一个数学家参与建立模型并考虑到结果的预期精度,这并不常见。一本普通的书,比如数字食谱,也没有足够的关注精确性,尽管它通过提出更好的模型间接地引导你远离坏的模型。最后,程序员经常被这个问题困扰。嗯,这很容易,然后任何人都可以做到,我会失去一份高薪的工作。


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

有一点要知道,CPU可以在内部以不同的精度运行操作,比如80位。这意味着这样的代码可以触发:

1
2
3
4
5
6
7
8
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.
}

更复杂的计算更有可能。


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

第二:另一个问题是执行了精确的算术运算。想想1.0/3.0或pi。这样的值不能用有限的数字表示——既不是十进制的,也不是二进制的。所以这些值必须四舍五入以适应给定的空间。小数位数越多,精度越高。

现在考虑应用多个这样的操作,例如pi/3.0。这需要进行两次四舍五入:这样的π不准确,结果也不准确。这会使精度降低两次,如果重新处理,精度会变差。

因此,回到floatdoublefloat根据标准(c11,附录F,也适用于其余部分)的可用位较少,因此roundig的精度将低于double。想想有一个小数点,有两个小数(m.ff,称之为float),一个有四个小数(m.ffff,称之为double)。如果所有计算都使用double,那么在结果只有2个正确的小数位数之前,可以进行更多的操作,而不是从float开始,即使float结果足够。

请注意,在一些(嵌入式)CPU上,如ARM Cortex-M4F,硬件FPU只支持FOLAT(单精度),因此双精度运算将花费更多。其他的MCU根本没有硬件浮点计算器,所以它们必须模拟我的软件(非常昂贵)。在大多数GPU上,float执行起来也比double便宜得多,有时甚至比10倍还便宜。


我将在这里添加一个非决定性的答案,并且说,既然你把这个问题标记为C++,就不能保证浮点数据的精确度。绝大多数实现在实现浮点类型时使用IEEE-754,但这不是必需的。C++语言唯一需要的是(C++规范3.3.1.8):

There are three ?oating 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 ?oating-point types is implementation-de?ned. Integral and ?oating 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.


存储float所需的空间量是恒定的,同样地,double所需的空间量也是恒定的;然而,有用的精度量一般在223中的一部分与224中的一部分(对于float而言)或252中的一部分与253中的一部分(对于double而言)之间存在差异。非常接近于零的精度不是很好,第二个最小的正值是最小值的两倍,这反过来也将无穷大于零。然而,在整个范围内,精度将如上文所述发生变化。

请注意,虽然在整个范围内相对精度变化小于2倍的类型通常是不实际的,但精度变化有时会导致计算结果的精确性比它们看起来的要差得多。例如,考虑16777215.0f + 4.0f - 4.0f。所有的值都可以用相同的比例精确表示为float,最接近大的值是16777215中的+/-1部分,但第一次添加会导致float范围的一部分,其中值仅以8388610中的一部分分隔,导致结果四舍五入为16777220。因此,减去4得到16777216而不是16777215。对于16777216附近的大多数float值,加上4.0f和减去4.0f会得到原始值不变,但在转换点处更改的精度会导致结果在最低位置被额外的位关闭。


答案很简单,但很复杂。这些数字以二进制形式存储。根据它是浮点型还是双精度型,计算机使用不同数量的二进制来存储数字。你得到的精度取决于你的二进制文件。如果你不知道二进制数字是如何工作的,最好查一下。但简单地说,有些数字比其他数字需要更多的1和0。

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