为了知道更多一点,打算自己来一个why系列。

  • 面试官:同学, 请问 0.1 + 0.2 等于多少
  • 同学:不等于0.3, 因为精度问题
  • 面试官:能更深入的说一下嘛
  • 同学:......

上面的同学,就是曾今的我!

所以,干!

来解决 0.1 + 0.2 这个小学生都会的题目,大致分三个步骤

  1. 进制转换
  • 十进制转二进制
  • 二进制转十进制
  1. IEEE 754浮点数标准
  2. 浮点数计算
  3. 0.1 + 0.2

进制转换

十进制转二进制

  • 整数: 采用 除2取余,逆序排列法。具体做法是:用2整除十进制整数,可以得到一个商和余数;再用2去除商,又会得到一个商和余数,如此进行,直到商为小于1时为止,然后把先得到的余数作为二进制数的低位有效位,后得到的余数作为二进制数的高位有效位,依次排列起来。
  • 小数: 采用乘2取整,顺序排列法。具体做法是:用2乘十进制小数,可以得到积,将积的整数部分取出,再用2乘余下的小数部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,此时0或1为二进制的最后一位。或者达到所要求的精度为止

我们依旧使用 9.375 来分析,

先看整数部分:9, 按照规则,除2取余,逆序排列

结果为: 1001

再看小数部分: 0.375 ,按照规则 采用乘2取整,顺序排列

结果为: 011

结合起来 9.375 = 整数2 + 小数2 = 1001 + .011 = 1001.011

验证:

(9.375).toString(2)  // 1001.011
Number.prototype.toString.call(9.375, 2) // 1001.011
Number.prototype.toString.call( Number(9.375), 2) // 1001.011

二进制转十进制

  • 小数点前或者整数要从右到左用二进制的每个数去乘以2的相应次方并递增
  • 小数点后则是从左往右乘以二的相应负次方并递减。

例如:二进制数1001.011转化成十进制

整数部分:1001 = 1 * 20 + 0 * 21 + 0 * 22 + 1 * 23 = 1 + 0 + 0 + 8 = 9

小数部分:011 = 0 * 2-1 + 1 * 2-2 + + 1 * 2-3 = 0 + 0.25 + 0.125 = 0.375

1001.0112 = 整数部分 + 小数部分 = 910 + 0.375 10 = 9.375

当然我们怎么验证结果了,调用Number.prototype.toString

(9.375).toString(2)  // 1001.011
Number.prototype.toString.call(9.375, 2) // 1001.011
Number.prototype.toString.call( Number(9.375), 2) // 1001.011

IEEE 754浮点数标准



以双精度浮点格式为例,如上图,三个参数 S E M:

名称                        长度        比特位置

符号位    Sign  (S)      : 1bit        (b63)
指数部分Exponent (E) : 11bit (b62-b52)
尾数部分Mantissa (M) : 52bit (b51-b0) 双精度的指数部分(E)采用的偏置码为1023 S=1表示负数 S=0表示正数

求值公式 (-1)^S*(1.M)*2^(E-1023)

这里有一个额外的参数,偏移码 1023, 更多可以参考IEEE 754浮点数标准中64位浮点数为什么指数偏移量是1023

结合公司来看, 各个参数是怎么工作的:

二进制可能不好理解,我们先看一个10进制的数, 比如 1001.125, 我们可以写成

  • 1001.125
  • 100.1125*101
  • 10.01125*102
  • 1.001125*103

上面的(-1)^S*(1.M)*2^(E-1023) 同 1.001125*103 只不过使用的是二进制而已。

如果是小数了,同理 0.0000125 就是 1.25 * 10-5

我们来看看一个二进制的例子, 比如 103.0625

  • S 符号位

    因为正数,所以 S为0
  • E 指数位
    1. 转为二进制为 1100111.0001
    2. 规范化 1.1001110001 * 26, 6 = E - 1023 , E = 1029
    3. E = 1029 , 转为二进制 10000000101
  • M 尾数位

    对于 1.1001110001 , M的值为 1001110001, 因为长度有52位,后面补充0就行, 结果为

    1001110001000000000000000000000000000000000000000000

