浮点数精度问题详解

(/≧▽≦)/~┴┴ 嗨~我叫小奥 ✨✨✨
👀👀👀 个人博客:小奥的博客
👍👍👍:个人CSDN
⭐️⭐️⭐️:Github传送门
🍹 本人24应届生一枚,技术和水平有限,如果文章中有不正确的内容,欢迎多多指正!
📜 欢迎点赞收藏关注哟! ❤️

问题引入

首先我们来看下面一组代码的执行结果

	public static void main(String[] args) {
        System.out.println(2.0f - 1.9f == 0.1f);  // false
        System.out.println(16777218.0f - 16777217.0f == 2.0f); // true
        System.out.println(2.0d - 1.9d == 0.1d);  // false
        System.out.println(18_014_398_509_481_988d - 18_014_398_509_481_986d == 4d);  // true
    }

浮点数实现原理

浮点类型

浮点类型用于表示有小数的部分。在Java中有两种浮点类型,具体内容如表3-2表示。

在这里插入图片描述

一般情况应该尽量避免使用float。

浮点数中有效位数的含义:浮点数能精确表达的十进制的位数

1.123456	1位整数 + 6位小数 = 7位十进制数
1000.123	4位整数 + 3位小数 = 7位十进制数
16777218	共8位十进制数,前7位为有效位数,最后1位为无效位(float无法精确表达)

正确使用浮点数

(1)比较两个浮点数是否相等,不能直接使用 == 号(全等号)。

	public static void main(String[] args){
        double precision = 0.000001; // 10^-6
        double d1 = 2.0d - 1.9d;
        double d2 = 0.1d;
        if (Math.abs(d1 - d2) < precision) {
            // 在精度范围内,d1等于d2
            System.out.println("true");
        }
    }

(2)尽量不要使用float,而是使用double。

(3)涉及到金额的场景,一定要使用BigDecimal类。

浮点数的原理

在计算机的底层,所有的数据都是由0和1存储的,也就是二进制,每个0和1都是一个bit,小数也是同样如此。

在计算机中,通常使用IEEE754标准来规定如何以二进制的方式表示存储十进制的数据。

以float单精度浮点数为例:

在这里插入图片描述

  • 符号位:0表示负数,1表示整数
  • 偏移指数位(biased exponent):用来表示规范化后的指数值
  • 尾数位(fraction):用来表示规范化后的尾数值

IEEE754规范化

十进制:10.75
二进制:b(1010.11)
规范化:b(1.01011) * 2^3,其中1.01011是尾数, 3是指数

偏移指数位

假设规范化后,指数值为E,
对于floatE的取值范围规定是[-127, 128],为了避免负数,于是将E加上一个固定值的偏移量127. E' = E + 127E'的取值范围是[0, 255]E'即“偏移指数”,用“偏移指数位”中的8个比特来表示
十进制:10.75
规范化:b(1.01011) * 2^3
指数值:3
偏移指数值: 3 + 127 = 130 (加上偏移量,避免出现负数)
偏移指数位: 1000 00108位比特的二进制)

尾数位

假设规范化后,尾数为F
对于float:
	尾数F最多使用23位比特来存储。F的最高位肯定是1,为了增加精度,在存储时,我们可以省略掉最高位,只存储小数后的数字
十进制:10.75
规范化:b(1.01011) * 2^3
尾数值:1.01011
尾数位:0101 1000 0000 0000 0000 000 (23位)

总结

将十进制10.75转化为二进制数:

在这里插入图片描述

再由二进制反推出计算机内十进制的表示:

在这里插入图片描述

最后浮点数10.75在计算机内表示为1.3125。

注意:二进制天生不能精确表达某些十进制数字

比如:

0.1 = 2^-4 + 2^-5 + 2^-8 + 2^-9 + 2^-12 + 2^-13+… B(0.000 110 011 001 100 …) 无限二进制表示

而在计算机中,浮点数的存储位数是有限的。

解释下最开始的代码问题

System.out.println(2.0f - 1.9f == 0.1f);  // false

在这里插入图片描述

System.out.println(16777218.0f - 16777217.0f == 2.0f); // true

在这里插入图片描述

这也就是说float精度是7位有效位数的经典示例。

大数吃小数问题

	public static void main(String[] args){
        float s = 0f;
        float x = 1f;
        for(int i = 0; i < 3000_0000; i++) {
            s += x;
        }
        System.out.printf("%1.10f\n", s);
    }
	// 输出
	// 16777216.0000000000

下面我们讲解一下浮点数的相加过程:

  • 指数位对齐,小的向大的对齐
  • 尾数求和
  • 规范化

0.5 + 0.125为例:

0.5 = (-1)^0 * 1.0 * 2^-1
0.125 = (-1)^0 * 1.0 * 2^-3

0.5的指数位-1,0.125的指数为-3,因此0.125的有效位需要右移,即 0.125 = (-1)^0 * 0.01 * 2^-1

然后相加 0.5 + 0.125 = (-1)^0 * 1.01 * 2^-1

从浮点数相加的过程我们知道,由于浮点数有效位是23位,当较大的数是较小数的2^24 = 16777216倍及以上的时候,较小的数为了将指数位向较大的数对齐,有效位就会右移,右移超过了23位,就会成为无效位。

解决方法:使用更高精度的数据类型,尽量避免较大数和较小数直接相加,或者通过补偿的方式。

(1)使用double代替float。

	public static void main(String[] args){
        double s = 0d;
        double x = 1d;
        for(int i = 0; i < 3000_0000; i++) {
            s += x;
        }
        // 输出:30000000.0000000000
        System.out.printf("%1.10f\n", s);
    }

(2)分段求和。

	public static void main(String[] args){
        float s = 0f;
        float s1 = 0f, s2 = 0f, s3 = 0f;
        float x = 1f;
        for(int i = 0; i < 1000_0000; i++) {
            s1 += x;
        }
        for(int i = 0; i < 1000_0000; i++) {
            s2 += x;
        }
        for(int i = 0; i < 1000_0000; i++) {
            s3 += x;
        }
        // 输出:30000000.0000000000
        s = s1 + s2 + s3;
        System.out.printf("%1.10f\n", s);
    }

(3)Kahan求和

思想:保存较大数和较小数相加过程中,较小数丢失的有效数字,然后补偿回来。

	public static void main(String[] args){
        // s 总和  eps 丢失的有效数字 t 临时和
        float s = 0f, eps = 0f,  t = 0f;
        float x = 1f;
        for(int i = 0; i < 3000_0000; i++) {
           float y = x - eps; // y是被截断的小数
           t = s + y; // s是大的,y是小的,所以y的低位数字会丢失
           eps = t - s - y; // t - s 恢复y的高位部分, -y 恢复y的低位部分
           s = t; // 保存总和s
            // 下一次循环,丢失的部分将会尝试添加到y中
        }
        // 输出:30000000.0000000000
        System.out.printf("%1.10f\n", s);
    }
  • 16
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值