浮点数,为什么不精确?

本文不是详细教程、是我的笔记,我重新梳理了浮点数相关的知识,所以不会写的特别详细、不会详细介绍 IEEE754 浮点数标准。

本文用 B(binary) 下标表示二进制,用 D (decimal)表示 10 进制。

先说几个基础知识:

  • 二进制小数比如 0.125B * 2的1次方 = 1.25B,1.25B * 2的 -1 次方 = 0.125B
    也就是说:二进制数字,乘以 2 的 N 次方,则小数点往右边移动 N 位;乘以 2 的 -N 次方,则小数点往左边移动 N 位。
  • 十进制小数转换成的二进制小数,和浮点数如何在计算机中存储,是两个问题!!

十进制的整数,在计算机中是怎么用二进制存储的?这个程序员基本都知道,大概过程就是将十进制转换成二进制的 0 和 1 然后存储,这个很容易理解。

不好理解的是,十进制的小数,比如 0.125 以什么样的二进制形式存储在计算机中呢?

再次插入两个基本概念:定点数和浮点数。

“定点”定的是“小数点”!所以定点数也可以表示整数和小数,定点数如果要表示整数或小数,分为以下三种情况:

  • 1. 纯整数:例如整数100,小数点其实在最后一位,所以忽略不写
  • 2. 纯小数:例如:0.123,小数点固定在最高位
  • 3. 整数+小数:例如1.24、10.34,小数点在指定某个位置

对于前两种情况,纯整数和纯小数,因为小数点固定在最低位和最高位,所以它们用定点数表示时,原理是相同的,只需要把整数部分、小数部分,按照十进制转二进制的规则,分别转换即可。

而对于整数 + 小数的情况,用定点表示时,需要约定小数点的位置,才能在计算机中表示。

定点数表示整数+小数

种情况下,我们需要先约定小数点的位置。

依旧以 1 个字节(8 bit)为例,我们可以约定前 5 位表示整数部分,后 3 位表示小数部分。

对于数字 1.5 用定点数表示就是这样:

这就是用定点数表示一个小数的方式。这里再总结一下这个过程:

  • 1. 在有限的 bit 宽度下,先约定小数点的位置
  • 2. 整数部分和小数部分,分别转换为二进制表示
  • 3. 两部分二进制组合起来,即是结果

但是有没有发现一个问题,我们约定了前 5 位表示整数部分,后 3 位表示小数部分,此时这个整数部分的二进制最大值只能是 11111,即十进制的 31,小数部分的二进制最大只能表示 0.111,即十进制的 0.875。

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

  • 1.扩大 bit 的宽度:例如使用 2 个字节、4 个字节,这样整数部分和小数部分宽度增加,表示范围也就变大了。
  • 2.改变小数点的位置:小数点向后移动,整个数字范围就会扩大,但是小数部分的精度就会越来越低,没有办法表示类似 0.00001 这种高精度的值。

由此我们发现,不管如何约定小数点的位置,都会存在以下问题:

  • 数值的表示范围有限(小数点越靠左,整个数值范围越小)
  • 数值的精度范围有限(小数点越靠右,数值精度越低)

总的来说,就是用定点数表示的小数,不仅数值的范围表示有限,而且其精度也很低。要想解决这 2 个问题,所以人们就提出了使用「浮点数」的方式表示数字。

浮点数

何为“浮点”?如下图,同一个数,小数点位置不同,乘以 10 的 N 次幂的 N 值就不同了!

8.345 = 8.345 * 10^0
8.345 = 83.45 * 10^-1
8.345 = 834.5 * 10^-2

如上,float 浮点数,第一位是标志位(表示正负,0表示正,1表示负),之后8位表示“幂(exponent)”再之后23位表示“尾数(mantissa)”,这个“尾数”是不是感觉有点不好理解,你可以理解为 “83.45 * 10^-1” 中的 83.45 。

举例:将十进制数 25.125 转换为浮点数

整数部分:25(D) = 11001(B)
小数部分:0.125(D) = 0.001(B)
用二进制科学计数法表示:25.125(D) = 11001.001(B) = 1.1001001 * 2^4(B)

