float

2022/02/13

很多人大概都知道整型变量可以直接(即按二进制位逐位比较)进行相等判断,但浮点型变量则不可以。原因很简单,在一定范围内,整数个数是有限的,所以整型可以在一定条件下表示所有整数。但对于实数,即使范围给定,其个数依旧是无穷的,所以无法用有限的二进制位数来一一对应某范围的所有实数。因而,实数在计算机里只能近似表示,也就是表示为我们熟知的(二进制)浮点数。如后文所述,其设计的特殊性决定了与整型变量之间的用法差异。

浮点数的设计,之前几乎每个处理器厂商都有自己的解决方案。但1985年出现的IEEE-754标准,因其简单易懂且易于实现,便成了最为广泛使用的浮点数运算标准,为许多处理器厂商所采用。

IEEE-754标准表示的二进制浮点数,包含三个部分:符号位、指数部分和尾数部分

那么,在golang语言中,浮点型具体又是怎么存储的呢?

浮点型结构

在Go语言编程中,用来表示小数的有两种类型:

  • float32(单精度类型,占据4个字节 byte,32个二进制位 bit)
  • float64(双精度类型,占据8个字节 byte,64个二进制位 bit)

我们知道,任何数据存储到计算机中都会变成0 1这样的二进制代码,只是不同类型的数据编码方式不同

对于浮点数,可以认为氛围两部分:整数和小数。对于整数部分来说,方式和int类型一样,从地位到高位,分别表示2的0次方 2^0、2^1、2^2 … … 等;对于小数部分来说,编码方式逻辑上一致,具体实现有所差异,从左到右每一位分别标识2的-1次方 2^-1、2^-2、2^-3 … …

首先,我们试着用二进制数据来表示一个小数:

二进制 十进制 计算方式
3.5 11.1 2^0 + 2^0 + 2^-1
10.625 1010.101 2^3 + 2^1 + 2^-1 + 2^-3
0.6 0.10011001… 2^-1 + 2^-4 + 2^-5 + 2^-8 + …

转换成以 2 为底的科学计数法:

二进制 十进制 计算方式
3.5 11.1 1.11 * 2^1
10.625 1010.101 1.010101 * 2^3
0.6 0.10011001… 1.0011001… * 2^-1

从上面我们可以观察到,对于任何数来说,表示成二进制科学计数法后,都成以转换成 1.xxx(尾数) * 2 的 n 次方(指数)。

这里需要注意到的一点是,比如上图中的十进制小数0.6,表示成二进制后变成了以1001循环的无限循环小数。

这便是浮点数有精度问题的根源之一,在代码中声明的小数0.6,计算机底层其实是无法精确存储那个无限循环的二进制数的。

只能存入一个零舍一入(类似于十进制的四舍五入)后的近似值。

对于小于0的负数来说,则可以表示成 -1.xxx(尾数) * 2 的 n 次方(指数)

所以内存中要存储这个小数,按照 IEEE-754标准 分成三部分:

  • 正负号
  • 指数
  • 尾数

如图所示:

cmd

具体存储方式如上图所示。最高位有1bit存储正负号,然后指数部分占据8bits(4字节)或11bits(8字节),其余部分全都用来存储尾数部分。

对于指数部分,这里存储的结果是实际的指数加上偏移量之后的结果。

这里设置偏移量,是为了让指数部分不出现负数,全都为大于等于0的正整数。

尾数部分的存储,因为二进制的科学计数法,小数点前一定是1开头,因此我们尾数只需要存储小数点后面的部分即可。

接下来依然是举例说明,4字节浮点数(Golang 中的 float32):

cmd

再来观察一个 8 字节浮点数(Golang 中的 float64)的例子:

cmd

偏移量:

  • 字节浮点数的偏移量为 127
  • 字节浮点数的偏移量为 1023

加上偏移量可以统一地把正数和负数统一转化成无符号的证书,方便进行比较,举例说明:

4字节浮点数的指数部分为 -7 ,则通常表示为 10000111 1为符号位,代表它是一个负数。

7 表示为 00000111 0为符号位,代表它是一个负数。

如果把 7 和 +7 统一加上偏移量 127

那么 7 就变成 134 ,二进制表示为 10000110

-7变成 120 ,二进制表示为 01111000

两者进行比较大小的时候,计算机便无需比较两者的符号位

具体事例

二进制表示

我们使用下面语句来打印float32浮点数对应的二进制表示

fmt.Printf("%b", math.Float32bits(data))
十进制 二进制 说明
3 0 10000000 10000000 00000000 0000000 11.0 = 1.1*2^1 指数部分为127+1=128,尾数部分为1
1 0 01111111 00000000 00000000 0000000 1.0 = 1.0*2^0 指数部分为127,尾数部分为0
0.5 0 01111110 00000000 00000000 0000000 0.1 = 1.0*2^-1 指数部分为127-1=126,尾数部分为0
-0.25 1 01111101 00000000 00000000 0000000 -0.01 = -1.0*2^-2 指数部分为127-2=125,尾数部分为0,符号位为1

问题描述

因为有些小数转换成二进制后会变成无限循环小数,所以存在精度问题

导致一个小数用float32和float64表示后,结果并不相等,比如: 0.3

fmt.Println(float64(float32(0.3)) == float64(0.3))

输出结果为 false

我们看看float32单精度表示,对应的二进制为 0 01111101 00110011 00110011 0011010

换成float64双精度,对应的二进制为0 01111111 10100110 01100110 01100110 01100110 01100110 01100110 0110011

先用单精度表示,然后强转成双精度,对应的二进制为0 01111111 10100110 01100110 01100110 10000000 00000000 00000000 0000000

可以看到,强转后的双精度和直接双精度结果并不一样,这是因为单精度只能保存32位信息,强转成64为,后面缺失的32位怎么办呢,这里会默认补0,所以会导致不一样

判断浮点数是否相等

像上面0.3的情况,肉眼可见认为是相等的,但实际跑出来的结果却不相等,这是由于计算机存储机制导致的

那我们怎么能准确判断两个浮点数是否相等呢?

提供两个思路

转换成string类型

func Decimal(value float32) string {
   value1 := fmt.Sprintf("%.6f", value)
   return value1
}
func Compare(val1,val2 string) bool {
   indexf :=strings.Index(val1,".") + 4
   indexs :=strings.Index(val2,".") + 4
   if indexs != indexf {
      return false
   }else {
      if val1[0:indexf] == val2[0:indexs]{
         return true
      }else {
         return false
      }
   }
}

精度判断

package main

import (
    "fmt"
    "math"
)
const MIN = 0.000001
// MIN 为用户自定义的比较精度
func IsEqual(f1, f2 float64) bool {
    if f1>f2{
        return math.Dim(f1, f2) < MIN
    }else{
        return math.Dim(f2, f1) < MIN
    }
}
func main() {
    a := 0.9
    b := 1.0
        
    if IsEqual(a, b) {
        fmt.Println("a==b")
    }else{
        fmt.Println("a!=b")
    }
}

数据库中金额元存储的数据结构使用的是decimal(15,2),为了避免浮点型精度导致的问题,建议金额最好转换成最小单位(厘),用整型表示

Post Directory