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

Why double can store bigger numbers than unsigned long long?

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

在 unsigned long long 中,所有 64 位都用于存储值,另一方面,double 的符号为 1,指数为 11,尾数为 52。即使用于尾数的 52 位将用于存储没有浮点数的十进制数,它仍然有 63 位 ...

但是 LLONG_MAX 明显小于 DBL_MAX ...

为什么?

原因是 unsigned long long 将存储 精确的 整数,而 double 存储一个尾数(具有有限的 52 位精度)和一个指数。

这允许 double 存储非常大的数字(大约 10308)但不完全。 double 中有大约 15(将近 16)个有效的十进制数字,其余 308 个可能的小数是零(实际上未定义,但您可以假设 "zero" 以便更好地理解)。
一个unsigned long long只有19位数字,但每一位都是精确定义的。

编辑:
在回复下面的评论 "how does this exactly work" 时,您有 1 位符号,11 位指数和 52 位尾数。尾数在开头有一个隐含的“1”位,未存储,因此 有效地 你有 53 个尾数位。 253 是 9.007E15,所以你有 15,几乎 16 个十进制数字可以使用。
指数有一个符号位,范围从-1022到+1023,用于缩放(二进制左移或右移)尾数(21023大约是10307,因此存在范围限制),因此非常小和非常大的数字同样可以使用这种格式。
但是,当然,您可以表示的所有数字都只能具有适合 matissa 的精度。

总而言之,浮点数不是很直观,因为 "easy" 十进制数根本不一定表示为浮点数。这是因为尾数是二进制的。例如,可以(并且很容易)以完美的精度表示高达几十亿的任何正整数,或者像 0.5 或 0.25 或 0.0125 这样的数字。
另一方面,也可以表示像 10250 这样的数字,但只是近似值。事实上,你会发现 10250 和 10250+1 是同一个数字(等等,什么???)。这是因为虽然您可以轻松拥有 250 位数字,但您没有那么多 重要的 位数字(将 "significant" 读作 "known" 或 "defined")。
此外,表示像 0.3 这样看似简单的东西 只是近似的,即使 0.3 甚至不是 "big" 数字。但是,你不能用二进制表示 0.3,而且无论你附加什么二进制指数,你都不会找到任何结果正好是 0.3 的二进制数(但你可以非常接近)。

一些 "special values" 保留给 "infinity"(正面和负面)以及 "not a number",因此您 非常少 比总的理论范围。

另一方面,

unsigned long long 不以任何方式解释位模式。您可以表示的所有数字都只是位模式所表示的确切数字。每个数字的每个数字都是精确定义的,没有缩放。

IEEE754 浮点值可以存储更大的范围 数字,仅仅是因为它们牺牲了精度。

我的意思是 64 位整数类型可以表示其范围内的每个值,但 64 位双精度类型不能。

例如,尝试将 0.1 存储到双精度数实际上不会 0.1,它会给你类似的东西:

0.100000001490116119384765625

(这实际上是最接近的 单精度 精度值,但同样的效果将适用于双精度)。


但是,如果问题是 "how do you get a larger range with fewer bits available to you?",那只是其中一些位用于缩放值。

经典示例,假设您有四位十进制数字来存储一个值。使用整数,您可以表示 00009999 之间的数字。该范围内的精度是完美的,您可以表示每个整数值。

但是,让我们使用浮点数并使用最后一位数字作为比例,以便数字 1234 实际上代表数字 123 x 10<sup>4</sup>.

所以现在你的范围是从0(代表00000009)到999,000,000,000(代表9999999 x 10<sup>9</sup>).

但是您无法表示该范围内的每个数字。比如123,456是不能表示的,你能得到的衣橱里的数字是1233,它给你的是123,000。而且,事实上,整数值的精度为四位,现在只有三位。

这就是 IEEE754 的基本工作原理,牺牲了范围的精度。

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 或更少。

