掘金 后端 ( ) • 2024-05-07 18:16

hi,你好,我是猿java。

在现实生活中,我们对整数和小数的应用随处可见,比如算账、计数、时间、温度、体重等,并且,随着网络的迅速发展,这些场景似乎已经从线下迁移到网上。那么,这些数字是在计算机中是如何存储的呢?今天我们就来一探究竟。

在计算机中,通常用定点数和浮点数两种方式来存储实数。

定点数

定义

定点数,比较简单,从字面上理解为小数点固定的数。比如,100,3.14,200.08等等都可以看成是定点数。

通常意义上,定点数表示整数或小数,可以分为以下三种情况:

  • 纯整数:例如,400,小数点在最后一位,可以忽略
  • 纯小数:例如,0.68,小数点固定在最高位
  • 整数+小数:例如,3.14、9.18,小数点在指定某个位置

接下来,我们一起看看定点数的十进制和二进制的相互转换。

十进制整数转二进制

将十进制数转换为二进制数,通过不断地除以 2,直到商为 0。步骤:

  • 将十进制数除以2,记录商和余数
  • 将商再次除以2,记录新的商和余数
  • 重复这个过程,直到商为 0为止
  • 从下往上读取所有的余数,就得到了转换后的二进制数

比如:将十进制的 38转换成二进制

38 / 2 = 19 余 0
19 / 2 = 9  余 1
9  / 2 = 4  余 1
4  / 2 = 2  余 0
2  / 2 = 1  余 0
1  / 2 = 0  余 1

所以,(38)₁₀ 的二进制是 (100110)₂

二进制整数转十进制

将二进制数转换为十进制数,根据权值展开法,将每一位上的数字与其对应的权值相乘,然后将所有结果相加。权值是 2的幂,从右向左依次增加。

例如,将二进制数 100110转换成十进制:

1*2⁵ + 0*2⁴ + 0*2³ + 1*2² + 1*2¹ + 0*2⁰
= 32 + 0 + 0 + 4 + 2 + 0 
= 38

所以,(100110)₂ 的十进制是 (38)₁₀

十进制小数转二进制

十进制小数转二进制分两部分:整数部分转换上面已经讲解了,小数部分采用“乘2取整,从上到下顺序排列”。

例如,十进制小数 10.75 转为二进制小数:

# 整数部分
10 / 2 = 5 余 0
5  / 2 = 2 余 1
2  / 2 = 1 余 0
1  / 2 = 0 余 1

# 小数部分
0.75 \* 2 = 1.5 取整数部分 1
0.5  \* 2 = 1.0 取整数部分 1

所以,(10.75)₁₀ 的二进制是 (1010.11)₂

二进制小数转十进制

十进制小数转二进制分两部分,整数部分转为二进制上面已经讲解了,小数部分采用“乘2取整,从上到下顺序排列”。

例如,二进制小数转十进制小数:

# 整数部分
1*2³ + 0*2² + 1*2¹ + 0*2⁰ = 8 + 0 + 2 + 0 + 2 + 0 = 10

# 小数部分
1*2⁻¹ + 1*2⁻² = 0.5 + 0.25 = 0.75

# 整数部分 + 小数部分
10 + 0.75 = 10.75

所以,(1010.11)₂ 的十进制是 (10.75)₁₀

定点数的优缺点

优点

定点数的精度是固定的,可以根据需求进行灵活调整,这使得它们在需要固定精度的应用中非常有用。

缺点

定点数的精度是固定的,这意味着在处理大范围或者需要高精度的数据时,可能会丢失精度或者溢出。

比如,以 1个字节(8 bit)为例,假如约定前 4位表示整数部分,后 4位表示小数部分,因此,可以表达的最大数是:(1111.1111)₂=(15.9375)₁₀。

如果想要表示更大范围的值,怎么办?

  • 增加 bit:比如,使用 2个字节(16 bit)、4个字节(32bit),这样整数部分和小数部分都增加了,表示的数字范围也变大了。
  • 小数点右移:小数点右移,整数范围就变大了,因此整个数字范围就变大了,但是小数部分的精度就会越来越低。