拼接来 0-10000000101-1001110001000000000000000000000000000000000000000000

至于如何验证对不对,可以去IEEE 754 64位转换工具 验证一下。

大家知道,十进制有无限循坏小数,二进制也是存在的。遇到这种情况,10进制是四舍五入,那二进制呢。 只有0和1,那么是1就入吧。

当然 IEEE 754 是有好几种舍入误差的模式的,更多细节可以阅读 IEEE 754浮点数标准详解

我们看看 1.1, 最后尾数部分 000110011001100110011001100110011001100110011001100152 (11001)循环 ,53位是1,那就进位吧,

结果为 0001100110011001100110011001100110011001100110011010

这里知道十进制,怎么转换为二进制的浮点数存储了,下面就可以进行运算。

浮点数计算

浮点数的加减运算一般由以下五个步骤完成:对阶、尾数运算、规格化、舍入处理、溢出判断。 更多细节,可以阅读浮点数的运算步骤

1.对阶

这里的阶,就是指数位数,简单说,就是指数位保持一致。 即⊿E=E x-E y,将小阶码加上⊿E,使之与大阶码相等。

拿是十进制举例, 123.5 + 1426.00456

  • 等价于 1.235*102 + 1.42600456 * 103
  • 和指数高的对齐,高的为3 ,变成 0.1235*103 + 1.42600456 * 103

123.5 + 1426.00456 = 0.1235*103 + 1.42600456 * 103 = (0.125 + 1.42600456) * 103 = 1.54950456 * 103 = 1549.50456

举例是十进制, 计算机执行的是二进制而已。 这个过程可能会有几个问题。

  • 小阶对大阶的时候,会右移动, 因为指数部分,最多保留52位,就可能丢。
  • 相加或者相减,值可能溢出,就有了后面的溢出判断。

2.尾数运算

尾数运算就是进行完成对阶后的尾数相加减。

3.结果规格化

在机器中,为保证浮点数表示的唯一性,浮点数在机器中都是以规格化形式存储的。对于IEEE754标准的浮点数来说,就是尾数必须是1.M的形式。

再拿十进制举例。

80.5 + 90 = 8.05*101 + 9.0*101 = 17.051

尾数必须是1.M的形式 ,规格化 => 1.705 2

4.舍入处理

浮点运算在对阶或右规时,尾数需要右移,被右移出去的位会被丢掉,从而造成运算结果精度的损失。为了减少这种精度损失,可以将一定位数的移出位先保留起来,称为保护位,在规格化后用于舍入处理。

IEEE754标准列出了四种可选的舍入处理方法,默认使用四舍五入。

5.溢出判断

与定点数运算不同的是,浮点数的溢出是以其运算结果的阶码的值是否产生溢出来判断的。

若阶码的值超过了阶码所能表示的最大正数,则为上溢,进一步,若此时浮点数为正数,则为正上溢,记为+∞,若浮点数为负数,则为负上溢,记为-∞;若阶码的值超过了阶码所能表示的最小负数,则为下溢,进一步,若此时浮点数为正数,则为正下溢,若浮点数为负数,则为负下溢。正下溢和负下溢都作为0处理

0.1 + 0.2 != 0.3

换算成 IEEE 754 标准的二进制数据结构

在如上基础之类,我们再开始我们的议题0.1 + 0.2 != 0.3, 计算机首先会把十进制转为二进制,然后进行加法。

0.1 转 二进制为, 终止条件是 直到积中的小数部分为零, 但是从下面的结果来看, 所以简单表示为

0.0 0011 0011 (0011)无限循环,无限循环,这可不行,这次轮到 IEEE 754标准 出场了,IEEE 754标准定义了单精度和双精度浮点格式

2 * 0.1
2 * 0.2 0 2 * 0.4 0
2 * 0.8 0
2 * 1.6 1
2 * 1.2 1 2 * 0.4 0
2 * 0.8 0
2 * 1.6 1
2 * 1.2 1 2 * 0.4 0
2 * 0.8 0
2 * 1.6 1
2 * 1.2 1
.....无限循环0011....