也许您觉得 "storing a number in N bits" 是最基本的东西,其实有多种方法可以做到。事实上,更准确的说法是我们 表示 N 位的数字,因为其含义取决于我们采用的约定。原则上,我们可以采用任何我们喜欢的约定,让不同的 N 位模式代表数字。有用于 unsigned long long 和其他整数类型的二进制约定,以及用于 double 的尾数+指数约定,但我们也可以定义我们自己的(荒谬的)约定,其中,例如,所有位都为零表示您想要指定的任何巨大数字。在实践中,我们通常使用允许我们使用我们 运行 我们的程序所在的硬件有效地组合(加、乘等)数字的约定。

也就是说,您的问题必须通过比较最大的二进制 N 位数和 2^exponent * mantissa 形式的最大数来回答,其中 exponent mantissa 是 E-和 M 位二进制数(尾数开头隐含 1)。即 2^(2^E-1) * (2^M - 1),通常确实远大于 2^N - 1

免责声明

这是为了提供一个关于浮点编码如何工作的简单易懂的解释。这是一种简化,它不涵盖真正的 IEEE 754 浮点标准的任何技术方面(归一化、带符号的零、无穷大、NaN、舍入等)。不过,这里提出的思路是正确的。


理解浮点数的工作原理受到严重阻碍,因为计算机使用基数 2 中的数字,而人类不容易处理它们。我将尝试使用基数 10.

来解释浮点数是如何工作的

让我们使用符号和基数 10 数字(即我们每天​​使用的从 09 的常用数字)构造一个浮点数表示法。

假设我们有 10 个正方形单元格,每个单元格可以包含一个符号(+-)或一个十进制数字(0123456789).

我们可以用这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)之间的有符号值。此外,它可以表示这些限制之间的每个整数值。更重要的是,每个值都有一个表示,而且是准确的。

浮点表示可以存储 -999,999+999,999 之间的尾数 (M) 和 -99 之间的指数 (E) 的有符号值和 +99.

它可以存储-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,654E会是+3([=63的后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 和 [=73] 之间的所有值生成相同的表示=](整数或实数值,没关系)。

结论

对于大值(大于M的最大值),浮点表示法在它能表示的数字之间有间隙。随着 E 值的增加,这些差距变得越来越大。

“浮点数”的整个思想就是存储十几个最有代表性的数字(数字的开头)和数字的大小。

我们以光速为例。它的价值约为300,000 km/s。如此庞大,出于大多数实际目的,您不关心它是 300,000.001 km/s 还是 300,000.326 km/s.

事实上,它甚至没有那么大,更好的近似是 299,792.458 km/s.

浮点数提取了光速的重要特征:其大小为数十万km/s(E=5),其值为3(百数以千计 km/s).

speed of light = 3*10^5 km/s

我们的浮点表示可以近似为:299,792 km/s (M=299,792, E=0).

What kind of magic is happening ???

让你代表101位数字的同一种魔法

10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 

1.0 * 10<sup>100</sup>

它只是以 2 为基数,而不是以 10 为基数:

0.57149369564113749110789177415267 * 2<sup>333</sup>

此表示法允许您以紧凑的方式表示非常大(或非常小)的值。不是存储每个数字,而是存储 significand(a.k.a。尾数或分数)和 exponent。这样,可以用仅占用 64 位的格式表示数百位十进制数字。

它是允许浮点数表示如此大的范围值的指数。指数值1024只需要10位来存储,而2<sup>1024</sup>是一个308位的数。

权衡是并非每个值都可以准确表示。对于 64 位整数,02<sup>64</sup>-1(或 -2<sup>63</sup>2<sup>63</sup>-1) 有一个精确的表示。由于多种原因,浮点数并非如此。首先,你只有这么多位,只能给你这么多位数的精度。例如,如果您只有 3 位有效数字,那么您不能表示 0.123 和 0.124、或 1.23 和 1.24、或 123 和 124、或 1230000 和 1240000 之间的值。当您接近范围的边缘时,可表示值之间的差距变大。

其次,就像有些值不能用有限的数字表示一样(3/10给出了非终止序列0.33333...<sub>10</sub>),有些值无法用有限的位数表示(1/10给出了非终止序列1.100110011001...<sub> 2</sub>)。