浮点精度是否可变或不变?

39

我一直在获取混合的答案,关于浮点数(例如floatdoublelong double)是否具有一个且仅有一个精度值,或者具有可变的精度值。

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

然而,另一个名为“float和double之间的区别”的主题说:

通常情况下,double具有15到16个小数位的精度

另一个来源说:

类型为float的变量通常具有约7个有效数字的精度

类型为double的变量通常具有约16个有效数字的精度

如果我正在处理易受影响的代码,当我的值不精确时很容易出错,我不喜欢参考上述近似值。因此,让我们澄清一下。浮点精度是可变的还是不变的,为什么?


11
内部以二进制形式存储,因此十进制精度不准确。 - n0rd
3
如果你不喜欢近似值,可以使用定点数学代替。 - Michael Dorgan
10
这里的“about”是由于从有效位数转换到有效数字所导致的。 - Degustaf
3
这个博客有一系列关于浮点数运算的好文章。由于二进制和十进制表示之间的非精确转换,你不会得到比“大约”更好的答案,所以你可能需要全面阅读这个主题。 - jaggedSpire
1
@MichaelDorgan:如果你不喜欢近似值,那么你需要坚持使用整数运算。定点运算(虽然比浮点运算更容易预测)仍然只是对实数/有理数的近似,而这些才是你在几乎所有有趣的应用中想要表达的内容。并且它通常是比浮点数更糟糕的近似值!(参考链接:http://programmers.stackexchange.com/questions/87457/why-do-you-need-float-double/87520#87520) - leftaroundabout
显示剩余4条评论
10个回答

29

精度是固定的,对于双精度而言是恰好53个二进制数字(如果我们排除隐式前导1,则为52个)。这相当于约15位十进制数字

  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个十进制数字。


2
如果你曾经使用过有效数字的科学计数法来处理数字,那么浮点数就是它们的二进制等价物。 - jaggedSpire
3
科学计数法通常缺少 NaN、-0、+/-inf 和非规范化数,因此并不完全等同。 ;) - Yakk - Adam Nevraumont
2
你可能需要详细解释一下“大约15位小数”,因为这就是问题所在(由于所表示的数字不同,十进制位数可能会有所变化)。 - Guvante
2
只有编译器使用IEC 60559浮点数(_STDC_IEC_559_已定义),这才是真实的。对于嵌入式系统,特别是没有(兼容的)FPU的情况并非必然如此。 - too honest for this site
1
@WanderingFool 好的,现在已经更新了帖子,使用上标来表示指数。 :-) 无论如何,符号不会影响精度:如果s == 0,那么数字是正数,如果s == 1,那么数字是负数。就这样。 - C. K. Young
显示剩余11条评论

