为什么double可以存储比unsigned long long更大的数字?

15

问题是,我不太明白为什么 double 可以存储比 unsigned long long 更大的数字。 因为它们都是8个字节长,即64位。

在 unsigned long long 中,所有64位都被用来存储值,而另一方面,double 带有1个符号位、11个指数位和52个尾数位。即使使用了52位用于存储没有浮点的小数,它仍然拥有63位……

但 LLONG_MAX 比 DBL_MAX 显着更小……

为什么呢?


11
尺寸增加所得之处在于失去精度。 - Roger Rowland
7
指数的魔力。 - Peter - Reinstate Monica
2
有趣的是,我认为这两种格式都可以区分大约相同数量的数字。通常,浮点数会有一些无效的位模式,使它们稍微劣势一些。 - Peter - Reinstate Monica
2
是的,我非常确定告诉过你如何找出答案了。我提供的链接文章展示了11位指数如何将小数部分进行比例缩放,从而大幅增加范围(但会牺牲精度)。如果你阅读了这篇文章仍然不理解,你应该提出另一个问题,详细说明你遇到的问题。我已经更新了答案,提供了一些关于比例缩放的信息,这可能会对你有所帮助,但你可能需要花时间深入研究它的工作原理。 - paxdiablo
3
必要链接:计算机科学家应该了解的浮点数算术知识(PDF文件)。 - John Bode
显示剩余8条评论
6个回答

33
原因在于unsigned long long只存储精确的整数,而double则存储一个底数(有限的52位精度)和指数。
这使得double可以存储非常大的数字(约为10的308次方),但不是精确的。在double中,您有大约15(几乎16)个有效十进制数字,其余308个可能的小数位都是零(实际上是未定义的,但您可以假设为“零”以更好地理解)。
unsigned long long只有19位数字,但每一位数字都是精确定义的。 编辑:
回复下面的评论“这个到底是怎么工作的”,你有1位符号位,11位指数和52位底数。基数在开头有一个隐含的“1”位,没有存储,因此实际上你有53个底数位。2的53次方是9.007E15,因此您可以使用15个几乎16个小数位。
指数有一个符号位,并且可以从-1022到+1023范围内变化,用于缩放(二进制向左或向右移位)底数(2的1023次方约为10的307次方,因此范围有限),因此非常小和非常大的数字同样可能使用此格式。
但是,所有您可以表示的数字仅具有适合于底数的精度。
总之,浮点数并不是非常直观,因为“易于理解”的十进制数字未必可用浮点数完全表示出来。这是由于底数是二进制的原因。例如,任何小于几十亿的正整数或0.5或0.25或0.0125这样的数字都可以用完全精确性表示。另一方面,也可以表示像10250这样的数字,但只能近似表示。实际上,你会发现10250和10250+1是相同的数字(等等,什么?)。这是因为尽管你可以轻松拥有250位数字,但你没有那么多有效数字(把“有效”读作“已知”或“定义”)。 此外,即使是像0.3这样看似简单的东西,也只能近似地表示,尽管0.3甚至不是一个“大”的数字。但是,你无法用二进制表示0.3,无论你附加什么二进制指数,都找不到任何二进制数字会得到完全等于0.3的结果(但你可以非常接近)。
一些“特殊值”被保留为“无穷大”(正无穷和负无穷),以及“非数字”,因此你的范围比理论范围略微少一些。
另一方面,无符号长整型unsigned long long不对比特模式进行任何解释。所有你可以表示的数字都只是表示该比特模式所代表的确切数字。每个数字的每一位都是精确定义的,没有进行任何缩放。

@denis631 这只是一个关于 double 的 15-16 个有效十进制数字和 unsigned long long 的 19 个精确数字的陈述的快速演示:http://ideone.com/8igfpt - axiac
我不同意“unsigned long long ... 在任何方式下都不解释位模式”的说法。将零和一的序列映射到实数的每个系统都涉及解释。二进制位置制只是对程序员特别熟悉而已。 - Patricia Shanahan
@PatriciaShanahan:当你说“实数”时,我认为你指的是“实际数字”,而不是实数,因为long是整型。如果是这样,那么你的说法是错误的。二进制表示法和十进制表示法在其表示能力方面是完全等效的系统。从一个系统转换到另一个系统在任何方向上都是无损的。 - kmote
@kmote 我所说的“实数”是指有理数柯西序列的任何极限。有许多方法可以将实数的有限子集表示为例如64个零和一的序列,并具有不同的解释。将零和一解释为位值系统中的二进制数字只是其中之一。唯一特殊的是它在编程中非常常用。 - Patricia Shanahan

