为啥 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
(这实际上是最接近的单精度值,但同样的效果也适用于双精度)。
但是,如果问题是“如何以更少的位数获得更大的范围?”,只是其中一些位用于缩放值。
经典示例,假设您有四个十进制数字来存储一个值。使用整数,您可以表示数字 0000
到 9999
(含)。该范围内的精度是完美的,您可以表示每个整数值。
但是,让我们使用浮点数并使用最后一位数字作为刻度,以便数字 1234
实际上代表数字 123 x 10<sup>4</sup>
。
所以现在您的范围是从0
(由0000
到0009
表示)到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
数字(即我们每天使用的从 0
到 9
的常用数字)构造一个浮点数表示。
假设我们有10
方形单元格,每个单元格可以包含符号(+
或 -
)或十进制数字(0
、1
、2
、3
、 4
、5
、6
、7
、8
或 9
)。
我们可以使用 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
将是+3
(V
的最后 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 位整数,0
和 2<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 更大的数字?的主要内容,如果未能解决你的问题,请参考以下文章