golang 浮点数精度丢失问题详解

本贴最后更新于 937 天前,其中的信息可能已经时移世易

请看以下 Go 代码,会返回 0.7 吗?

var num float32
for i := 0; i < 7; i++{
    num = num + 0.1
}

fmt.Println(num)

答案可能出人意料,是:0.70000005

也许有人会问,是不是 Go 语言的问题?换其他语言试试?
OK,我们换 JS 试试。

浮点数.jpeg

答案依然令人意外。
除此之外,你还可以试试 C、C++、Java、PHP 等其他语言的 float 类型相加,看得到的数据是否精确;

还有,除了语言之外,你还可以在 MySQL 等数据库中试试 float 类型数据的字段叠加,得到的数据是否精确。

我可以先告诉你答案:只要是float类型的数据相加,无论在任何语言、任何数据库、任何中间件中进行加法(减法乘除法)运算,得到的数据,都不会精确。

这是浮点类型的精度丢失现象。(Loss of significance)

要了解产生这个现象的原因,就要先了解计算机是如何定义和表示 float 类型的。
不同于正整数类型的表示方法,float 类型在计算机中的表示略显复杂,遵循的是 IEEE 754标准

下面,我们就讲一下 IEEE 754标准

我们首先回顾一下整数类型在计算机中的表示。
我们知道:计算机只认识 0 和 1;那么,对于像 6 一样的这种正整数,我们要做十进制到二进制的转换。

精度.png

所以,十进制 6 最终转化为二进制为 110

这很好理解,但是,如何表示 6.1 等这类小数呢?
有人说了,可以找个特殊的符号,用来表示小数点 .,把 6.161 隔开;听起来是个不错的办法。其实 IEEE 754 还真就是这么做的,只不过思路略有些复杂,总体思路就是:仿照用”科学计数法”!

我们再回顾一下什么是 科学计数法
把一个数表示成a与10的n次幂相乘的形式(1≤|a|<10,a不为分数形式,n为整数),这种记数法叫做科学记数法。
也就是:1.360X10^4 这种计数方式。

我们可以仿照科学计数法,来表示浮点数,把二进制数统一表示成 1.0110101 X 2^n 这种形式。
数据层面怎么表示出这种形式呢?根据 IEEE 754 的标准,将数据分为三部分:

total.png

从左到右分别表示:符号位(正负数)、指数位和小数位

以单精度浮点数为例,单精度浮点数一共 32 位(双精度 64 位,即平时所说的 double 类型),具体内部表示为:total01.png

这里有个地方要特别注意:因为数据最终要表示成 1.0110101 X 2^n 这种形式,整数位在二进制下,永远都是 1,所以在表示 float 类型的时候,直接把 1 给去掉了,假如有就占据一个 bit 的空间,既然那个 bit 位上永远都是 1,所以干脆去掉了。

那么,具体该如何展示呢?例如小数点后的数字怎么表示?6.1 能否写成 110.1 呢?如果能的话小数点后这个 1 代表什么呢?个数一?那添加几个零的话,能否认为是十、一百、一千?似乎是不可以,因为这样只能满足”视觉效果”,逻辑层面直接说不通。

要明白在小数点后的数字代表除以 2 后的数字,例如二进制下小数点后的第一位 1 代表 1 / 2 等于 0.5,第二位 1 代表 1/2/2 等于 0.25,依次类推第三位 1 则代表 0.125…具体请看下图:

binarytable.png

所以,给定一个小数,譬如 0.1,要想得到对应的二进制数,应该是和小数点左边的计算方式相反:乘以2,记录整数位

0.1 X 2 = 0.2  0
0.2 X 2 = 0.4  0
0.4 X 2 = 0.8  0
0.8 X 2 = 1.6  1
(1.6 - 1 = 0.6)
0.6 X 2 = 1.2  1
(1.2 - 1 = 0.2) 
0.2 X 2 = 0.4  0
0.4 X 2 = 0.8  0
0.8 X 2 = 1.6  1
(1.6 - 1 = 0.6)
0.6 X 2 = 1.2  1
(1.2 - 1 = 0.2) 
0.2 X 2 = 0.4  0
0.4 X 2 = 0.8  0
0.8 X 2 = 1.6  1

... 
// 无限循环下去

所以,0.1 用二进制表示为:0.000110011001100110011...
因此 6.1 用二进制应该表示为:110.000110011001100110011...
用”科学计数法“表示为:1.10000110011001100110011... X 2^2
OK,看来小数位的数可以确定了是 10000110011001100110011,即去掉整数位 1 后,向后截取的 23 位数(浮点数不精确的本质原因)。

符号位 0 表示正数,1 表示负数,所以可以确定是 6.1 的符号位是 0;现在符号位有了,小数位有了,只剩下指数 2 如的表示了,该如何表示呢?直接在 8 位的空间内转化为 000000010