25
所有现代计算机都使用二进制浮点运算。这意味着我们有一个二进制尾数,通常单精度为24位,双精度为53位,扩展精度为64位。(x86处理器支持扩展精度,但ARM或其他类型的处理器可能不支持。)
24、53和64位尾数意味着对于介于2^k和2^(k+1)之间的浮点数,下一个较大的数分别为2^(k-23),2^(k-52)和2^(k-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略小于2的k+1次方且略大于10的n次方,例如1000≤x<1024,则小数部分位数最多。如果x略高于2的k次方且略低于10的n次方,例如1/1024≤x<1/1000,则小数部分位数最少。同样的二进制精度可以产生最多相差1.3个数字或log10(2×10)的十进制精度。当然,您也可以阅读文章“计算机科学家应该了解的有关浮点运算的所有知识”。

我还没有时间仔细检查你的数学。但如果一切都没问题,这是一个很好的答案。干得好,先生。 - Wandering Fool
2
"“所有现代计算机都使用二进制浮点算术”这种说法有些过于夸张了。现代处理器中直接支持十进制浮点数的有Power 6、7、8等。IEEE 754-2008比其前身规范增加了十进制浮点数格式。我看到硬件十进制浮点数支持正在缓慢扩展。" - chux - Reinstate Monica
@Cort Ammon 公平的观点是所有现代计算机都使用二进制。然而,在这里使用“这意味着我们有一个二进制...”暗示所有现代计算机仅使用二进制。随着现在定义良好的十进制格式和每年更便宜的硬件,FP数学为我们提供了新的领域,不应被归为“没有人再这样做,也永远不会再这样做”的类别。 - chux - Reinstate Monica
1
@Chris Jester-Young,我也不认为二进制浮点数的主导地位会消失。我的观点是,考虑十进制浮点数仍然是有效的。 - chux - Reinstate Monica
1
看起来不错。一个挑剔的补充是指出次正常(也称为非规格化)数字具有较少的精度。然而,这些数字非常小(难以置信的小),因此在那里减少的精度通常不是问题。我已经广泛地写过关于浮点数的博客。其中一篇相关文章是这篇: https://randomascii.wordpress.com/2012/03/08/float-precisionfrom-zero-to-100-digits-2/ 它讨论了精度,包括表示浮点数所需的唯一数字数量(九个)和浮点数保证表示的十进制数字数量(六个)之间的差异。 - Bruce Dawson
显示剩余3条评论

9

使用它的硬件协处理器(最初是8087),80x86代码提供三种精度:32位、64位和80位。这些非常接近1985年的IEEE-754标准。最近的标准规定了128位格式。浮点格式具有24、53、65和113个尾数位,这对应于7.22、15.95、19.57和34.02个小数点位。

计算公式为尾数位数 / log_2 10,其中以2为底的10的对数为3.321928095。

尽管任何特定实现的精度不会变化,但当浮点值转换为十进制时,它可能会出现变化。请注意,值0.1没有精确的二进制表示。它是一个重复的二进制模式(0.0001100110011001100110011001100...),就像我们在十进制中用3.33333333333333来逼近1/3一样。

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

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


3
@supercat,你在说什么?float 变成 double,double 仍然是 double,long double 也一直是 long double。没有任何不确定性。如果你想要一个 long double,必须明确指出。 - Random832
1
M_PI等常量需要使用double类型。GNU libc提供了M_PIl,用于long double版本。 - Random832
希望在速度和精度之间做出权衡的代码可以使用 double cm = inches * (double)CM_PER_INCH。只有一种常见情况需要特别知道或关心 CM_PER_INCHdouble 还是 long double,那就是在 printf 中。如果 CM_PER_INCHlong double,那么 printf("%8.4f %9.4f", size, size*CM_PER_INCH) 只有当类型 CM_PER_INCHdouble 同义词时才能工作。由于相当多的库定义了 long double 常量,而且大量使用这些常量的代码如果这些常量与 double 不兼容则会崩溃。 - supercat
1
@wallyk 我在数学领域的另一个StackExchange服务器上询问了上述公式。如果您或其他人愿意向我解释,请回答我的主题如何理解或推导出公式Mantissa bits / log2 10 = Decimal digits of precision? - Wandering Fool
1
@wallyk 这个问题,最显著的十进制数字精度是6还是7.225,可以在转换为二进制并返回十进制时不会失去精度? 可能对您有些兴趣。答案更详细地解释了7.225值是什么。 - Wandering Fool
显示剩余7条评论

8
通常情况下,在相同的2的幂范围内给定任何数字,浮点精度都是不变的 - 一个固定值。每个2的幂步长会改变绝对精度。在整个FP范围内,精度大致相对于大小。用十进制精度来描述这种相对二进制精度会产生一个摆动,其变化在DBL_DIG和DBL_DECIMAL_DIG之间,通常为15到17个十进制数字。
什么是精度?在FP中,讨论相对精度最有意义。
浮点数的形式为:
符号*尾数*pow(基数,指数)
它们具有对数分布。在100.0到3000.0之间(30倍范围)和2.0到60.0之间,不同的浮点数大约有相同数量。无论底层存储表示如何,这都是真实的。
1.23456789e100与1.23456789e-100具有大致相同的相对精度。
大多数计算机将 double 实现为binary64。该格式具有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的数字多一个绝对精度位。比4.0到8.0的数字多2位,依此类推。

C提供DBL_DIGDBL_DECIMAL_DIG以及它们的floatlong double对应项。 DBL_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

你给了我更多的思考。迄今为止,我已经阅读了所有答案,并在一些答案之间发现了不一致和矛盾。今天我将努力验证所有这些声明和证明,并暂缓选择我认为最好的答案,留待明天再作决定。 - Wandering Fool
@Wandering Fool 聪明的举动,对于一个“愚人”来说。你碰到了一个微妙而深刻的问题 - 如果你在周末之前选择等待,我甚至都不介意。可能还会有更多好的答案出现。很多复杂性在于人们以十进制思考,而计算机使用二进制,数学具有无限精度,而计算机是有限的。 - chux - Reinstate Monica
"If the fool would persist in his folly he would become wise" - William Blake, 1776, Proverbs of Hell. - Wandering Fool

6
不是,它是可变的。起点是非常薄弱的IEEE-754标准,它只规定了浮点数在内存中存储的格式。单精度可以准确保留7位数字,双精度可以保留15位数字。
然而,该标准的一个主要缺陷是它没有指定如何执行计算。特别是英特尔8087浮点处理器已经让程序员苦不堪言。该芯片的一个重大设计缺陷是它以比内存格式更高的位数存储浮点值,80位而不是32位或64位。这种设计选择背后的理论是允许中间计算更准确并且导致更少的舍入误差。
听起来像个好主意,但在实践中并不好用。编译器的编写者尝试生成尽可能长时间将中间值存储在FPU中的代码。存储值回内存很耗费时间,对于代码速度很重要。问题是,他经常必须存储值,因为FPU寄存器的数量有限,代码可能会跨越函数边界。此时,该值会被截断回来并失去大量精度。源代码的微小更改现在可能会产生截然不同的值。此外,未优化的程序版本与优化版本的结果不同。以一种完全无法诊断的方式,您必须查看机器代码才能知道结果为何不同。
英特尔重新设计了它们的处理器来解决这个问题,SSE指令集使用与内存格式相同的位数进行计算。然而,这种技术演进很慢,重新设计编译器的代码生成器和优化器需要大量投资。三大C++编译器都已经过渡到使用新技术了。但例如.NET Framework中的x86 Jitter仍在生成FPU代码,它永远都会这样做。
然后有系统误差,转换和计算的必然副作用是精度损失。首先是转换,人们使用十进制数字,但处理器使用二进制。我们使用的好看的圆整数,如0.1不能转换为处理器上的漂亮圆整数。0.1是10的幂次之和,但没有有限的2的幂次之和可以产生相同的值。转换它会产生无限多个1和0,就像你无法完美地写下10/3一样。因此,它需要被截断以适合处理器,并产生一个从小数值偏离+/-0.5 bit的值。
计算出现错误。乘法或除法会将结果的位数加倍,四舍五入后将其放回存储值中会产生+/-0.5位误差。减法是最危险的操作,可能会导致失去很多有效数字。例如,计算1.234567f - 1.234566f,则结果只剩下1个有效数字。这是一个垃圾结果。在数值算法中,对接近相同值的数字之间的差进行求和是非常常见的。
过多的系统误差最终是数学模型的缺陷。举个例子,您永远不希望使用高斯消元法,因为它对精度非常不友好。并始终考虑备选方案,LU分解是一种优秀的方法。然而,在构建模型时,数学家通常没有参考预期结果的精度。像《数值分析》这样的普通书籍也没有足够关注精度,尽管它通过提出更好的模型间接地引导您远离糟糕的模型。最终,程序员经常会被问题所困扰。如果那很容易,那么任何人都可以做到,我就失去了一份高薪的工作 :)