因此,对于表示大范围或者高精度的数,定点数存在其局限性,这时候“浮点数”就派上用场了。

IEEE 754

在讲解浮点数之前,我们需要先了解一个很重要的标准:IEEE 754,它是 20世纪80年代以来最广泛使用的浮点数运算标准,为许多 CPU、浮点运算器和编程语言(比如 Java)所采用。

这个标准定义了以下规范:

  • 表示浮点数的格式(包括负零-0)与反常值(Denormal number);
  • 一些特殊数值(无穷 Inf与非数值 NaN),以及这些数值的浮点数运算;
  • 指明了四种数值修约规则和五种例外状况(包括例外发生的时机与处理方式);

另外,在 IEEE 754标准推出之前,各个计算机公司对于浮点数的表示没有一个业界通用的标准,这给数据交换、计算机协同工作造成了极大不便,直到 1985年,IEEE 组织推出了 IEEE 754浮点数标准,才结束了这混乱的局面。

什么是浮点数

浮点数,是相对定点数而言的,从字面上可以解释为:小数点在浮动的数。在计算机科学中,浮点数是一种用于表示带有小数点的实数的数据类型,通常采用了科学计数法表示。

如下例子,十进制小数 300.14,用科学计数法表示,可以有多种方式,整体看上去,小数点好像在浮动。

300.14 = 30.014 × 10¹
300.14 = 3.0014 × 10²
300.14 = 0.31004 × 10³

在 IEEE 754标准中,浮点数在内存中以二进制形式存储,分为四个部分:符号位、基数、指数部分和尾数部分。

  • 符号位(Sign bit):用于表示数值的正负。0表示正数,1表示负数。
  • 指数部分(Exponent):用于表示数值的大小范围。该部分存储的是一个无符号整数,通常采用偏移表示,即用实际指数加上一个固定偏移值。
  • 基数(Base):也称为进制或底数,常见的基数包括十进制(基数为10)、二进制(基数为2)、八进制(基数为8)、十六进制(基数为16)等。
  • 尾数部分(Mantissa/Significand):用于表示数值的精度和小数部分。在IEEE 754中,尾数总是处于[1,2)之间的一个小数,这样可以省略掉小数点前面的 1,从而节省了一个 bit。 注意:significand 和 mantissa 都是用来指代浮点数中的尾数部分,只不过 mantissa是早期计算机的叫法。两个术语可以互换,用来表示浮点数中的尾数部分。

下图以 300.14为例展示了一个浮点数的构成:

image.png

精度和范围

在IEEE 754标准中规定了 4种主要的浮点数类型:单精度浮点数(float)、双精度浮点数(double)、延伸单精确度以及延伸双精确度。

单精度浮点数(float)

单精度浮点数占用 32位(4个字节),其中,符号占 1位,指数部分占 8位,尾数部分占 23位。指数部分可以表示的范围是 0~255,因为存在偏移量,因此指数的实际范围是从-126~+127,即 2⁻¹²⁶~2¹²⁷,转换成十进制,范围大约为 ±3.4x10⁻³⁸ 到 ±3.4x10³⁸之间。

双精度浮点数(double)

双精度浮点数占用 64位(8个字节),其中,符号占 1位,指数部分占 11位,尾数部分占 52位。指数部分可以表示的范围是 0~2047,因为存在偏移量,因此指数的实际范围是从-1022~+1023,即 2⁻¹⁰²²~ 2¹⁰²³,转换成十进制,范围大约为 ±1.7x10⁻³⁰⁸ 到 ±1.7x10³⁰⁸之间。

延伸单精确度

因为不常用,所以本文不进行讲解。

延伸双精确度

因为不常用,所以本文不进行讲解。

关于单精度浮点数和双精度浮点数的二进制表示如下图:

image.png

在了解完定点数和浮点数的一些基本概念之后,接下来讲解浮点数使用IEEE 754标准在内存是如何使用二进制存储的, 这里是IEEE 754标准的精华部分,也是比较难理解的部分,我会通过实际的例子结合图形进行分析。

IEEE 754转换

十进制转IEEE 754单精度浮点数二进制(没有精度丢失)