显然不可以,首先,如果指数位用 原码 表示,那么,针对指数位为负的情况,就得加一个符号位去表示,而且还会出现两个零的情况:000000001000000,操作起来过程复杂~

有人要问那如果使用补码呢?
如果使用补码,会出现以下情况,请看例子:

例如:1.01 X 2^-1 和 1.11 X 2^3比较大小?

     首先对比指数位, -1 和 3,分别转化为二进制数 ``111``和``011``;

     如果没有其他逻辑处理,``111``是"7",``011``是"3", 7会小于3吗?

可见使用补码,也不是很方便,于是,引用了另外一种编码方式——-移码。
先说说移码的定义:将每一个数值加上一个偏置常数(Excess / bias),通常,当编码位数为n的时候,bias取 "2^n-1" 或者 "2^n-1 - 1"

承接以上 1.01 X 2^-1 和 1.11 X 2^3 比较大小的例子:

例如:1.01 X 2^-1 和 1.11 X 2^3比较大小?
  
    指数为-1的则表示为 -1 + 4 = 3,二进制表示为:011

    指数为3的则表示为 3 + 4 = 7 二进制表示为:111

    7 > 3,即 111 > 011 比较完毕

就这样,浮点数”科学计数法“的指数位比较变得简单了,而且,消除了”正零“ 和 ”负零“ 不相同的问题。

因为 :

假设偏移量是:4

则移码表示的0只有:0 + 4 = 4,即“100”

IEEE 754 中,指数位移码的偏移量为指数位数的 2^n-1 - 1,为 127。

所以,回到 6.1 表示的问题上,指数位为:2 + 127 = 129,二进制表示为:10000001

因此,6.1IEEE 754 单精度浮点数标准的下,表示为:

ieee754.png

好了,现在了解了浮点数 IEEE 754 标准的表示方法,知道为何浮点数相加总是不精确了吧?

因为浮点数很多小数在二进制环境下很多都无法完整的表示,只能截取部分数据来近似的表示,两个数相加的话,就是两个近似的数相加的和,如果相加次数足够多,精确度自然也就会越来越低

转载

  • golang

    Go 语言是 Google 推出的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。谷歌首席软件工程师罗布派克(Rob Pike)说:我们之所以开发 Go,是因为过去 10 多年间软件开发的难度令人沮丧。Go 是谷歌 2009 发布的第二款编程语言。

    492 引用 • 1383 回帖 • 373 关注
  • 转载

    转载的帖子不会出现在社区全局列表上。

    10 引用 • 62 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
xhaoxiong
站在巨人的肩膀上学习与创新

