问题描述
出于好奇并想了解有关浮点的更多信息,我运行了以下 C 代码:
#include <stdio.h>
int main() {
float a = 1.0 + ((float) (1 << 22));
float b = 1.0 + ((float) (1 << 23));
float c = 1.0 + ((float) (1 << 24));
printf("a = %.6f\n",a);
printf("b = %.6f\n",b);
printf("c = %.6f",c);
}
结果是:
a = 4194305.000000
b = 8388609.000000
c = 16777216.000000
我不明白为什么会得到这些结果。谁能解释为什么 a、b 和 c 的位布局会导致每个值都是它的样子?我是位移和浮点数的新手,如果有明确的解释,将不胜感激。谢谢。
解决方法
(1 << 22)
是一个等于
的整数值2^22 = 4194304
然后您通过执行 (float) (1 << 22)
将其转换为浮点数,这会给您相同的值
4194304.0
然后你加上 1.0 得到结果 4194305.0
同样适用于其他情况。
所以这不是关于“浮点数的布局”——而是关于整数的布局以及从整数到浮点数的转换。
但是,最后一个使用 1 << 24
的情况有点有趣(并且与浮点格式有关)。
(1 << 24) is 16777216
并且可以转换为相同的浮点值,即
16777216.0
但是当你这样做
1.0 + 16777216.0
你仍然得到
16777216.0
原因是浮点数的精度有限(即并非所有数字都可以以浮点格式表示)。值 16777217.0 无法以浮点格式显示,因此将 1.0 添加到 16777216.0 仍然为您提供 16777216.0
顺便说一句:有几种舍入模式(参见例如 https://en.wikipedia.org/wiki/Floating-point_arithmetic#Rounding_modes),因此当无法以浮点格式显示精确结果时,您需要了解您的系统舍入模式以确定将使用哪个值而不是确切的结果。
,我将扩展 4386427 的答案,并深入探讨为什么 16777216.0 + 1 == 16777216.0
(这部分是为了我自己的利益 - 每次我解释它时,我自己都会更深入地理解它)。
首先,一个基本事实 - 您不能将无限数量的实数值压缩到有限数量的位中。二进制浮点格式只能存储近似值,除了极少量的实数值;唯一可以精确表示的值是不超过类型精度的 2 的幂之和(更多内容见下文)。
浮点值x
由模型表示
x = s * be * Σ(k=1,p) (fk * b-k)
哪里
-
s
是符号 (+/- 1) -
b
是基数(2
表示二进制浮点数,10
表示十进制浮点数等) -
p
是精度(有效数中基数-b
位数), -
e
是指数值 -
fk
是有效数中的k
位
举个例子,让我们看看如何用二进制浮点数表示值 3.14159
。二进制意味着我们的基数 b
是 2
,所以我们的有效数只能包含数字 0
或 1
。因此,与其将 3.14159
表示为 10
(3 * 100 + 1 * 10-1 + 4 * 10-2 + ...
) 的幂之和,不如将其表示为 2 的幂之和。
我们可以从重复除以 2 开始,直到得到小于 1 的值;除以 2 两次(即除以 4)给我们 0.7853975
(我们稍后会乘以这些 2)。现在我们需要将其表示为二进制分数。
1 * 2-1
是 0.12
或 0.5
十进制。 1 * 2-1 + 1 * 2-2
是 0.112
,或 0.75
十进制。因此,我们不断添加位,只要它们的总和小于或等于 0.7853975
。半小时后,使用 Excel 电子表格,我得到
0.11001001000011111100111110000000110111000011001112
所以为了在我们的二进制浮点模型中表达 3.14159
,我们可以写
1 * 22 * (1 * 2-1 + 1 * 2-2 + 0 * 2-3 + 0 * 2-4 + 1 * 2-5 + ... )
更简洁地表示为
1 * 22 * 0.11001001000011111100111110000000110111000011001112
请记住,我们之前将有效数除以 4
,因此我们将其与 22
相乘。然而,在我们继续之前,我们将标准化那个分数,使得小数点左边的数字不为零;我们可以通过乘以 2 来实现(给我们一个 1.570795
的有效数):
1 * 21 * 1.1001001000011111100111110000000110111000011001112
所以,这就是我们在二进制浮点模型中表示 3.14159
的方式 - 我们实际上如何存储它?
浮点格式各不相同(大多数系统使用 IEEE-754,但也有一些不使用),但它们几乎都做同样的事情 - 它们为符号保留一位,为指数保留一些位数,以及一些有效数位。 IEEE-754 单精度 (float
) 格式如下所示:
3 32222222 2221111111111
1 09876543 21098765432109876543210
+-+--------+-----------------------+
| | | |
+-+--------+-----------------------+
^ ^ ^
| | |
| | +----------------------- significand
| +-------------------------------- exponent
+---------------------------------- sign bit
1 位为符号保留(0
为正,1
为负),8 位为指数保留,23 位为有效数保留。有效数中小数点之前的前导数字未明确存储 - 假定为 1 表示归一化值,0 表示次正常/非正常值(我们不会在这里讨论)。
我们不会为指数保留第二个符号位 - 相反,我们会抵消指数值。对于 8 位指数,000000002
代表 -127
(IEEE-754 为零或非正规值保留),0111111112
代表 0
,100000002
代表1
和 111111112
代表 128
(保留用于 +/- 无穷大或 NaN
)。
所以我们将我们的值编码为
+-+--------+-----------------------+
|0|10000000|10010010000111111001111| assumes leading 1 before radix point
+-+--------+-----------------------+
现在,我们遇到了精度问题 - 32 位 float
只能存储小数点后有效数的前 23 位,这意味着我们只能存储分数 1.100100100001111110011112
。不幸的是,我们用了 50 位来完全表示我们的二进制分数;我们已经失去了一半所需的精度。因此,我们不存储 3.14159
,而是存储 3.14158987998962
。
但这是另一个问题 - 让我们在有效数上加 1:
+-+--------+-----------------------+
|0|10000000|10010010000111111010000|
+-+--------+-----------------------+
这是我们可以用这种格式表示的下一个更高的值。这加起来为 3.14159011840820
,这意味着我们不能在这两者之间存储任何值。现在,这里的差异很小 - 大约为 10-6。但是,随着值的大小增加(通过增加指数),该差距会变得更大。与其将原始分数乘以 21
,不如将其乘以 223
(8388608
):
+-+--------+-----------------------+
|0|10010110|10010010000111111001111|
+-+--------+-----------------------+
这给了我们 13176783.0
的值。同样,我们在有效数上加 1:
+-+--------+-----------------------+
|0|10010110|10010010000111111010000|
+-+--------+-----------------------+
这给了我们13176784.0
。如您所见,可表示值之间的差距已经从百万分之一变成了 1
。一旦指数大于有效数中的位数,就会开始出现大于 1
的间隙。
这最终就是为什么将 1.0
添加到 16777216.000000
没有改变值 - 你不能在 32 位 16777217.0
中表示 float
1。下一个可表示的值是 16777218.0
。当指数为 24
时,值之间的差距为 2
。当它是 25
时,间隙为 4
,并且该间隙大小随着指数的增加而增加一倍。
除非您有充分的理由不这样做,否则建议使用 double
而不是 float
。作为更广泛的类型,它可以比 float
更精确地表示更广泛的值。如果您在程序中使用 double
而不是 float
,它会按预期运行。
但就像 float
一样,double
只能表示极少量的实际值完全,并且在可表示的值之间存在差距。由于浮点值是近似值,所以对浮点值的算术只是近似值,误差会累积。
- 嗯,还有一个事实,即要添加浮点值,您必须使它们的指数一致,并且通过将
1.0
向左移动 23 位,您基本上以零结束。