这里以十进制转单精度浮点数并且没有精度丢失为例,核心流程包含 5个步骤:

1. 确认 Sign符号位

比如,19.59375的符号为 0(正数),-1.1的符号为 1(负数),将结果值(0/1)填充到二进制的 Sign bit区域。

2. 将十进制转成纯二进制

这个步骤,只是存粹的将十进制转换成二进制。

  • 对于整数部分,通过不断地除以 2,直到商为 0;
  • 对于小数部分,采用逐步乘 2取整,直到小数部分为 0或达到尾数所需的精度;

需要注意,对于小数部分处理结束的条件有 2个:小数部分为0 或 达到尾数所需的精度。只要满足一个就OK,这里也是为什么小数会丢精度的关键所在,在下面的例子会进行讲解。

对于没有精度丢失的转换,结束的条件是:小数部分为 0。

比如,0.25转成二进制

0.25 x 2 = 0.5  0
0.5 x 2 = 1.0  1 小数部分为0,结束

3. 标准化以确定尾数和无偏移指数

根据 IEEE 754标准,需要将二进制小数点放在最左边的 1 之后,比如,100.101需要左移 2位,变成 1.00101x2²,无偏移指数是 2;0.0011需要右移 3位,变成 1.1x2⁻³,无偏移指数是 -3。

这个步骤其实就是 IEEE 754标准的一个硬性规定,解决了上面提到的浮点数漂浮不定的问题。

4. 确认偏移指数

偏移指数,即用无偏移指数(步骤3 产生的结果)加上固定的偏移值127(2⁸-1=127),再转换成二进制。

假如,无偏移指数是 4,那么,偏移指数就等于4+127=131,转换成二进制就是10000011,然后,将二进制结果值10000011填充到 Exponent指数区域。

5. 移除尾数的前导 1

在步骤3中,需要将二进制小数点放在最左边的 1 之后,因此,每个小数点前面的值都是固定的 1(也叫做前导 1),在 IEEE 754标准中,会将这个前导 1移除,从而节省了 1个bit,再将结果值填充到二进制的 Significand尾数区域,不足部分填 0。

比如,1.001110011 移除小数点前固定的 1 变成了001110011,然后将结果值001110011填充到 Significand尾数区域。

为了更好的解释上面 5个步骤,这里以十进制19.59375转换成IEEE 754二进制为例进行讲解,整个过程如下图:

image.png

十进制转IEEE 754单精度浮点数二进制(有精度丢失)

这里以十进制转单精度浮点数并且有精度丢失为例,核心流程包含 6个步骤:

1. 确认 Sign符号位

比如,19.59375的符号为 0(正数),-1.1的符号为 1(负数),将结果值(0/1)填充到二进制的 Sign bit区域。

2. 将十进制转成纯二进制

这个步骤,只是存粹的将十进制转换成二进制。

  • 对于整数部分,通过不断地除以 2,直到商为 0;
  • 对于小数部分,采用逐步乘 2取整,直到小数部分为 0或达到所需的精度;

需要注意,对于小数部分处理结束的条件有 2个:小数部分为0 或 达到尾数所需的精度。

对于有精度丢失的转换,结束的条件是:达到所需的精度。

比如,0.3转成二进制

0.3 x 2 = 0.6  0
0.6 x 2 = 1.2  1
0.2 x 2 = 0.4  0
0.4 x 2 = 0.8  0
0.8 x 2 = 1.6  1
0.6 x 2 = 1.2  1 开始进入循环,只能达到所需的精度后按需舍入结束
0.2 x 2 = 0.4  0

3. 标准化以确定尾数和无偏移指数

根据 IEEE 754标准,需要将二进制小数点放在最左边的 1 之后,比如,100.101需要左移 2位,变成 1.00101x2²,无偏移指数是 2;0.0011需要右移 3位,变成 1.1x2⁻³,无偏移指数是 -3。

这个步骤其实就是 IEEE 754标准的一个硬性规定,解决了上面提到的浮点数漂浮不定的问题。

4. 确认偏移指数