啊,维基百科。他们在下面只对了一点点,是7.225个数字。 - Hans Passant
这是浮点数的一个重要部分,我仍在努力理解其中的最小保证精度。它所说的7.225个十进制数字的部分,称为单精度浮点格式的总精度。总精度是什么意思?它是绝对最小精度吗?还是平均精度或其他什么?如果您知道答案,是否也可以分享一下关于这个7.225值的参考资料,以解释它的真实含义?这是我在浮点数理论中遇到的最后一个问题。 - Wandering Fool
1
这是最大的保证精度。没有最小值,我指出了计算如何失去精度。在匆忙中进行一些减法运算时,总是因为其他计算而舍入半个比特。 - Hans Passant
这只是简单的数学计算。 浮点数在尾数中有24个二进制位,因此可以表示pow(2,24)个不同的值。 这相当于log10(16777216)= 7.2个十进制数字。 - Hans Passant
2
每次计算都会因为将结果四舍五入并适应24位而失去精度。因此,如果进行乘法运算,则结果精确到23.5个二进制位+/- 0.5个二进制位。使用相同的简单数学,现在您有7.07个十进制数字的精度。再进行一次乘法,您现在有23个二进制位+/- 1个二进制位或6.92个十进制数字的精度。这种情况会随着计算次数的增加而降低,减法速度更快。没有最小值,因为它完全取决于计算。 - Hans Passant
显示剩余7条评论

