Golang浮点数

本文深入探讨了浮点数在计算机中的表示方式,包括单精度和双精度浮点数的结构、数值范围以及精度。解释了浮点数有两个零、没有0.3的原因,并揭示了浮点数不满足结合律的现象。同时,文章讨论了浮点数中的无穷和NaN,以及指数采用移码的原因。最后,通过实例说明了浮点数运算的不精确性,强调了四舍五入的潜在问题。
摘要由CSDN通过智能技术生成

计算机的内存也是有限的。在主流的编程语言中,浮点数一般有32bit和64bit两种,分别对应单精度和双精度浮点数(有些地方还有半精度的浮点数),也就是说一个32bit的内存只能表达2^32种状态(大概40亿)!但是实际上数从无穷小到无穷大范围异常的广泛。计算机二进制位不够存储。所以发明了科学技术法来表达很大或者很小的数。浮点数也是采用类似规范化的科学记数法的思路。而IEEE754是浮点数格式的国际标准,目前主流的编程语言都是采用这个标准。

浮点数的结构与数值范围

单精度浮点数在机内占4个字节,用32位二进制描述。
双精度浮点数在机内占8个字节,用64位二进制描述。

浮点数在机内用指数型式表示,分解为:数符,尾数,指数符,指数四部分。
数符占1位二进制,表示数的正负。
指数符占1位二进制,表示指数的正负。
尾数表示浮点数有效数字,0.xxxxxxx,但不存开头的0和点
指数存指数的有效数字。

指数占多少位,尾数占多少位,由计算机系统决定。
可能是数符加尾数占24位,指数符加指数占8位 – float.
数符加尾数占48位,指数符加指数占16位 – double.

知道了这四部分的占位,按二进制估计大小范围,再换算为十进制,就是你想知道的数值范围。

这是C语言标识的float浮点数的内存布局:

// Little endian
union ieee754_float {
    float f;

    /* This is the IEEE 754 single-precision format.  */
    struct {
        unsigned int mantissa:23;
        unsigned int exponent:8;
        unsigned int negative:1;
    } ieee;

    /* This format makes it easier to see if a NaN is a signalling NaN.  */
    struct {
        unsigned int mantissa:22;
        unsigned int quiet_nan:1;
        unsigned int exponent:8;
        unsigned int negative:1;
    } ieee_nan;
};

其中最低的23it是mantissa对应有效数字,然后是8bit的exponent表示指数,最高位的1bit表示符号位。需要注意的是指数部分采用移码表示,也就是exponent作为无符号数减去127得到的最终的指数。

1、数值范围

float和double的范围是由指数的位数来决定的。

float的指数位有8位,而double的指数位有11位,分布如下:

float32:

符号位指数位尾数位
1bit8bits23bits

float64:

符号位指数位尾数位
1bit11bits52bits

于是,float的指数范围为-127+128,而double的指数范围为-1023+1024,并且指数位是按补码的形式来划分的。

其中负指数决定了浮点数所能表达的绝对值最小的非零数;

而正指数决定了浮点数所能表达的绝对值最大的数,也即决定了浮点数的取值范围。

float的范围为-2^128 ~ +2^128,也即-3.40E+38 ~ +3.40E+38;

double的范围为-2^1024 ~ +2^1024,也即-1.79E+308 ~ +1.79E+308。

float的精度问题

float和double的精度是由尾数的位数来决定的。浮点数在内存中是按科学计数法来存储的,其整数部分始终是一个隐含着的“1”,由于它是不变的,故不能对精度造成影响。

float:2^23 = 8388608,一共七位,这意味着最多能有7位有效数字,也即float的精度为6~7位有效数字,但绝对能保证的为6位。

double:2^52 = 4503599627370496,一共16位,同理,double的精度为15~16位,但绝对能保证的为15位

浮点数的诡异:浮点数有2个0

知道了浮点数的布局,就自然可以理解为什么浮点数会有2个0。因为浮点数有一个独立的符号位,只要吧0.0的符号位设置为负数就可以得到一个负数0:

fmt.Println(math.Float32frombits(0))     // 0
fmt.Println(math.Float32frombits(1<<31)) // -0

上面的代码通过在0的基础之上加1<<31将符号位设置为1,这样就得到了负0。
因此用浮点数作为map的Key时,可能会遇到一些诡异的事件。比如-0能取到0对应的值吗?

浮点数的诡异:没有0.3

fmt.Println内部打印的是字符串“0.3”,不是浮点数的0.3!
下面这个代码:

func main() {
    fmt.Printf("%f\n", 0.3)
    fmt.Printf("%.10f\n", 0.3)
    fmt.Printf("%.20f\n", 0.3)
}

// Output:
// 0.300000
// 0.3000000000
// 0.29999999999999998890

每次输出的结果都不一样,fmt包打印0.3的原因是因为IEEE754中不存在0.3这个数!
对于Go语言中,常量和变量的运算规则是不同的,因此0.1+0.2换成变量就会发生变化:

