浮点类型是如何存储的

2020-09-21  本文已影响0人  王某某的笔记

计算机如何存储字节

计算机中最小的存储单位是bit只能保存0和1,整数在内存中如何存储我们都知道,将要存储的数字转成2进制即可

用windows自带的计数器可以方便的查看整数对应的2进制值
如:
byte类型(单字节)

十进制 二进制
8 0000 1000
9 0000 1001
100 0110 0100
-5 1111 1011
-8 1111 1000

第一位为符号位,负数等于正数取反 +1

那浮点类型是如何用这么少的字节(如float 4字节)表示这么大(float 最大 3.4028235E38)的数字呢?

浮点类型是如何存储的

浮点数

浮点数,是属于有理数中某特定子集的数的数字表示,在计算机中用以近似表示任意某个实数。具体的说,这个实数由一个整数或定点数(即尾数)乘以某个基数(计算机中通常是2)的整数次幂得到,这种表示方法类似于基数为10的科学计数法。

科学计数法

科学计数法是一种记数的方法。把一个数表示成a与10的n次幂相乘的形式(1≤|a|<10,a不为分数形式,n为整数),这种记数法叫做科学计数法。当我们要标记或运算某个较大或较小且位数较多时,用科学计数法免去浪费很多空间和时间。

java中的浮点数字遵循 IEEE 754 标准

这也是一种目前最常用的浮点数标准!为许多CPU与浮点运算器所采用。

简单的说就是将一个浮点数字拆成3个部分(符号部分、指数部分、小数部分) 存储在连续的bit中,类似科学计数法。

用 {S,E,M}来表示一个数 V 的,即 V =(-1)S × M × 2E,如下:

S(符号位) E(指数位) M(有效数字位)

其中:

IEEE 754浮点数规范

+- d.ddd...d * β^e   (0 <= di < β)  

其中d.dd...d 为有效数字,β为基数,e 为指数

有效数字中 数字的个数 称为精度,我们可以用 p 来表示,即可称为 p 位有效数字精度。
每个数字 d 介于 0 和基数 β 之间,包括 0。

十进制表示

对十进制的浮点数,即基数 β 等于 10 的浮点数而言,上面的表达式非常容易理解。
如 12.34,我们可以根据上面的表达式表达为:
1×101 + 2×100 + 3×10-1 + 4×10-2
其规范的浮点数表达为:1.234×101

二进制表示

但对二进制来说,上面的表达式同样可以简单地表达。
唯一不同之处在于:二进制的 β 等于 2,而每个数字 d 只能在 0 和 1 之间取值。

如二进制数 1001.101,我们可以根据上面的表达式表达为:
1×23 + 0×22 + 0×21 + 1×20 + 1×2-1 + 0×2-2 + 1×2-3
其规范浮点数表达为:1.001101×23

二进制转换为十进制

二进制数 1001.101 转成十进制如下:

= 1 × 23 + 0 × 22 + 0 × 21 + 1 × 20 + 1 × 2-1 + 0 × 2-2 + 1×2-3
= 8 + 0 + 0 + 1 + 1/2 + 0 + 1/8
= 9又1/8 (9又8分之1)
= 9.625

由上面的等式,我们可以得出:
向左移动二进制小数点一位相当于这个数除以 2,而向右移动二进制小数点一位相当于这个数乘以 2。
如 101.11 = 5又3/4 (5.75),向左移动一位,得到 10.111 = 2又7/8 (2.875)。

除此之外,我们还可以得到这样一个基本规律:
一个十进制小数要能用浮点数精确地表示,最后一位必须是 5(当然这是必要条件,并非充分条件)。
如下面的示例所示:

二进制小数 2的多少次方 十进制的小数
0.1 2-1 0.5
0.01 2-2 0.25
0.001 2-3 0.125
0.0001 2-4 0.0625
0.00001 2-5 0.03125
0.000001 2-6 0.015625
... ... ...

十进制转换为二进制

基本换算方法:
将10进制的数拆分成整数和小数两个部分
整数部分除以2,取余数;小数部分乘以2,取整数位。

示例:
将十进制 1.1 转成 二进制

整数部分:1
1

小数部分:0.1

0.1 *2 = 0.2 -> 0
0.2 *2 = 0.4 -> 0
0.4 *2 = 0.8 -> 0
0.8 *2 = 1.6 -> 1
0.6 *2 = 1.2 -> 1
0.2 *2 = 0.4 -> 0
0.4 *2 = 0.8 -> 0
0.8 *2 = 1.6 -> 1
0.6 *2 = 1.2 -> 1
0.2 *2 = 0.4 -> 0
0.4 *2 = 0.8 -> 0
0.8 *2 = 1.6 -> 1
0.6 *2 = 1.2 -> 1
0.2 *2 = 0.4 -> 0
0.4 *2 = 0.8 -> 0
0.8 *2 = 1.6 -> 1
......