我们以0.1为例,开始计算

  • 符号位S

    因为是正数,那么符号位为 0 。
  • 指数部分E

    首先我们将0.1转为2进制数0.0 0011 0011 (0011)无限循环,因为是正数,那么符号位为 0。 然后我们根据正规化的二进制浮点数表达,那么它以1.bbbbb...这种形式表示, 为:1.1001 1001 (1001)无限循环 x 2-4

    E-1023 = -4 那么 E = 1019, 1019的二进制表示为 1111111011, 因为有11位,前面加0, 为01111111011
  • 尾数部分M, 无限循环

    1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001无限循环

    当64bit的存储空间无法存储完整的无限循环小数,而IEEE 754 Floating-point采用round to nearest, tie to even的舍入模式。

    1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 就进位为

    1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010

最终

0-01111111011-1001100110011001100110011001100110011001100110011010

你也可使用 IEEE 754 64位转换工具来验证自己的结果是否正确。

同理0.2的最终结果为

0-01111111100-1001100110011001100110011001100110011001100110011010

接下来,进行标准的浮点数运算

对阶

小阶对大阶。

0.1 的指数 01111111011 = 1019

0.2 的指数 01111111100 = 1020

0.2 的指数大,0.1的调整指数位为 01111111100, 同时位数部分右移一位,如下:

0.1    0-01111111100-11001100110011001100110011001100110011001100110011010
0.2 0-01111111100-1001100110011001100110011001100110011001100110011010

尾数运算

可以看到有进位

    0.11001100110011001100110011001100110011001100110011010    ---0.1尾数
1.1001100110011001100110011001100110011001100110011010 ---0.2尾数
-----------------------------------------------------------------------------
10.01100110011001100110011001100110011001100110011001110 结果

结果规格化

需要右移一位, E+1 = 1020 + 1 = 1021 = 1111111101

1.M = 1.001100110011001100110011001100110011001100110011001110

舍入处理

尾数小数部分 0011001100110011001100110011001100110011001100110011 10 长度为54,

四舍五入 0011001100110011001100110011001100110011001100110100

溢出检查

指数没有溢出

结果计算::

E 为 1021, S为0

计算值: (-1)^S*(1.M)*2^(E-1023) => (1.0011001100110011001100110011001100110011001100110100)*2^(-2)

=> 0.010011001100110011001100110011001100110011001100110100

通过 在线进制转换, 结果为 0.30000000000000004

话外

既然有这个问题,那么我们怎么老保证结果的正确性呢。

  • 比如钱,明确的知道最多两位小数, 那么不妨 先 *100
  • 与一个非常小的值对比。比如 Number.EPSILON

浮点数的运算步骤

IEEE 754-维基百科

IEEE 754浮点数标准详解

JS魔法堂:彻底理解0.1 + 0.2 === 0.30000000000000004的背后

IEEE 754 64位转换工具

