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

Posted

技术标签:

【中文标题】为啥 double 可以存储比 unsigned long long 更大的数字?【英文标题】:Why double can store bigger numbers than unsigned long long?为什么 double 可以存储比 unsigned long long 更大的数字? 【发布时间】:2015-05-05 12:16:12 【问题描述】:

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

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

但是 LLONG_MAX 明显小于 DBL_MAX ...

为什么?

【问题讨论】:

在尺寸上获得的东西会在精度上损失。 指数的魔力。 有趣的是,我假设这两种格式可以区分大约相同数量的数字。一般来说,一些位模式对浮点数无效,给他们带来一点劣势。 是的,我很确定告诉你如何找出答案,我链接到的文章展示了 11 位指数如何缩放小数部分以大幅增加范围(以精度为代价)。如果您阅读了帖子并且仍然不明白,您应该提出另一个问题,详细说明您遇到的问题。我已经用一些有关缩放的信息更新了答案,这可能会对您有所帮助,但您可能需要硬着头皮研究它的实际工作原理。 必填链接:What Every Computer Scientist Should Know About Floating Point Arithmetic(PDF 文件)。 【参考方案1】:

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

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

编辑: 在回复下面的评论“这到底是如何工作的”时,您有 1 位用于符号,11 位用于指数,52 位用于尾数。尾数在开头有一个隐含的“1”位,它没有被存储,所以实际上你有 53 个尾数位。 253 是 9.007E15,所以你有 15 个,几乎 16 个十进制数字可以使用。 指数有一个符号位,范围从 -1022 到 +1023,用于缩放(二进制左移或右移)尾数(21023 约为 10307 sup>,因此范围有限制),因此非常小的和非常大的数字同样可以使用这种格式。 但是,当然,您可以表示的所有数字都只有与 matissa 匹配的精度。

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

一些“特殊值”是为“无穷大”(正数和负数)以及“非数字”保留的,因此您的值小于总理论范围。 p>

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

【讨论】:

@denis631 只是关于double 的15-16 位有效十进制数字与unsigned unsigned long 的19 位精确数字的快速演示:ideone.com/8igfpt 我不同意“unsigned long long ... 不会以任何方式解释位模式”。 每一个系统将一系列0和1映射到一个实数都涉及到解释。二进制位置系统恰好是程序员特别熟悉的。 @PatriciaShanahan:当您说“实数”时,我假设您的意思是“实际数字”而不是 Real number,因为 long 是 integral type。如果是这样,你的断言就错了。就其表示能力而言,二进制符号和十进制符号是完全相同的系统。从一个到另一个的转换在任一方向上都是无损的。 @kmote 我的意思是“实数”,是指有理数的柯西序列的任何限制。有很多方法可以将实数的有限子集表示为例如序列64个0和1,有不同的解释。将零和一解释为位置系统中的二进制数字只是其中之一。它唯一的特别之处在于它在编程中非常常用。【参考方案2】:

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

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

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

0.100000001490116119384765625

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


但是,如果问题是“如何以更少的位数获得更大的范围?”,只是其中一些位用于缩放值。

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

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

所以现在您的范围是从0(由00000009 表示)到999,000,000,000(由9999 表示为999 x 10<sup>9</sup>)。

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

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

【讨论】:

【参考方案3】:

免责声明

这是试图提供一个易于理解的关于浮点编码如何工作的解释。这是一种简化,不涵盖真正的 IEEE 754 浮点标准的任何技术方面(归一化、有符号零、无穷大、NaN、舍入等)。但是,这里提出的想法是正确的。


由于计算机使用基数为2 的数字而人类不容易处理它们,这一事实严重阻碍了对浮点数如何工作的理解。我将尝试解释浮点数是如何使用 base 10 工作的。

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

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

我们可以使用 10 位数字来存储有符号整数。符号一位,数值九位:

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+99 之间的指数 (E) 的有符号值。

它可以存储-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/s 还是 300,000.326 km/s

其实也没那么大,更好的近似是299,792.458 km/s

浮点数提取光速的重要特征:其量级为数十万公里/秒(E=5),其值为3(数十万公里/秒)。

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

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

【讨论】:

【参考方案4】:

发生了什么魔法???

同样的魔法可以让你表示 101 位数字

10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 

作为

1.0 * 10<sup>100</sup>

这只是你在 base 2 中做的而不是 base 10:

0.57149369564113749110789177415267 * 2<sup>333</sup>

此表示法允许您以紧凑的方式表示非常大(或非常小)的值。不是存储每个数字,而是存储 significand(也称为尾数或分数)和 指数。这样,数百个十进制数字长的数字可以以仅占用 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>),有些值不能用有限位数表示(@ 987654332@ 给出非终止序列1.100110011001...<sub>2</sub>)。

【讨论】:

【参考方案5】:

也许您觉得“将一个数字存储在 N 位中”是基本的东西,但有多种方法可以做到这一点。事实上,更准确的说法是我们表示一个 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

【讨论】:

【参考方案6】:

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

【讨论】:

以上是关于为啥 double 可以存储比 unsigned long long 更大的数字?的主要内容,如果未能解决你的问题,请参考以下文章

数据类型unsigned char表示范围(存储值的范围)是多少,为啥(写出计算过程)

C++中float是怎么存储数据的?

MySQL基础知识总结

mysql 中unsigned

为啥 c 中的幂函数需要比预期更长的时间

如何使用 JNI 从本机 c 库将 double 和 unsigned int 返回到 java