二进制形式表示为:
1.000110011001100110011...

再将上面的数换算成10进制

1 + 1/16 + 1/32 + 1/256 + 1/512  + ...
约等于  
(32 + 16 + 2 + 1)/512    
约等于  
  51/512  
约等于  
  0.099609375

再加上整数1,约等于:
1.099609375

计算的位数越多越精确

注意:
二进制小数不像整数一样,只要位数足够,它就可以表示所有整数。
在有限长度的编码中,二进制小数一般无法精确的表示任意小数,比如十进制小数0.2,我们并不能将其准确的表示为一个二进制数,只能增加二进制长度提高表示的精度。

十进制的0.2 转换成二进制为:0.00110011001100110011001100110011001100110011001100110......


java API 中对Double 和 Float 类型的描述

Double 双精度浮点数 64bit (8byte)

根据 IEEE 754 浮点“双精度格式”位布局。

如果参数是正无穷大,则结果为 0x7ff0000000000000L。
如果参数是负无穷大,则结果为 0xfff0000000000000L。
如果参数是 NaN,则结果为 0x7ff8000000000000L。

Float 单精度浮点数 32bit (4byte)

根据 IEEE 754 浮点“单一格式”位布局。

如果参数为正无穷大,则结果为 0x7f800000。
如果参数为负无穷大,则结果为 0xff800000。
如果参数为 NaN,则结果为 0x7fc00000。

掩码位说明

这里以 double类型说明

将一个浮点数与上面的掩码进行与运算,即可得到对应的 符号位、指数位、尾数位 的值。

这里的多少多少位是从右往左数的,当转成2进制不够64位时在前面补零即可


按照浮点数计算规范要求:(划重点)

故前面十进制数(1.1)的二进制形式:

1.000110011001100110011...

用浮点类型表示:

所以存为:
0 01111111111 000110011001100110011...

用java代码输出进行验证

System.out.println(Long.toBinaryString(Double.doubleToLongBits(1.1)));
//11111111110001100110011001100110011001100110011001100110011010
//这种情况前面要补两个0
//0011111111110001100110011001100110011001100110011001100110011010

System.out.println(Long.toBinaryString(Double.doubleToLongBits(-1.1)));
//1011111111110001100110011001100110011001100110011001100110011010

浮点数精度问题

根据 IEEE 754 规范

在二进制,第一个有效数字必定是“1”,因此这个“1”并不会存储。
单精和双精浮点数的有效数字分别是有存储的23和52个位,加上最左边没有存储的第1个位,即是24和53个位。

通过计算其能表示的最大值,换十进制来看其精度:

为什么会丢失精度

浮点运算很少是精确的,只要是超过精度能表示的范围就会产生误差。而往往产生误差不是因为数的大小,而是因为数的精度。

我自己理解为分两种情况(这个不一定是对)

  1. 当有小数时,浮点数本身就不能精确记录其数值,只记了一个近似值,此时进行计算就很可能不对
  2. 有效位数不够用了,导致舍入

1、不能精确记录其数值

通过上面的转换示例,我们知道小数的二进制表示一般都不是精确的,在有限的精度下只能尽量的表示近似值

值本身就不是精确的,再进行计算就很可能产生误差

0.1+0.2=0.30000000000000004
示例代码:
double d1 = 0.1;
double d2 = 0.2;
double d3 = d1 + d2;
System.out.println(d3);
System.out.println("######################################");
System.out.println(new BigDecimal(d1));
System.out.println(new BigDecimal(d2));
System.out.println(new BigDecimal(d3));

System.out.println("######################################");
System.out.println(Long.toBinaryString(Double.doubleToLongBits(d1)));
System.out.println(Long.toBinaryString(Double.doubleToLongBits(d2)));
System.out.println(Long.toBinaryString(Double.doubleToLongBits(d3)));

输出:

0.30000000000000004
######################################
0.1000000000000000055511151231257827021181583404541015625
0.200000000000000011102230246251565404236316680908203125
0.3000000000000000444089209850062616169452667236328125
######################################
11111110111001100110011001100110011001100110011001100110011010
11111111001001100110011001100110011001100110011001100110011010
11111111010011001100110011001100110011001100110011001100110100

0.1
原始值: 0 01111111011 1001100110011001100110011001100110011001100110011010
指数:1019 -1023 = -4
二进制形式:
0.00011001100110011001100110011001100110011001100110011010