Why系统:0.1 + 0.2 != 0.3的更多相关文章

  1. IIS6.0添加上.net4.0后,以前的.net系统出现“服务器应用程序不可用”的错误提示解决办法

    把VS2010开发的网站.net4.0部署到Windows Server 2003的服务器上去, Windows Server 2003操作系统自带的为IIS 6.0,IIS 6.0一般只支持.NET ...

  2. 【Android 系统开发】CyanogenMod 13.0 源码下载 编译 ROM 制作 ( 手机平台 : 小米4 | 编译平台 : Ubuntu 14.04 LTS 虚拟机)

                 分类: Android 系统开发(5)                                              作者同类文章X 版权声明:本文为博主原创文章 ...

  3. windows 系统如何安装 mysql 8.0.15 数据库?

    windows 系统如何安装 mysql 8.0.15 数据库? 1. 下载安装包 下载地址:https://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0. ...

  4. Windows系统 安装 Qt 5.7.0

    Windows系统 安装 Qt 5.7.0 我们的电脑系统:Windows 10 64位 Qt5 软件:Qt 5. 7. 0 下载 Qt 5.7.0 软件 在这个网站里面,下载:Qt 5.7.0 fo ...

  5. CentOS-6.4-DVD系统中安装Oracle-11.2.0.4

    完整版见https://jadyer.github.io/2014/05/18/centos-install-oracle/ /** * CentOS-6.4-DVD系统中安装Oracle-11.2. ...

  6. 从OLLVM4.0.0升级到LLVM8.0.1,并且给LLVM增加Pass 插件系统

    版本太低了,用得我这个揪心. 上周日决定把手头的ollvm从4.0.0升级到LLVM8.0.1. 里面的Pass的话,决定移植到8.0.1里面. 我习惯从代码上来动手 1:下载LLVM  https: ...

  7. VMware15.5.0安装MacOS10.15.0系统 安装步骤(上)

    VMware15.5.0安装MacOS10.15.0系统安装步骤(上)超详细! 说明: 本文是目前最新的安装和调配教程且MacOS10.15和10.16版本搭建方法相同,我也会在一些细节地方加上小技巧 ...

  8. 基于golang分布式爬虫系统的架构体系v1.0

    基于golang分布式爬虫系统的架构体系v1.0 一.什么是分布式系统 分布式系统是一个硬件或软件组件分布在不同的网络计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统.简单来说就是一群独立计算机 ...

  9. 第三方系统平台如何对接gooflow2.0

    第一步,参与者数据源配置 目前提供3种参与者数据源(员工,角色,部门),还有一种sql语句 XML配置如下 <?xml version="1.0" encoding=&quo ...

  10. VMware15.5.0安装MacOS10.15.0系统 安装步骤(下)

    VMware15.5.0安装MacOS10.15.0系统安装步骤(下)超详细! 接上文第5条如果没看过上篇的话传送门:https://www.cnblogs.com/Top-chen/p/128024 ...

随机推荐

  1. 【Kata Daily 190908】How Much?

    原题: I always thought that my old friend John was rather richer than he looked, but I never knew exac ...

  2. 18、Celery

    Celery 1.什么是Clelery Celery是一个简单.灵活且可靠的,处理大量消息的分布式系统 专注于实时处理的异步任务队列 同时也支持任务调度 Celery架构 Celery的架构由三部分组 ...

  3. 【线上问题排查技巧】动态修改LOGGER日志级别

    前言 大多数情况下,我们会在打印日志时定义日志的LOGGER级别,用来控制输出的信息范围. 一方面,过多的输出会影响查看日志的效率,另一方面,过少的日志让问题定位变得困难. 但当线上出现问题时,线上容 ...

  4. linux基本操作之linux登陆

    一 文本登陆方式: 输入用户名与密码:/etc/passwd文件对照(口令文件,保存基本的用户信息):/etc/shadow文件对照(影子文件,保存密码信息):启动相应的shell程序:用户得到 一个 ...

  5. VS2017新建MVC+ORM中的LinqDb访问数据库项目

    1.前提概述 ORM对象关系映射(Object-Relational Mapping)是一种程序技术,用于实现面向对象编程语言里不同类型系统的数据之间的转换.从效果上说,它其实是创建了一个可在编程语言 ...

  6. 从头学起Verilog(三):Verilog逻辑设计

    引言 经过了组合逻辑和时序逻辑的复习,终于到了Verilog部分.这里主要介绍Verilog一些基础内容,包括结构化模型.TestBench编写和仿真.真值表模型. 这部分内容不多,也都十分基础,大家 ...

  7. mysql中delete from t1 where id = 10加锁状况叙述

    在Next_Key Lock算法中,不仅仅锁定住所找到的索引,而且还锁定住这些索引覆盖的范围.因此在这个范围内的插入都是不允许的.这样就避免了在这个范围内插入数据导致的幻读问题. delete fro ...

  8. Python_算法汇总

    1. 约瑟夫环: # 约瑟夫环:共31个数,每隔9个删除一个,要求输出前15个号码 a=[x for x in range(1,31)] #生成编号 del_number = 8 #该删除的编号 fo ...

  9. hadoop启动脚本

    记录一下一个简单的hadoop启动脚本 就是启动zookeeper集群,hadoop的HDFS和YRAN的脚本 start-cluster.sh 关于关闭的脚本,只需要顺序换一下,然后将start改为 ...

  10. linux 身份鉴别口令复杂度整改

    口令复杂度: 1.首先安装apt install libpam-cracklib -y2.vim /etc/pam.d/common-password3.在第2步末尾添加password requis ...