推荐标签 标签

  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1083 引用 • 3461 回帖 • 286 关注
  • Vim

    Vim 是类 UNIX 系统文本编辑器 Vi 的加强版本,加入了更多特性来帮助编辑源代码。Vim 的部分增强功能包括文件比较(vimdiff)、语法高亮、全面的帮助系统、本地脚本(Vimscript)和便于选择的可视化模式。

    27 引用 • 66 回帖
  • 996
    13 引用 • 200 回帖
  • IDEA

    IDEA 全称 IntelliJ IDEA,是一款 Java 语言开发的集成环境,在业界被公认为最好的 Java 开发工具之一。IDEA 是 JetBrains 公司的产品,这家公司总部位于捷克共和国的首都布拉格,开发人员以严谨著称的东欧程序员为主。

    180 引用 • 400 回帖
  • Solidity

    Solidity 是一种智能合约高级语言,运行在 [以太坊] 虚拟机(EVM)之上。它的语法接近于 JavaScript,是一种面向对象的语言。

    3 引用 • 18 回帖 • 349 关注
  • BND

    BND(Baidu Netdisk Downloader)是一款图形界面的百度网盘不限速下载器,支持 Windows、Linux 和 Mac,详细介绍请看这里

    107 引用 • 1281 回帖 • 20 关注
  • 反馈

    Communication channel for makers and users.

    123 引用 • 906 回帖 • 192 关注
  • DNSPod

    DNSPod 建立于 2006 年 3 月份,是一款免费智能 DNS 产品。 DNSPod 可以为同时有电信、网通、教育网服务器的网站提供智能的解析,让电信用户访问电信的服务器,网通的用户访问网通的服务器,教育网的用户访问教育网的服务器,达到互联互通的效果。

    6 引用 • 26 回帖 • 522 关注
  • API

    应用程序编程接口(Application Programming Interface)是一些预先定义的函数,目的是提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力,而又无需访问源码,或理解内部工作机制的细节。

    76 引用 • 421 回帖
  • 服务器

    服务器,也称伺服器,是提供计算服务的设备。由于服务器需要响应服务请求,并进行处理,因此一般来说服务器应具备承担服务并且保障服务的能力。

    124 引用 • 580 回帖
  • Jenkins

    Jenkins 是一套开源的持续集成工具。它提供了非常丰富的插件,让构建、部署、自动化集成项目变得简单易用。

    51 引用 • 37 回帖 • 1 关注
  • Linux

    Linux 是一套免费使用和自由传播的类 Unix 操作系统,是一个基于 POSIX 和 Unix 的多用户、多任务、支持多线程和多 CPU 的操作系统。它能运行主要的 Unix 工具软件、应用程序和网络协议,并支持 32 位和 64 位硬件。Linux 继承了 Unix 以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统。

    915 引用 • 931 回帖
  • Sphinx

    Sphinx 是一个基于 SQL 的全文检索引擎,可以结合 MySQL、PostgreSQL 做全文搜索,它可以提供比数据库本身更专业的搜索功能,使得应用程序更容易实现专业化的全文检索。

    1 引用 • 178 关注
  • CSDN

    CSDN (Chinese Software Developer Network) 创立于 1999 年,是中国的 IT 社区和服务平台,为中国的软件开发者和 IT 从业者提供知识传播、职业发展、软件开发等全生命周期服务,满足他们在职业发展中学习及共享知识和信息、建立职业发展社交圈、通过软件开发实现技术商业化等刚性需求。

    14 引用 • 155 回帖 • 3 关注
  • Vditor

    Vditor 是一款浏览器端的 Markdown 编辑器,支持所见即所得、即时渲染(类似 Typora)和分屏预览模式。它使用 TypeScript 实现,支持原生 JavaScript、Vue、React 和 Angular。

    312 引用 • 1666 回帖 • 1 关注
  • Ant-Design

    Ant Design 是服务于企业级产品的设计体系,基于确定和自然的设计价值观上的模块化解决方案,让设计者和开发者专注于更好的用户体验。

    17 引用 • 23 回帖 • 2 关注
  • 持续集成

    持续集成(Continuous Integration)是一种软件开发实践,即团队开发成员经常集成他们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。

    14 引用 • 7 回帖 • 2 关注
  • Ruby

    Ruby 是一种开源的面向对象程序设计的服务器端脚本语言,在 20 世纪 90 年代中期由日本的松本行弘(まつもとゆきひろ/Yukihiro Matsumoto)设计并开发。在 Ruby 社区,松本也被称为马茨(Matz)。

    7 引用 • 31 回帖 • 177 关注
  • 京东

    京东是中国最大的自营式电商企业,2015 年第一季度在中国自营式 B2C 电商市场的占有率为 56.3%。2014 年 5 月,京东在美国纳斯达克证券交易所正式挂牌上市(股票代码:JD),是中国第一个成功赴美上市的大型综合型电商平台,与腾讯、百度等中国互联网巨头共同跻身全球前十大互联网公司排行榜。

    14 引用 • 102 回帖 • 402 关注
  • Kafka

    Kafka 是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者规模的网站中的所有动作流数据。 这种动作(网页浏览,搜索和其他用户的行动)是现代系统中许多功能的基础。 这些数据通常是由于吞吐量的要求而通过处理日志和日志聚合来解决。

    35 引用 • 35 回帖
  • 生活

    生活是指人类生存过程中的各项活动的总和,范畴较广,一般指为幸福的意义而存在。生活实际上是对人生的一种诠释。生活包括人类在社会中与自己息息相关的日常活动和心理影射。

    228 引用 • 1450 回帖
  • 人工智能

    人工智能(Artificial Intelligence)是研究、开发用于模拟、延伸和扩展人的智能的理论、方法、技术及应用系统的一门技术科学。

    75 引用 • 145 回帖
  • React

    React 是 Facebook 开源的一个用于构建 UI 的 JavaScript 库。

    192 引用 • 291 回帖 • 440 关注
  • jsoup

    jsoup 是一款 Java 的 HTML 解析器,可直接解析某个 URL 地址、HTML 文本内容。它提供了一套非常省力的 API,可通过 DOM,CSS 以及类似于 jQuery 的操作方法来取出和操作数据。

    6 引用 • 1 回帖 • 458 关注
  • HTML

    HTML5 是 HTML 下一个的主要修订版本,现在仍处于发展阶段。广义论及 HTML5 时,实际指的是包括 HTML、CSS 和 JavaScript 在内的一套技术组合。

    103 引用 • 294 回帖
  • Windows

    Microsoft Windows 是美国微软公司研发的一套操作系统,它问世于 1985 年,起初仅仅是 Microsoft-DOS 模拟环境,后续的系统版本由于微软不断的更新升级,不但易用,也慢慢的成为家家户户人们最喜爱的操作系统。

    215 引用 • 462 回帖 • 1 关注
  • WiFiDog

    WiFiDog 是一套开源的无线热点认证管理工具,主要功能包括:位置相关的内容递送;用户认证和授权;集中式网络监控。

    1 引用 • 7 回帖 • 545 关注