0.2
原始值:0 01111111100 1001100110011001100110011001100110011001100110011010
指数:1020 -1023 = -3
二进制形式:
0.001001100110011001100110011001100110011001100110011010

0.3
原始值:0 01111111101 0011001100110011001100110011001100110011001100110100
指数:1021 = -2
二进制形式:
0.010011001100110011001100110011001100110011001100110100

二进制加法运算

0.00011001100110011001100110011001100110011001100110011010
+
0.001001100110011001100110011001100110011001100110011010
=
0.010011001100110011001100110011001100110011001100110100
其他示例:
double a = 0.03;
double b = 0.01;
System.out.println(a - b); 
//结果 0.019999999999999997

double x = 10.2;
double y = 10.03;
System.out.println(x + y); 
//结果 20.229999999999997

double dx = 1.099999999999999999999999999999d;
System.out.println(dx);
//结果 1.1

double dy = 1.1000000000000000000000000000001d;
System.out.println(dy);
//结果 1.1

2、有效位数不够用

这里用float验证,float最大的精度是8位数

// 有效位数不够
float f = 1.23456789f;
System.out.println(f);   // 1.2345679   最大只能有8位

System.out.println("=====================");
float f1 = 10000f;
System.out.println(f1);  // 10000.0
float f2 = 1.123456f;
System.out.println(f2);  // 1.123456

// 相加之后有效位数不够
float f3 = f1 + f2;
System.out.println(f3);  // 10001.123   位数不够,后面的被省略了

关于舍入

对于不能精确的表示的数,采取一种系统的方法:找到“最接近”的匹配值,它可以用期望的浮点形式表现出来,这就是舍入。

对于舍入,可以有很多种规则,可以向上舍入,向下舍入,向偶数舍入。如果我们只采用前两种中的一种,就会造成平均数过大或者过小,实际上这时候就是引入了统计偏差。如果是采用偶数舍入,则有一半的机会是向上舍入,一半的机会是向下舍入,这样子可以避免统计偏差。而 IEEE 754 就是采用向最近偶数舍入(round to nearest even)的规则。

(这段是网上抄的)


其他

大端 小端问题

这里以java语言示例,用大端的方式示例(网络序)

java中是以大端模式存储的,java对我们屏蔽了内部字节顺序的问题以实现跨平台!

实际在不同的cpu架构下,存储方式不同,我们常用的X86是以小端的模式存储的。

网络传输一般采用大端序,也被称之为网络字节序,或网络序。IP协议中定义大端序为网络字节序。


测试代码

二进制字符串转成Double

public static void main(String[] args) {
    // 4607632778762754458
    String s = "0011111111110001100110011001100110011001100110011001100110011010";
    System.out.println("二进制字符串 = " + s);
    long l = Long.parseLong(s, 2);
    System.out.println("转成Long = " + l);
    double d = Double.longBitsToDouble(l);
    System.out.println("再将Long转成Double = " + d);
}

输出:

二进制字符串 = 0011111111110001100110011001100110011001100110011001100110011010
转成Long = 4607632778762754458
再将Long转成Double = 1.1

写内存的方式转Double

/**
 * bit字符串转Double
 * 
 * @param bitStr 64位的01字符串
 * @throws Exception
 */
public static void bit2Double(String bitStr) throws Exception {
    String[] array = splitString(bitStr, 8);
    System.out.println(Arrays.toString(array));

    Unsafe unsafe = getUnsafe();
    long address = unsafe.allocateMemory(8L);

    for (int i = 0; i < array.length; i++) {
        String bits = array[i];
        byte bt = (byte) Integer.parseInt(bits, 2);
        // 因为实际上是小端模式存储的,所以这里从后面开始写入
        unsafe.putByte(address + (7 - i), (byte) bt);
    }

    long lVal = unsafe.getLong(address);
    System.out.println("对应的long值是:" + lVal);
    System.out.println(Long.toBinaryString(lVal));

    double dVal = unsafe.getDouble(address);
    System.out.println("转成Double 类型是:" + dVal);
    System.out.println(Long.toBinaryString(Double.doubleToLongBits(dVal)));

}

public static String[] splitString(String source, int length) {
    String[] array = new String[length];
    for (int i = 0; i < length; i++) {
        array[i] = source.substring(i * length, (i + 1) * length);
    }
    return array;
}

public static Unsafe getUnsafe() throws Exception {
    Field f = Unsafe.class.getDeclaredField("theUnsafe");
    f.setAccessible(true);
    Unsafe unsafe = (Unsafe) f.get(null);
    return unsafe;
}
上一篇 下一篇

猜你喜欢

热点阅读