浅析Golang中float64的精度问题

 更新时间:2023年08月08日 11:47:40   作者:X_PENG  
这篇文章主要来和大家一起探讨一下Golang中关于float64的精度问题,文中的示例代码讲解详细,具有一定的学习价值,感兴趣的小伙伴可以了解下

现象

func main() {
   x := uint64(1)
   for i := 0; i < 53; i++ {
      x = x * 2
   }
   fmt.Println("2^53 =", x)
   xStr := strconv.FormatUint(x, 10)
   fmt.Println("len(2^53) =", len(xStr))
   xAdded1 := x + 1
   fmt.Println("2^53 + 1 =", xAdded1)                   // 9007199254740993
   fmt.Println("float64(2^53 + 1) =", float64(xAdded1)) // 9.007199254740992e+15,出现了精度问题
   xAdded2 := x + 2
   fmt.Println("2^53 + 2 =", xAdded2)                   // 9007199254740994
   fmt.Println("float64(2^53 + 2) =", float64(xAdded2)) // 9.007199254740994e+15,没有出现精度问题
   fmt.Println("math.MaxInt64 =", int64(math.MaxInt64))
   fmt.Println("float64(math.MaxInt64) =", float64(math.MaxInt64)) // 精度问题
   fmt.Println("math.MaxUint64 =", uint64(math.MaxUint64))
}

运行结果:

2^53 = 9007199254740992
len(2^53) = 16
2^53 + 1 = 9007199254740993
float64(2^53 + 1) = 9.007199254740992e+15
2^53 + 2 = 9007199254740994
float64(2^53 + 2) = 9.007199254740994e+15
math.MaxInt64 = 9223372036854775807
float64(math.MaxInt64) = 9.223372036854776e+18
math.MaxUint64 = 18446744073709551615

分析

可以看到float64无法精确存储2^53 + 1,但能精确存储2^53 + 2,为什么?

首先,float64的尾数位有52位,尾数的最大长度只能是52+1=53位,尾数长度超过53就无法精确存储,会存在精度问题。

无法精确存储2^53+1

2^53+1的二进制是10000000....0001(中间有52个0),根据IEEE标准则是(-1)^0 * 1.000000000...01(中间有52个0) * 2^53,尾数长度是54,超过了53,因此float64无法存储第54位,只能舍去最后的1,所以存在精度问题。

能精确存储2^53+2

2^53+2的二进制是1000000...010(中间51个0),根据IEEE标准则是(-1)^0 * 1.00000000...01(中间是51个0)* 2^53,尾数长度是53,float64的尾数位可以精确存储,因此没有精度问题。

知识补充

计算机的浮点数表示

科学计数法和IEEE标准

在计算机中,是用二进制的科学计数法来表示和存储浮点数的。因为科学计数法可以唯一地表示任何一个数,且所占用的存储空间会更少。

比如:对于一个二进制数100000...000(共127个0),如果不用科学计数法,需要16个字节来存储。如果用科学计数法:1*2^127,只需要用二进制表示出:有效数字和指数即可,压根不需要16个字节。

IEEE浮点标准用V=(-1)^s * M * 2^E的形式来表示一个数:

  • 符号:s决定该数是正数还是负数(0正1负),而对于数值0的符号位解释作为特殊情况处理。
  • 尾数(相当于有效数字):M是一个二进制小数,对于规格化表示:1<=M<2
  • 阶码(相当于指数):阶码E决定了二进制小数的小数点位置。(可为负数)

将一个浮点数的表示转成如上形式,然后分别对符号、尾数和阶码进行编码就能得到浮点数的机器表示
如下,IEEE标准规定:

  • 对于单精度浮点数,1位符号位+8位阶码位+23位尾数位。
  • 对于双精度浮点数,1位符号位+11位阶码位+52位尾数位。

IEEE标准规定:阶码位表示的是无符号数e,阶码E无符号数e的关系是:E = e - (2^(n-1) - 1)

比如,对于单精度浮点数(8位阶码位),无符号数e的范围是[0, 255],因此E = e - (2^7 - 1) = e - 127,所以阶码E的范围是[-127, 128],即指数的范围是[-127, 128]。

尾数的规格化表示: 尾数M必须1<=M<2

为什么要规格化?保证浮点数有唯一的表示。若不对浮点数的表示作出明确规定,同一个浮点数的表示就不是唯一的,比如对于十进制数1.75表示可能有1.11*2^0、0.111*2^1、0.0111*2^2等。

规格化表示后,尾数一定是1.xxxx由于第一位一定是1,所以不需要显式地表示它,因此尾数位全部用来表示尾数1.xxxx之后的xxxx部分,也就是说【尾数的长度(去除小数点)】最多是【尾数位+1位】,尾数超过这个长度之后的数字位都会被舍弃,从而出现精度问题。