5
浮点变量的类型定义了可以表示什么范围的值以及有多少个小数位(!)。由于十进制和二进制小数之间没有整数关系,因此十进制小数实际上是一个近似值。
其次,另一个问题是执行精度算术运算。想一想 1.0/3.0 或 PI。这些值不能用有限数量的数字表示——无论是十进制还是二进制。因此,这些值必须舍入以适应给定的空间。可用的小数位数越多,精度就越高。
现在想象一下应用多个这样的操作,例如 PI/3.0。这将需要舍入两次:PI本身不是精确的,结果也不是。如果重复这样做,就会损失两次精度,而且情况会变得更糟。
所以,回到float和double:根据标准(C11,Annex F,也适用于其他部分),float可用的位较少,因此舍入的精度将比double低。想象一下有两位小数位数的十进制数(m.ff,称为float)和四位小数位数的十进制数(m.ffff,称为double)。如果所有计算都使用double,则可以进行更多的操作,直到您的结果只有2个正确的小数位数,而如果您已经从float开始,即使float结果足够,也会比较少。
请注意,在某些(嵌入式)CPU上,如ARM Cortex-M4F,硬件FPU仅支持folat(单精度),因此double算术将更加昂贵。其他MCU根本没有硬件浮点计算器,因此必须通过软件模拟(非常昂贵)。在大多数GPU上,float执行起来也比double便宜得多,有时甚至高达10倍以上。

5

存储器以二进制形式精确计数,正如其他答案所解释的那样。

需要知道的一件事是,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.
}

更复杂的计算更可能发生。

返回翻译后的文本:True。注意:FLT_EVAL_METHOD 表示使用更高精度/范围的浮点类型。 - chux - Reinstate Monica

4
我将在此添加一些离题的答案,并说由于您已将这个问题标记为C ++,因此浮点数据的精度没有任何保证。绝大多数实现在实现其浮点类型时使用IEEE-754,但这并不是必需的。 C ++语言要求的唯一事情是(C ++规范§3.9.1.8):
有三种浮点类型:float、double和long double。类型double提供的精度至少与类型float相同,类型long double提供的精度至少与类型double相同。类型float的值集合是类型double的值集合的子集;类型double的值集合是类型long double的值集合的子集。浮点类型的值表示是实现定义的。整数和浮点类型统称算术类型。标准模板std::numeric_limits(18.3)的特化应指定每种算术类型的最大和最小值,以供实现使用。

3
“there is no guarantee whatsoever about precision of floating point data” 这句话并没有考虑到 C 规范中的 DBL_DIG 等变量。它们有效地描述了 float, double 等变量的最小十进制精度(在§5.2.4.2.2中有明确定义)。此外,这个值至少为10。因此,C程序可以确信 double 变量至少保证有10个十进制数字的精度。 - chux - Reinstate Monica

3
存储float和double数值所需的空间是固定的,然而有用精度的相对值通常是不确定的。例如,float类型通常变化在2的23次方到2的24次方之间,而double类型则是2的52次方到2的53次方之间。接近零值的精度不够好,第二个最小的正值是最小值的两倍,而最小值又是无限大的。不过,在大部分数值范围内,精度会按照上述方式变化。
需要注意的是,虽然实现绝对精度变化小于二倍的类型通常是不切实际的,但是精度变化有时会导致计算结果比预期的不够准确。例如,考虑16777215.0f + 4.0f - 4.0f。这些值都可以在同一尺度下精确地表示为float,而大值的最接近值与其相差约为16777215的倒数。但是,因为第一个加法的结果位于float范围中的某个位置,而在该位置上数值仅相差约为8388610,所以结果被舍入为16777220。因此,减去4后的结果为16777216而不是16777215。对于大多数接近16777216的float值,加上4.0f并减去4.0f将不会改变原始值,但是在临界点附近的精度变化会导致结果在最低位上比预期多一个位。

0

这个问题的答案既简单又复杂。这些数字是以二进制形式存储的。根据是浮点数还是双精度浮点数,计算机使用不同数量的二进制来存储数字。您获得的精度取决于您的二进制。如果您不知道二进制数是如何工作的,建议您查阅相关资料。但简单地说,有些数字需要比其他数字更多的1和0。

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


“实际精度”是二进制精度,且不可改变。将其转换为不反映实际存储的形式时,其精度不能被称为“实际精度”。 - Pete Kirkham

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接