17

IEEE754浮点数之所以能够存储更大的数字范围,是因为它们牺牲了精度。

也就是说,一个64位整数类型可以表示其范围内的每个值,但64位双精度浮点数却不能。

例如,试图将0.1存储到双精度浮点数中并不会真正给你0.1,而是会给你类似这样的结果:

0.100000001490116119384765625

(实际上这是最接近精度值,但双精度也适用相同的效果。)


但是,如果问题是“如何在可用的较少位数中获得更大的范围?”,那就很简单了,其中一些位被用于缩放值。

一个典型的例子,假设你有四个十进制数字来存储一个值。对于整数,你可以表示从00009999(包括)的数字。在该范围内的精度是完美的,你可以表示每个整数值。

然而,现在让我们使用浮点数,将最后一位数字用作比例因子,使数字1234实际表示数字123 x 104

所以,现在你的范围从0(由00000009表示)到999,000,000,000(由9999表示为999 x 109)。

但是,在该范围内不能表示每个数字。123,456例如无法表示,最接近的是数字1233,它给出123,000。实际上,在整数值具有四个数字的精度的情况下,现在你只有三个数字。

这就是IEEE754的基本工作原理,为了获得更大的范围而牺牲精度。


6

免责声明

本文旨在提供易于理解的浮点数编码工作原理的解释。这是一种简化,不涵盖任何真实IEEE 754浮点数标准(规范化、有符号零、无穷大、NaNs、舍入等)的技术方面。然而,此处提出的想法是正确的。


由于计算机使用二进制,在理解浮点数的工作原理时受到了严重阻碍,而人类很难处理它们。我将尝试使用十进制来解释浮点数的工作原理。

让我们构建一个使用符号和基于10位数字(即日常使用的常规数字09)的浮点数表示。

假设我们有10个正方形单元格,每个单元格可以容纳符号(+-)或十进制数字(0, 1, 2, 3, 4, 5, 6, 7, 89)。

我们可以使用10个数字存储有符号整数。一个数字用于符号,9个数字用于值:

sign -+   +-------- 9 decimal digits -----+
      v   v                               v
    +---+---+---+---+---+---+---+---+---+---+
    | + | 0 | 0 | 0 | 0 | 0 | 1 | 5 | 0 | 0 |
    +---+---+---+---+---+---+---+---+---+---+

这是数值 1500 表示为整数的方式。

我们也可以用它们来存储浮点数。例如,7个数字表示尾数,3个数字表示指数:

  +------ sign digits --------+
  v                           v
+---+---+---+---+---+---+---+---+---+---+
| + | 0 | 0 | 0 | 1 | 5 | 0 | + | 0 | 1 |
+---+---+---+---+---+---+---+---+---+---+
|<-------- Mantissa ------->|<-- Exp -->|       

这是将 1500 表示成浮点数的可能方式之一(使用我们的10个十进制数字表示法)。
尾数(M)的值为+150,指数(E)的值为+1。上述表示的值为:
V = M * 10^E = 150 * 10^1 = 1500

范围

整数表示可以存储介于-(10^9-1)-999,999,999)和+(10^9-1)+999,999,999)之间的有符号值。此外,它可以表示这些限制之间的每个整数值。更重要的是,每个值都有一个单一的表示,并且是精确的。

浮点表示法可以存储幂指数(E)介于-99+99之间,尾数(M)介于-999,999+999,999之间的有符号值。

它可以存储介于-999,999*10^99+999,999*10^99之间的值。这些数字具有105位数字,比上面表示为整数的最大数字的9位数字要多得多。

精度损失

请注意,对于整数值,M存储值的符号和前6位数字(或更少),而E是未适合M的数字的位数。

V = M * 10^E