示例

将如下十进制数转成单精度浮点数表示:

  • 1.5
  • -12.5

对于1.5,其二进制小数是1.1,按照IEEE标准转成V=(-1)^s * M * 2^E的形式,即(-1)^0 * 1.1 * 2^0,所以:s=0;M=1.1;E=0。然后分别用1位符号位、8位阶码位和23位尾数位进行编码:

  • 因为是正数,所以单精度浮点数的1位符号位为0
  • 尾数是1.1,尾数位只需存储1.1之后的1,因此单精度浮点数的23位尾数位就是10000000000000000000000
  • 阶码E=0,则e=E+127=127,因此阶码位对应的无符号数e是127,所以单精度浮点数的8位阶码位是01111111

最终,二进制表示为00111111110000000000000000000000

同理,对于-12.5,其二进制小数是-1100.1,即(-1)^1 * 1.1001 * 2^3,所以:s=0;M=1.1001;E=3。然后分别用1位符号位、8位阶码位和23位尾数位进行编码:

  • 因为是负数,所以符号位是1
  • 尾数是1.1001,尾数位只需存储1.1001之后的1001,所以尾数位是10010000000000000000000
  • 阶码E=3,则e=E+127=130,因此阶码位对应的无符号数e是130,则阶码位是10000010

最终,二进制表示为11000001010010000000000000000000

精度问题

单精度浮点数有23位尾数位,最多只能表示23+1=24位长度的尾数;双精度浮点数有52位尾数位,最多只能表示52+1=53位长度的尾数。 也就是说:

  • 对于单精度浮点数,若尾数的长度超过24位,24位之后的数字就无法存储,会出现精度问题
  • 对于双精度浮点数,若尾数的长度超过53位,53位之后的数字就无法存储,会出现精度问题

到此这篇关于浅析Golang中float64的精度问题的文章就介绍到这了,更多相关Go float64精度内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Go语言对JSON进行编码和解码的方法

    Go语言对JSON进行编码和解码的方法

    这篇文章主要介绍了Go语言对JSON进行编码和解码的方法,涉及Go语言操作json的技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-02-02
  • 一文详解Go Http Server原理

    一文详解Go Http Server原理

    这篇文章主要为大家介绍了Go Http Server原理示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-01-01
  • 教你用go语言实现比特币交易功能(Transaction)

    教你用go语言实现比特币交易功能(Transaction)

    每一笔比特币交易都会创造输出,输出都会被区块链记录下来。给某个人发送比特币,实际上意味着创造新的 UTXO 并注册到那个人的地址,可以为他所用,今天通过本文给大家分享go语言实现比特币交易功能,一起看看吧
    2021-05-05
  • 解决golang gin框架跨域及注解的问题

    解决golang gin框架跨域及注解的问题

    这篇文章主要介绍了解决golang gin框架跨域及注解的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-03-03
  • Golang 单元测试和基准测试实例详解

    Golang 单元测试和基准测试实例详解

    这篇文章主要为大家介绍了Golang 单元测试和基准测试实例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • Go语言Gin框架中使用MySQL数据库的三种方式

    Go语言Gin框架中使用MySQL数据库的三种方式

    本文主要介绍了Go语言Gin框架中使用MySQL数据库的三种方式,通过三种方式实现增删改查的操作,具有一定的参考价值,感兴趣的可以了解一下
    2023-11-11
  • Golang记录、计算函数执行耗时、运行时间的一个简单方法

    Golang记录、计算函数执行耗时、运行时间的一个简单方法

    这篇文章主要介绍了Golang记录、计算函数执行耗时、运行时间的一个简单方法,本文直接给出代码实例,需要的朋友可以参考下
    2015-07-07
  • 用go实现反向代理的代码示例

    用go实现反向代理的代码示例

    当实现反向代理时,Go语言是一个强大而受欢迎的选择,Go具有出色的并发性和网络编程支持,使其成为构建高性能反向代理的理想工具,在本文中,我将介绍如何使用Go语言实现一个简单的反向代理服务器,并提供相应的源代码,需要的朋友可以参考下
    2023-06-06
  • Golang中如何使用lua进行扩展详解

    Golang中如何使用lua进行扩展详解

    这篇文章主要给大家介绍了关于Golang中如何使用lua进行扩展的相关资料,这是最近在工作中遇到的一个问题,觉着有必要分享出来给大家学习,文中给出了详细的示例,需要的朋友可以参考借鉴,下面来一起看看吧。
    2017-10-10
  • golang中切片copy复制和等号复制的区别介绍

    golang中切片copy复制和等号复制的区别介绍

    这篇文章主要介绍了golang中切片copy复制和等号复制的区别,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04

最新评论