偏移指数,即用无偏移指数(步骤3 产生的结果)加上固定的偏移值127(2⁸-1=127),再转换成二进制。

假如,无偏移指数是 4,那么,偏移指数就等于4+127=131,转换成二进制就是10000011,然后,将二进制结果值10000011填充到 Exponent指数区域。

5. 移除尾数的前导 1

在步骤3中,需要将二进制小数点放在最左边的 1 之后,因此,每个小数点前面的值都是固定的 1(也叫做前导 1),在 IEEE 754标准中,会将这个前导 1移除,从而节省了 1bit,再将结果值填充到二进制的 Significand尾数区域,不足部分填0。

比如,1.001110011 移除小数点前固定的 1 变成了001110011,然后将结果值001110011填充到 Significand尾数区域。

6.按需向上或者向下舍入

在步骤2中,小数部分转换成二进制的时候不是因为乘 2使得小数部分为 0结束,而是因为产生了循环,导致达到了尾数所需的精度(单精度 23bit,双精度 52bit),对于超出的精度范围,需要如何处理?

答案:[按需向上或者向下舍入]

为了更好的解释上面 6个步骤,这里以十进制-123.3转换成 IEEE 754二进制为例进行讲解,整体流程如下图:

image.png

注意:截图中步骤6黄色字体1001,是指超出尾数 23bit范围的二进制数,需要被舍入

通过上面两个例子的分析,相信大家还是会有困惑:为什么是二进制中存储的是偏移指数而不是指针? 为什么偏移指数是通过指数加上一个固定的偏移值? 这个固定的偏移值是怎么计算的?为什么尾数需要把前导 1移除? IEEE 754 的舍入规则是什么?下面我们就一一解答。

IEEE 754单精度浮点数二进制转十进制

为了帮助大家更好地理解 IEEE 754的转换,这里还提供了 2个 IEEE 754单精度浮点数二进制转十进制的逆向例子,如下图:

image.png

偏移指数

偏移指数,也叫指数偏移值(exponent bias),即浮点数表示法中指数域的编码值,等于指数的实际值加上某个固定的偏移值,IEEE 754标准规定该固定值偏移为2 ͤ⁻¹-1,其中 e为存储指数的 bit长度,因此,单精度浮点数的固定偏移值是2⁸⁻¹-1=127,双精度浮点数的固定偏移值是2¹¹⁻¹-1=1023

为什么需要偏移指数?

从 1.00101 x 2² 和 1.1 x 2⁻³ 可以看出来,指数有正负数的区分,即有符号的区分,因此,IEEE 754标准中的偏移指数主要解决两个问题:

  1. 表示负指数: 在使用二进制表示浮点数的指数时,如果采用纯粹的二进制表示,那么需要额外的符号位来表示指数的正负。采用偏移指数的方式,可以将指数全部看作非负数,因为将偏移量添加到指数部分后,所有的指数都是正数,0则表示了最小的指数。
  2. 排序浮点数: 使用偏移指数的方式可以更容易地对浮点数进行排序。因此第 1点已经把指数全部转换成了无符号,所以,浮点数的比较直接变成了对二进制的自然排序比较,不需要单独处理符号位和指数部分的符号。

固定值偏移值如何计算?

单精度浮点数

对于单精度浮点数,它的指数域是 8个bit,表示的有符号范围是-127~128(-2⁷-1 ~ 2⁷),如何让这个范围 >=0 ?

答案:加上 127。所以,IEEE 754标准把 127设定为单精度浮点数的固定偏移值。

双精度浮点数

同理,对于双精度浮点数,它的指数域是 11个bit,表示的有符号范围是-1023~1024(-2¹⁰-1 ~ 2¹⁰),如何让这个范围 >=0 ?

答案:加上 1023。所以,IEEE 754标准把 1023设定为双精度浮点数的固定偏移值。

具体信息如下图:

image.png

为什么要移除尾数的前导 1?

在讲解浮点数构成时提过,浮点数的小数点是浮动的,因此,IEEE 754标准定义了一套固定的格式:在二进制数中,通过移位,将小数点前面的值固定为 1,IEEE754 称这种形式的浮点数为规范化浮点数。

