一.引言
上一遍文章介绍了二进制与十进制数字之间的转换,本文介绍现在应用比较广泛的浮点数标准 IEEE754。
二.IEEE754 简介
1.整体介绍
编辑IEEE754 代表二进制浮点算数标准,一般常用的为单精确度32位以及双精确度64位,还有不常用的延伸单精度43位以及延伸双精确度79位,Scala 常用的 Float 和 Double 分别采用了 IEEE754 的单精度32位和双精确度64位的标准。其中包含 Sign + Exponent + Fraction 三个值:
SIgn:符号位,0代表正,1代表负,很多时候正数会省略第一位的符号位0
Exponent:阶码或阶数,代表指数位
Fraction:分数值,对应的M为尾数,表示浮点数的有效数字
2.公式
对于 32 位的单精度浮点数,IEEE754 表示为:
编辑
对于 64 位的单精度浮点数,IEEE754 表示为:
编辑
SIgn:其中第一位代表符号位即正负数
M:其中 M ∈ [1,2),写成 1.xxxx 的形式,由于二进制数字保存时第一位总是1,所以此处只需要保留 xxxx 即可,所以这里采用了 1+M 的形式,这样可以节省1位存储位置
Exponent:32 位的情况下阶码 E 的取值范围为 8 bit,对应 32 位中的 2-9 位;64 位的情况下阶码E 的取值范围为 11bit,对应 64 位中的 2-12 位。以单精度浮点数为例,它的指数域为 8 bit,固定偏移值为 2^{8-1} - 1 = 127,IEEE754 约定阶码在添加时需要加上对应的偏移量,所以出现了公式最后的表述: E-127,同理 64 位需要增加偏移量 1023。
这边的解释看着不太好理解,一会通过示例可以轻松搞定~
3.表达形式
针对 IEEE754 的上述二进制表达方式,其表述的数字主要分为3个类型:
A.规约形式
当阶码 E 的二进制值不全为 0 或者 1 时,所表示的值为规格化的值,或者规约形式的浮点数。
B.非规约形式
当阶码 E 的二进制全为 0,所表示的值为非规格化的值,或者非规约形式的浮点数,此时浮点数的指数 E=1-127 / E=1-1023,有效数字 M 不再加第一位的1,而是还原为 0.xxxx 的形式,从而表示 ±0 或者很接近 0 的小数字。
C.特殊形式
± 无穷:阶码 E 全为1时且有效数字 M 全为0,根据 S 的大小表示正负无穷大
NaN:当 E 全为 1 时,如果有效数字 M 不全为 0,表示这个浮点数不是一个数,即为 NaN
三.Double 转换为 IEEE754
十进制浮点数转化为 IEEE754 对应浮点标准数需要首先将十进制浮点数换算为常规二进制表达形式,然后根据 IEEE754 对应的 value 形式并根据 S+E+M 的顺序转化为 IEEE754 的32位、64位形式。
1.单精度 Float 转换为 IEEE754 (手工版)
给定上一篇文章的示例 Float = 66.59375 ,其二进制对应编码为 1000010.10011,其转换为标准形式为 1.00001010011 * 2^{6} 下面套用公式:
编辑
S: 66.59375 为正数,所以 s=0
M:1 + M = 1.00001010011 所以 1.xxxx 的形式下,M=0.00001010011
E: 2^{E-127} = 2^{6} 推出 E-127 = 6 推出 E=133,133 的二进制形式为 10000101
根据 S + E + M 的形式进行拼接:
IEEE754 66.59375 F = 0 + 10000101 + 00001010011 + 补 0 至 32 位
=> 1000010100001010011000000000000
2.双精度 Double 转换为 IEEE754 (手工版)
依旧使用上面示例 Double = 66.59375 = 1000010.10011 = 1.00001010011 * 2^{6},套用公式:
编辑
S:66.59375 为正数,所以 s=0
M:1 + M = 1.00001010011 所以 1.xxxx 的形式下,M=0.00001010011
E:2^{E-1023} = 2^{6} 推出 E-1023 = 6 推出 E=1029,1029 的二进制形式为 10000000101
根据 S + E + M 的形式进行拼接:
IEEE754 66.59375 D = 0 + 10000000101 + 00001010011 + 补 0 至 64 位
=> 0100000001010000101001100000000000000000000000000000000000000000
3.Float / Double 转换为 IEEE754 (代码版)
代码的实现主要复刻了上述的手动过程,但是代码并未考虑非规约和特殊值的情况,所以只使用一些常见的规约类型浮点数,主要过程分 3 步:
A.根据 num 的正负值判断 s 的值
B.排除第一位1,并根据后续的数字获取有效数字 M
C.通过 e_dec 计算有效数字 E 的原始值,再根据 Float 或者 Double 的公式对 E 增加偏移量获取 真实 E
D. 根据 S + E + M 的顺序并补 0 得到最终的结果,如果不好记可以和 SIM 卡的谐音记在一起
def doubleToIEEE754(num: Double, StringType: String): String = { val binaryString = doubleToBin(num) val s = if (num >= 0) { 0 } else { 1 } val m = binaryString.replace(".", "").slice(1, binaryString.length - 1) val e_dec = binaryString.split("\\.")(0).length - 1 val e = if (StringType.toUpperCase().equals("F")) { // V = (-1)^s *(1+M)* 2^(E-127)(单精度) (e_dec + 127).toBinaryString } else if (StringType.toUpperCase().equals("D")) { // V = (-1)^s *(1+M)* 2^(E-1023)(双精度) (e_dec + 1023).toBinaryString } else { "NULL" } val IEEE754String = if (e != "NULL") { val re = s + e + m val length = if (StringType.equals("D")) 64 else if (StringType.equals("F")) 32 else re.length re + repeatString("0", length - re.length) } else { "" } IEEE754String }
试验一下上面的示例:
val num = 66.59375 println(doubleToIEEE754(num, "D")) println(doubleToIEEE754(num, "F"))
0100000001010000101001100000000000000000000000000000000000000000 01000010100001010011000000000000
4.Float / Double 转换为 IEEE754 (官方 API 版)
java 为 Float 和 Double 提供了转化 IEEE754 的 API:
Float 32 位:
这里要求 num 是 Float 类型
val bitF = java.lang.Integer.toBinaryString(java.lang.Float.floatToRawIntBits(num))
Double 64 位:
这里要求 num 是 Double 类型
val bitD = java.lang.Long.toBinaryString(java.lang.Double.doubleToRawLongBits(num))
不管是手工推导还是代码版本大家都可以和官方 API 得到的结果进行验证,这里需要注意下官方 API 在 num 是正数的情况下得到的结果长度分别为 31 位和 63 位,因为第一位代表正数的 0 自动省略了。
四. IEEE754 转换为 Double
上面介绍了 Double 转换为 IEEE754 的过程,其中需要二进制的数字进行中间的过度,同样 IEEE754 转换为 Double 也需要二进制数字的转化:
A.将 IEEE754 根据 Float / Double 的位数,截出 S + E + M
B.根据 Value 的公式,将 S、E、M 代入公式得到对应的二进制形式
C.将二进制形式的浮点数转化为十进制,完成 double 的转化
def IEEE754ToDouble(binaryString: String, stringType: String): Double = { if (stringType.toUpperCase().equals("F")) { // V = (-1)^s *(1+M)* 2^(E-127)(单精度) val s = binaryString.slice(0, 1) val e = binaryString.slice(1, 9) val m = binaryString.slice(9, binaryString.length) var binFloat = if (e.equals("00000000")) { m } else { "1" + m } val cut = binToInteger(e) - 127 binFloat = binFloat.slice(0, cut+1) + "." + binFloat.slice(cut+1, binFloat.length) val floatNum = binToDouble(binFloat) if (s.equals("0")) { floatNum } else { -1 * floatNum } } else if (stringType.toUpperCase().equals("D")) { // V = (-1)^s *(1+M)* 2^(E-1023)(双精度) val s = binaryString.slice(0, 1) val e = binaryString.slice(1, 12) val m = binaryString.slice(12, binaryString.length) var binDouble = if (e.equals("00000000000")) { m } else { "1" + m } val cut = binToInteger(e) - 1023 binDouble = binDouble.slice(0, cut+1) + "." + binDouble.slice(cut+1, binDouble.length) val doubleNum = binToDouble(binDouble) println(binDouble, doubleNum) if (s.equals("0")) { doubleNum } else { -1 * doubleNum } } else { println("请输入正确的模式!") Double.NaN } }
这里并未考虑非规约和特殊值的情况,并且对 Double 的范围也并不完全支持,只是一个思路的拓展,有兴趣的同学可以深化一下该方法~
五.验证
下面基于上述的互转方法和 API 进行调用和验证:
// 双精度 && 单精度 println(repeatString("=", 50)) val floatBit = java.lang.Integer.toBinaryString(java.lang.Float.floatToRawIntBits(num.toFloat)) val floatBitDiy = doubleToIEEE754(num, "F") val floatNum = IEEE754ToDouble("0" + floatBit, "F") println(s"Num: $num FloatNum: $floatNum 单精度: $floatBit 长度: ${floatBit.length}") println("API:" + floatBitDiy) println("DIY:" + "0" + floatBit) println(repeatString("=", 50)) val doubleBit = java.lang.Long.toBinaryString(java.lang.Double.doubleToRawLongBits(num)) val doubleBitDiy = doubleToIEEE754(num, "D") val doubleNum = IEEE754ToDouble("0" + doubleBit, "D") println(s"Num: $num DoubleNum: $doubleNum 双精度: $doubleBit, 长度: ${doubleBit.length}") println("API:" + doubleBitDiy) println("DIY:" + "0" + doubleBit) println(repeatString("=", 50))
一些常规的规约数的互相转化还是可以和 API 对应的:
编辑
上面常用的 repeatString 函数为:
def repeatString(char: String, n: Int): String = List.fill(n)(char).mkString
其余 BinToInteger 、IntegerToBin,DecimalToBin 以及 BinToDecimal 可以参考之前的文章:
六.总结
上面通过演示和代码对 IEEE754 浮点数标准做了一些基本的分解,主要就是十进制、二进制还有公式的分解与代入,除此之外对非规约值和特殊值并没有深入探讨,后续有机会可以继续深入。