func main() {
    var a = 0.1
    var b = 0.2
    fmt.Println(0.1+0.2, a+b)
}
// 0.3 0.30000000000000004

第一个输出是0.3,第二个居然不是0.3。还好我们已经知道没有0.3这个浮点数,因此第一个0.1+0.2的结果也不是0.3。不能表示0.3的原因是因为计算机采用的是二进制的科学记数法,而0.3无法通过用2的不同指数的状态组成得到。
不仅没有0.3,浮点数中缺少的数字多了去了。根据抽屉原理,float32只能表示2的32次方个状态,也就是40多亿个数。但是float32的值域可是已经大大超出了40亿的范围。

无穷有正负

无穷在浮点数中对应一个特殊的数(或者说指数值最大的那种0)。参考前面的浮点数内存布局,指数有8个bit表示,最大的指数表示是255。
下面我们构造一个指数部分是255的0:

fmt.Println(math.Float32frombits(255<<23)) // +Inf 

这样得到的就是一个正的无穷(255最大的指数表示穷,0有效位表示无)。
有了正无穷,得到负无穷就比较容易了。只要加一个符号标志位即可:

fmt.Println(math.Float32frombits(255<<23 + 1<<31)) // -Inf

用科学记数法表示是这样:0.0*2^(255-127)

Nan不是一个数,它表示的是一种bit模式

在IEEE754浮点数的指数值域中255是最重要的一个,因为无穷对应的指数就是255。在无穷中除了指数是255之外,有效位部分是0。那么问题来了,指数为255,有效位不是0的是啥玩意?
可以跑代码看看:

fmt.Println(math.Float32frombits(255<<23 + 1)) // NaN
fmt.Println(math.Float32frombits(255<<23 + 2)) // NaN

它们都是NaN,也就是Not-a-Number,非数不是数。NaN不是一个非数,而是一类非数。
Nan有个重要的特性,就是自己和自己都不相等(想想如果用它作为map的key会有什么效果)。Nan是非法的运算得来的,比如sqrt(-1)或0/0都是非数。

浮点数不满足结合率

浮点数不满足结合率,比如a+(b+c)和(a+b)+c不等价。具体原因和开头的x=x+1方程类似。
如果x=x+1成立,但是x=x+2并不一定成立。如果x!=x+2,那么显然它和x=(x+1)+1结果是不等价的。

四舍五入是错误的

四舍五入分为2个部分,四舍还是四舍,但是五入就不一定是五入了。还存在五舍的情形。五作为一个绝对中间的位置,凭什么总是要五入(借钱的人和还钱的人肯定也有不同的看法)?
那什么时候需要五舍?根据计算的结果长得是否漂亮,选择五舍或五入。

指数为什么要采用移码

在早年间,浮点数运算芯片是一个奢侈的玩意。码农们都不做浮点数运算。但是运气也有背的时候,比如需要给一个浮点数表示的数组排序。
正如凌凌漆所言,IEEE754并非浪得虚名。即使没有浮点数芯片,码农们依然可以快速给浮点数数组排序:将浮点数数组当中整数数字进行排序就可以了。
为何?因为浮点数数组有序的话,那么同样bit模式的整数数组依然是有序的。其中第一个原因是符号位保证有序,第二个原因是指数采用移码保证大指数较大,最后几十有效数字部分比较小数点部分大数字。

解浮点数方程: x+1=x
不是纯数学意义上的方程, 对应计算机的一个浮点数问题:

if((float)(x+1.0) == (float)(x)) { x = ? }

简单分析, ieee754中float采用23bit表示有效位, 再加省略的1, 共有24bit.当结果超出24bit时, 小数部分被被丢失:

var a float32 = 1 << 24
var b float32 = a+1
fmt.Println(math.Float32bits(a)) // 1266679808fmt.Println(math.Float32bits(b)) // 1266679808

因此浮点数运算是不满足结合率的!

浮点数分类

首先是符号位,根据符号位可以分为正数和负数两类(包括两种0)。不过符号位分类比较简单,这里忽略。此外,根据指数位和有效数字部分的不同组合,可以分为以下几种:

switch {
case 指数 在 1-254 之间:    
	// 规范的浮点数(不是0,也不是很小的浮点数)
	// 也就是小数点前是1,但是这位被省略了
	// 我们平时见到的非0浮点数大多数都是这类
case 指数 == 0:    
	if 有效数 == 0 {        
		// 这是 0    
	} else {
        // 非规范的浮点数,对应很小的浮点数  
        // 也就是小数点前是0,但是这位被省略了    
    }
case 指数 == 255:    
	if 有效数 == 0 { 
	    // 无穷
    } else { 
       // NaN 
    }
}

所以说指数部分是IEEE754的精髓。

浮点数在数轴的分布

在这里插入图片描述
浮点数分布不均匀:越靠近原点越密;同一指数阶码分布均匀。

原文地址:原文地址

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值