Why系列:0.1 + 0.2 != 0.3

简介: 来解决 0.1 + 0.2 这个小学生都会的题目,大致分四个步骤1.进制转换2.IEEE 754浮点数标准3.浮点数计算4.0.1 + 0.2

1.JPG



前言


为了知道更多一点,打算自己来一个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


2.JPG


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


结果为: 011


3.JPG


结合起来 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浮点数标准


4.JPG


以双精度浮点格式为例,如上图,三个参数 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位转换工具

相关文章
|
编解码 Windows
UE-windows包蓝图分辨率设置
windows包蓝图分辨率设置
|
存储 JavaScript 前端开发
vue3 专用 indexedDB 封装库,基于Promise告别回调地狱(二)
https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API 这个大概是官网吧,原始是英文的,现在陆续是出中文版。有空的话还是多看看官网。
|
6月前
|
人工智能 搜索推荐 知识图谱
深度实践:Geo优化中,如何基于“两大核心+四轮驱动”设计高信任度JSON-LD
在AI搜索时代,GEO正取代传统SEO。本文详解于磊老师首创的“两大核心+四轮驱动”GEO优化体系,揭示如何通过人性化设计与内容交叉验证,结合E-E-A-T、结构化数据等,打造高信任度JSON-LD,提升AI采纳率与获客效率。
412 2
|
5月前
|
人工智能 自然语言处理 监控
AI Ping: 一站式大模型服务评测与API调用平台技术解析
在当前大模型应用爆发式增长的背景下,开发者面临着一个共同的痛点:如何高效、低成本地调用大模型服务? 本文将深入解析AI Ping如何通过其vibe coding工具链实现"零成本"接入三大主流免费模型,帮助开发者在日常开发中显著降低AI使用成本。
AI Ping: 一站式大模型服务评测与API调用平台技术解析
|
10月前
|
人工智能 前端开发 机器人
10+热门 AI Agent 框架深度解析:谁更适合你的项目?
一个合适的 Agent 框架,决定了你AI应用落地的速度与质量。选框架 ≠ 选最火! 真正能跑起来、跑得稳、跑得远的 Agent 框架,才是你的最优解。
|
机器学习/深度学习 编解码 算法
【YOLOv8改进 - 特征融合NECK】SDI:多层次特征融合模块,替换contact操作
YOLOv8专栏探讨了该目标检测算法的创新改进,包括新机制和实战案例。文章介绍了U-Net v2,一种用于医学图像分割的高效U-Net变体,它通过SDI模块融合语义和细节信息,提升分割准确性。SDI模块结合空间和通道注意力,经通道减缩、尺寸调整和平滑后,用哈达玛积融合特征。提供的核心代码展示了SDI模块的实现。更多详情和论文、代码链接见原文。
|
Python
Python中使用默认参数(Default Arguments)
【7月更文挑战第24天】
659 1
|
Ubuntu 网络安全 数据安全/隐私保护
ubuntu server连接wifi教程
本文提供了一个简化Ubuntu Server在Raspberry Pi系统上配置过程的脚本"config_ubuntu_server",包括自动和手动两种方法来设置root权限、SSH配置,并连接WiFi,同时支持无密码SSH访问,适合初学者和高级用户。
1098 3
|
运维 数据安全/隐私保护 网络协议
【网络建设与运维】2024年河北省职业院校技能大赛中职组“网络建设与运维”赛项例题(八)
【网络建设与运维】2024年河北省职业院校技能大赛中职组“网络建设与运维”赛项例题(八)
【网络建设与运维】2024年河北省职业院校技能大赛中职组“网络建设与运维”赛项例题(八)
|
存储 Linux Windows
GPT与MBR:硬盘分区表格式的革新与区别
GPT与MBR:硬盘分区表格式的革新与区别
1769 0