因此,对于规范化浮点数,既然尾数的前导永远是 1,那干脆不存储,尾数其实比实际的多 1位,也就是说单精度的是 24位,双精度是 52位。

IEEE 754 的舍入规则

 

关于舍入,IEEE 754标准提供了 4种方法:

1. 舍入到最接近

这是 IEEE 754标准的默认方式,将结果舍入为最接近且可以表示的值,但是当存在两个数一样接近的时候,则取其中的偶数(在二进制中是以0结尾的)。

取偶数最关键的步骤是找到一个中间值,先确定要保留的有效数字,找到要保留的有效数字最低位的下一位。如果这位是进制的一半,而且之后的位数都为 0,则这个值就是中间值。

这里以二进制为例,有效位数保留到小数点后 2位:

  • 10.00011,中间值为 10.00100,小于中间值,向下舍入为 10.00
  • 10.00110,中间值为 10.00100,大于中间值,向上舍入为 10.01
  • 10.11100,中间值为 10.11100,等于中间值,要保留的最低有效位 1 为奇数,向上舍入为 11.00
  • 10.10100,中间值为 10.10100,等于中间值,要保留的最低有效位 0 为偶数,向下舍入为 10.10

因此,上述十进制-123.3转换成 IEEE 754二进制例子的舍入方式,采用向上舍入,即最后一位 +1。

2. 朝 +∞方向舍入

3个要点:

  • 正数多余位不全为 0,进位1
  • 正数多余位全为 0,直接截尾
  • 负数直接截尾

这里以二进制为例,有效位数保留到小数点后 3位:

  • 0.0011001,正数,从小数点后 4位起,不全为0,则向上进位(最后一位 +1),结果值为 1.010,
  • 0.0010000,正数,从小数点后 4位起,全为0,则直接截尾(从 4位起全部舍弃),结果值为 0.001
  • -0.0011010,负数直接截尾(从 4位起全部舍弃),结果值为 -0.001

3. 朝 -∞方向舍入

3个要点:

  • 正数直接截尾
  • 负数多余位全为0,直接截尾
  • 负数多余位不全为 0,进位1

这里以二进制为例,有效位数保留到小数点后 3位:

  • 0.0011001,正数,则直接截尾(从 4位起全部舍弃),结果值为 0.001
  • -0.001000,负数,从小数点后 4位起全为 0,则直接截尾(从 4位起全部舍弃),结果值 -0.001
  • -0.001101,负数,从小数点后 4位起不全为 0,则向上进位(最后一位 +1),结果值-0.010

4. 朝 0方向舍入

2个要点:

  • 正数直接截尾
  • 负数直接截尾

数学上有 4舍5入,计算机中 0舍1入,因此,朝 0方向舍入就是直接舍弃。

这里以二进制为例,有效位数保留到小数点后 3位:

  • 0.001100,正数,则直接截尾(从 4位起全部舍弃)弃,结果值 0.001
  • -0.001100,负数,则直接截尾(从 4位起全部舍弃)弃,结果值 -0.001

非规范化浮点数

上文提到了规范化浮点数,既然有规范化浮点数,是不是也存在非规范化浮点数?非规范化浮点数又是什么呢?

在 IEEE 754标准中,将“指数部分全是0,尾数部分非0”这样的浮点数称为非规范化浮点数,一般用于表示 0或者无限接近 0的很小的数字。

另外,IEEE 754标准还规定:非规范化浮点数的指数偏移值比规范化浮点数的指数偏移值小 1。

例如,最小的规范化单精度浮点数的指数部分编码值为1,指数的实际值为-126;而非规范化的单精度浮点数的指数域编码值为0,对应的指数实际值也是 -126 而不是-127。实际上非规范化浮点数仍然是有效可以使用的,只是它们的绝对值已经小于所有的规约浮点数的绝对值;即所有的非规范化浮点数比规约浮点数更接近0。规约浮点数的尾数大于等于1且小于2,而非规范化浮点数的尾数小于1且大于0。

下图展示了非规范化单精度浮点数的二进制表示:

image.png

特殊值

另外,IEEE 754中还定义4个特殊值,如下表:

image.png

  • 正负无穷大:指数全是 1,尾数全是 0,代表这个数是正负无穷大(±∞),正负取决于 S符号位。
  • NaN:指数全是 1,尾数非 0,代表这不是一个数字(NaN,Not a Number)。
  • 0:指数全是 0,尾数全是0,代表这个数是±0,正负取决于 S符号位。
  • 很小的值:指数全是 0,尾数不全是0,代表这个数是一个很小的非规范化浮点数。

浮点数的比较

有了 IEEE 754标准,浮点数的比较就很简单,基本上可以按照符号位、指数域、尾数域的顺序作字典比较。显然,所有正数大于负数;正负号相同时,指数的二进制表示法更大的其浮点数值更大。

浮点数的运算与函数

浮点数的运算和函数包含以下几种:

  • 加减乘除(Add、subtract、multiply、divide)。在加减运算中负零与零相等:−0.0=0.0
  • 平方根(Square root):
  • 浮点余数。返回值 x-(round(x/y)*y)。
  • 近似到最近的整数 𝑟 𝑜 𝑢 𝑛 𝑑(𝑥)。如果恰好在两个相邻整数之间,则近似到偶数。
  • 比较运算. -Inf <负的规范化浮点数数<负的非规范化浮点数< -0.0 = 0.0 <正的非规范化浮点数<正的规范化浮点数< Inf;
  • 特殊比较: -Inf = -Inf, Inf = Inf, NaN与任何浮点数(包括自身)的比较结果都为假,即 (NaN ≠ x) = false.

为什么 1.0 - 0.9 != 0.1?

先看 Java中的一个例子:

image.png

通过运行结果可以发现:在 Java中,不管是单精度 float,还是双精度 double,1.0 - 0.9 != 0.1,为什么?

从上面的讲解,我们已经知道浮点数 IEEE 754标准在内存中的存储方式,这里我们再简单的分析1.0 - 0.9场景:

1.0 可以精确转换成IEEE 754标准二进制为:0 01111111 00000000000000000000000

0.9 转换成IEEE 754标准二进制为:

符号位:0
偏移指数: -1 + 127 = (126)₁₀ = (0111 1110)₂
尾数:
0.9 x 2 = 1.8  1
0.8 x 2 = 1.6  1
0.6 x 2 = 1.2  1
0.2 x 2 = 0.4  0
0.4 x 2 = 0.8  0
0.8 x 2 = 1.6  1 开始进入循环,只能达到所需的精度后按需舍入结束
0.6 x 2 = 1.2  1

在0.9 转换成 IEEE 754标准的二进制时,出现了循环,这样的话,不管是单精度还是双精度,0.9转换成二进制之后都有精度损失,所以,对于 float 或者 double浮点数 0.9,转换后其真实值已经不是 0.9,因此,1.0 - 0.9 != 0.1

当然,除了这个例子,还有很多经典的例子,比如 0.1 + 0.2 为什么等于 0.30000000000000004?

那么,如何保证1.0 - 0.9 == 0.1呢?我会在另外一篇文章进行详细的讲解,敬请期待!

总结

本文讲解了定点数和浮点数,重点分析了浮点数以及IEEE 754标准下浮点数是如何存储的。

  • 浮点数一般用科学计数法表示
  • IEEE 754标准的浮点数二进制实际包含:符号位,偏移指数,尾数 3部分。
  • 十进制小数在转换为二进制时,如果可以精确转换,则不存在精度丢失。
  • 十进制小数在转换为二进制时,如果无法精确转换(存在循环或者超出尾数的范围),则存在精度丢失的问题。
  • IEEE 754舍入规则有 4种方法:舍入到最接近、朝 +∞方向舍入、朝 -∞方向舍入以及朝 0方向舍入。

参考资料

IEEE 754 维基百科

字符大全

ieee-754在线转换

进制在线转换

IEEE 754 Standard for Floating Point Binary Arithmetic

Why 0.1 + 0.2 === 0.30000000000000004: Implementing IEEE 754 in JS

How to perform round to even with floating point numbers

IEEE 754 的舍入规则