OK,到这里,“尾数(mantissa)”已经确定是 1.1001001 了,而“幂(exponent)”是 4 ,在二进制中“底数”默认是 2 。

这里有几个地方要注意

关于“尾数(mantissa)”注意事项

“尾数(mantissa)”按规范,小数点前面只有一位且必须是1(这也注定了“尾数(mantissa)”的值范围: 1 <= M < 2 ),所以可以省略,这一位可以省略。

关于“幂(exponent)”注意事项

上面说了,“幂(exponent)”一共8个比特位,可表示的值范围是 0-255,但是呢,“幂(exponent)”可以是负数啊,但一共8个比特位,难道还有再拿出来一位表示正负?太浪费了吧!

IEEE 规范是这样处理的:一共8个比特位的“幂(exponent)”可表示的范围是 -127 到 128,但是,“幂(exponent)”的实际值,加上一个中间数 127 后的值当作存在计算机中的“幂(exponent)”,比如 1.1001001 * 2^4(B) 中的“幂(exponent)”是 4 但是存的是时候存 127+4=131 ,即:10000011

所以 25.125(D) = 11001.001(B) = 1.1001001 * 2^4(B) 在计算机中是这样存储的:

然后我们再看看,25.125(D) 在计算机中对应的二进制是怎么存的:

浮点数为什么有精度损失?

其实浮点数为什么会有精度损失,跟上面介绍的浮点数的储存形式,没有绝对的关系。

十进制小数,无法全部都用二进制小数表示。

要理解这句话,你可以拿起纸和笔计算一下,将 0.1D 转换成二进制小数,你会发现会无限循环下去。这就说明了“十进制小数,无法全部都用二进制小数表示。”。说明了,其实跟 float、double 关系不大,不管用 float 还是 double 都注定了0.1D 无法精确转换成二进制小数。

我再举个例子,这是我自己想的,来帮助理解“十进制小数,无法全部都用二进制小数表示。”这句话,如下:

对于正整数,不管几进制,都可以完全一对一转换,因为不管几进制,增加或减少的单位都是 1 。

但是小数,是“平分”的过程,十进制小数,是以 10 为单位去平分,比如 0.1 是将1分成10份,0.2 是将1分成10份后取其中两份,0.01是将 1 分成 10*10 份。不同进制之间,平分的单位是不一样的,二进制小数,是以 2 为单位平分。这就注定了,不同进制的小数,无法一一对应。

几乎每篇介绍浮点类型 float 的时候都会说:

浮点数 float 的精度通常被认为是 6到7位有效数字

那么,浮点数 float 的精度通常被认为是6到7位有效数字,“6到7位有效数”具体是什么意思?怎么理解呢?

1. 什么是有效数字?

有效数字是指一个数字中从第一个非零数字开始,到最后一位数字为止的所有数字。例如:

  • 123.456 有 6 位有效数字。
  • 0.00123 有 3 位有效数字(前导零不算)。【看过前面对浮点数存储的表述,你应该能明白为什么前导0都不算了,因为尾数的首位默认都是1】
  • 1.2300 有 5 位有效数字(末尾的零也算)。

2. 为什么是6到7位有效数字?

  • 23 位尾数 可以表示的最大二进制精度是 2的23次方 = 8,388,608223 = 8,388,608,这大约相当于 7 位十进制数字。【结合下面例子理解,我这里简单说下,这里的“精度”指的是“尾数”,float中尾数占23个比特位,可以表示6到7位十进制数字。
  • 但由于浮点数的存储方式和舍入误差,实际精度可能会稍微降低,因此通常认为 float 的精度是 6到7位有效数字

再结合下面例子理解:

上面这个例子中,123.456 是 6 位有效数字,float 可以准确表示。

上面这个例子中,123.456789 是 9 位有效数字,float 无法准确表示,最后几位会被舍入,输出结果为 123.45679

无论是大数还是小数,float 的精度都限制在 6到7位有效数字。

OK,本文结束,重点是要理解,float 的精度/有效尾数是 6到7 位,指的是“尾数”的精确度!!!

码先生
Author: 码先生

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注