让我们尝试使用浮点数编码来表示 V = +987,654,321
由于M的范围仅限于+999,999,因此它只能存储+987,654,而E将为+3V的最后3位数字无法适应M)。
将它们组合在一起:
+987,654 * 10^(+3) = +987,654,000

这不是我们原始的V值,而是使用这种表示法得到的最佳近似值。
需要注意的是,在(包括)+987,654,000+987,654,999之间的所有数字都使用相同的值(M=+987,654, E=+3)进行近似。此外,无法为大于+999,999的数字存储小数位数。
作为一般规则,对于大于M的最大值(+999.999)的数字,该方法对于+999,999*10^E+999,999*10^(E+1)-1之间的所有值(整数或实数值,没有关系)产生相同的表示。
结论
对于大值(大于M的最大值),浮点表示法在其所能表示的数字之间存在间隙。这些间隙随着E值的增加而变得越来越大。
"浮点数"的整体思想是存储十几个最具代表性的数字(数字的开头)和数字的数量级。
以光速为例。它的值约为300,000 km/s。由于如此巨大,对于大多数实际目的而言,您不关心它是否为300,000.001 km/s300,000.326 km/s
事实上,它甚至不那么大,更好的近似值是299,792.458 km/s
浮点数提取了光速的重要特征:其数量级为数十万公里/秒(E=5),其值为3(数十万公里/秒)。
speed of light = 3*10^5 km/s

我们的浮点数表示可以近似为:299,792千米/秒M=299,792E=0)。

3
什么样的魔法正在发生??
与你所代表的101位数一样的魔力。
10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 

as

1.0 * 10100

这只是在进行二进制计算时,使用的是基数2而非10:

0.57149369564113749110789177415267 * 2333.

此表示法能够以紧凑的方式表示非常大(或非常小)的值。您只需存储有效数字(也称为尾数或分数)和指数,而不是每个数字。因此,长度为数百个十进制数字的数字可以用仅占用64位的格式表示。

正是指数使浮点数能够表示如此大范围的值。指数值1024只需要10位即可存储,但21024是一个308位数字。

不过,这种表示法的缺点是并不能精确地表示每个值。使用64位整数,介于0264-1之间的每个值(或者介于-263263-1之间的每个值)都有精确的表示法。然而对于浮点数来说,并非如此,原因有几个。首先,您只有那么多位数,因此只能表示有限的小数位数。例如,如果您只有三个有效数字,则无法表示介于0.123和0.124、1.23和1.24、123和124、1230000和1240000之间的值。随着您接近您的范围边缘,可表示值之间的差距变得更大。

其次,就像有些值不能在有限的十进制数字中表示一样(3/10给出非终止序列0.33333...10),有些值也不能在有限的位数中表示(1/10给出非终止序列1.100110011001...2)。


2
也许你觉得“用N个比特存储一个数字”是一件基础的事情,实际上有各种实现方式。事实上,更精确的说法是我们用N个比特来表示一个数字,因为其含义取决于我们采用的约定。我们原则上可以采用任何约定,使得不同N位模式代表不同的数字。有二进制约定,如用于unsigned long long和其他整数类型的约定,还有用于double的尾数+指数约定,但我们也可以定义自己的(荒谬的)约定,例如所有比特都是零意味着任何你想指定的巨大数字。在实践中,我们通常使用允许我们使用我们运行程序的硬件有效地组合(相加、相乘等)数字的约定。
话虽如此,要回答你的问题就必须比较最大的二进制N位数与形式为2^exponent * mantissa的最大数字,其中exponentmantissa是E位和M位二进制数(在尾数开头隐含1)。这是2^(2^E-1) * (2^M - 1),通常确实远大于2^N - 1

0

以下是 Damon 和 Paxdiablo 解释的一个小例子:

#include <stdio.h>

int main(void) {
    double d = 2LL<<52;
    long long ll = 2LL<<52;
    printf("d:%.0f  ll:%lld\n", d, ll);
    d++; ll++;
    printf("d:%.0f  ll:%lld\n", d, ll);
}

输出:

d:72057594037927936  ll:72057594037927936
d:72057594037927936  ll:72057594037927937

使用移位操作51位或更少的情况下,这两个变量将以相同的方式递增。


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