移位后浮点数的位布局如何给我某些值?

How does the bit layout of the floats give me certain values after bit shifting?

出于好奇并想了解更多有关浮点的知识,我运行编写了以下 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 的最后一个案例有点有趣(并且与 float 格式有关)。

(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(这部分是为了我自己的利益 - 每次我解释它我自己都会更深入地理解它).

首先,一个基本事实——你不能将无限数量的实数值压缩到有限数量的比特中。二进制浮点格式只能存储 approximations 除了极少数实数值;唯一可以 精确 表示的值是不超过类型精度的 2 的幂之和(详见下文)。

浮点值x由模型表示

<em>x</em> = <em>s * b<sup>e</sup> * Σ(k=1,p) (f<sub>k</sub> * b<sup>-k</sup>)</em>

哪里

  • s 是符号 (+/- 1)
  • b为基数(2为二进制浮点数,10为十进制浮点数等)
  • p是精度(基数-b位数),
  • e是指数值
  • <em>f<sub>k</sub></em>k尾数的第

例如,让我们看看如何用二进制浮点数表示值 3.14159。二进制表示我们的基数b2,所以我们的有效数字只能包含数字01。因此,与其将 3.14159 表示为 10 的幂之和 (3 * 10<sup>0</sup> + 1 * 10<sup>- 1</sup> + 4 * 10<sup>-2</sup> + ...), 我们需要将其表示为2的幂之和.

我们可以从重复除以 2 开始,直到得到小于 1 的值;两次除以 2(即除以 4)得到 0.7853975(稍后我们会将这些 2 相乘)。现在我们需要将其表示为二进制分数。

1 * 2<sup>-1</sup>0.1<sub>2</sub>,或 0.5 十进制。 1 * 2<sup>-1</sup> + 1 * 2<sup>-2</sup>0.11<sub>2</sub>,或 0.75 十进制。因此,只要它们的总和小于或等于 0.7853975,我们就会继续添加位。半小时后,使用 Excel 电子表格,我得到

0.1100100100001111110011111000000011011100001100111<sub>2</sub>

所以要在我们的二进制浮点模型中表达3.14159,我们可以写

1 * 2<sup>2</sup> * (1 * 2<sup>-1</sup> + 1 * 2<sup>-2</sup> + 0 * 2<sup>-3</sup> + 0 * 2<sup>-4</sup> + 1 * 2<sup>-5</sup> + ... )

更紧凑地表示为

1 * 2<sup>2</sup> * 0.1100100100001111110011111000000011011100001100111<sub>2</sub>

记得我们之前将有效数除以 4,所以我们将它乘以 2<sup>2</sup>。然而,在我们继续之前,我们要规范化那个分数,使得小数点左边的数字不为零;我们可以通过乘以 2(给我们一个 1.570795 的有效数)来做到这一点:

1 * 2<sup>1</sup> * 1.100100100001111110011111000000011011100001100111<sub>2</sub>

所以,这就是我们在二进制浮点模型中表示 3.14159 的方式 - 我们实际上如何 存储 它?

浮点格式各不相同(大多数系统使用 IEEE-754,但也有一些不使用),但它们几乎都做同样的事情 - 它们为符号保留一位,为符号保留一些位数指数,以及有效数字的一些位数。 IEEE-754 单精度 (float) 格式如下所示:

 3 32222222 2221111111111
 1 09876543 21098765432109876543210
+-+--------+-----------------------+
| |        |                       |
+-+--------+-----------------------+
 ^ ^        ^
 | |        |
 | |        +----------------------- significand
 | +-------------------------------- exponent
 +---------------------------------- sign bit

符号保留1位(0表示正数,1表示负数),8位保留指数,23位保留尾数。有效数中小数点之前的前导数字未明确存储 - 对于标准化值假定为 1,对于 subnormal/non-normal 值假定为 0(我们不会在此处讨论)。

我们不为指数保留第二个符号位 - 相反,我们偏移指数值。对于 8 位指数,00000000<sub>2</sub> 表示 -127(由 IEEE-754 保留用于零值或非正规值),011111111<sub>2</sub>表示0,10000000<sub>2</sub>表示 1,而 11111111<sub>2</sub> 表示 128(为 +/- 无穷大或 NaN 保留) .

所以我们将我们的值编码为

+-+--------+-----------------------+
|0|10000000|10010010000111111001111|  assumes leading 1 before radix point
+-+--------+-----------------------+

现在,我们遇到了精度问题 - 32 位 float 只能存储小数点后有效数的前 23 位,这意味着我们只能存储小数 1.10010010000111111001111<sub>2</sub>。不幸的是,我们用了 50 位来完全表示我们的二进制小数;我们已经失去了所需精度的 一半。因此,我们没有存储 3.14159,我们存储的是 3.1415<strong><em>8987998962</em></strong> .

但这是另一个问题 - 让我们在该有效数字上加 1:

+-+--------+-----------------------+
|0|10000000|10010010000111111010000|
+-+--------+-----------------------+

这是我们可以用这种格式表示的下一个更高的值。这加起来为 3.1415<strong><em>9011840820</em></strong>,这意味着我们不能在这两者之间存储任何值。现在,这里的差别很小——大约是 10-6。但是,随着值的大小增加(通过增加指数),差距会变大。不是将我们的原始分数乘以 2<sup>1</sup>,而是将其乘以 2<sup>23</sup> (8388608):

+-+--------+-----------------------+
|0|10010110|10010010000111111001111|
+-+--------+-----------------------+

这给了我们 13176783.0 的价值。同样,我们将 1 添加到有效数:

+-+--------+-----------------------+
|0|10010110|10010010000111111010000|
+-+--------+-----------------------+

这给了我们 13176784.0。如您所见,可表示值之间的差距已经从百万分之一变为 1。一旦指数大于有效位数中的位数,您就会开始获得大于 1.

的间隙

最终,这就是为什么将 1.0 添加到 16777216.000000 不会更改值的原因 - 您不能在 32 位 float1。下一个可表示的值是 16777218.0。当指数为24时,数值之间的差距为2。当它是 25 时,差距是 4,并且差距大小随着指数的增加而不断加倍。

除非你有充分的理由不这样做,否则建议使用 double 而不是 float。作为一种更广泛的类型,它可以表示更广泛的值,并且比 float 具有更高的精度。如果您在您的程序中使用 double 而不是 float,它的行为将符合预期。

但就像 float 一样,double 只能表示无限小数量的实数值 恰好 并且可表示的值之间会有间隙。由于浮点值是近似值,所以浮点值的运算只是近似值,误差会累积。


  1. 好吧,事实上,要添加浮点值,您必须使它们的指数一致,并且通过向左移动 1.0 23 位数字,您基本上得到零。