大多数语言都提供从float到double的转换,比如C语言,你可以直接通过一个赋值语句来实现把一个float的数字转成 double。而某些蛋疼的语言里面,对二进制的支持实在是少的可怜,我们还是不得不处理这样蛋疼的问题。
MQL4 这种语言大家可能没有这么听说过,是一种写金融交易策略的语言。我的一个同事在用这种语言写策略的时候,遇到了一个问题,要从网络中接收float的二进制数据,然后进行计算,而这种语言只支持double,没有float的。于是,我这个救火队员上马了。
说句实话,我非常喜欢这样蛋疼的问题。当然,对二进制,底层非常熟悉的人,这基本上不是问题。而我工作了这样多年,说句实话,我还真不知道 float 和 double的内部是什么样的一个结构。于是我查找了很多资料,终于基本上搞懂了浮点数,于是我准备把我所学的写成博客,方便后面的人查看资料。首先就从我解决的这个问题开始吧。
面对一个问题,首先就要从了解“敌人”开始。我首先要知道 float 和 double 是怎么表示一个数字的。有了这些知识,我想就能有办法把一个float转成 double。
幸好有google。我找到了著名的 阮一峰 老师的一篇博客。详细的可以看老师的博客:浮点数的二进制表示
简单的说,一个浮点数,不管是 float 和 double 由三部分组成。
1. 符号
2. 小数点的位置
3. 有效数字
这符合我们的常识认识,一个小数就是由这三部分组成的。小数点的位置,有时候可以用“科学计数法”来表示,而有效数字可以简单的认为是一个整数,根据IEEE 754的标准,在一个 float 4个字节,32个位中,这是三个部分分配如下:
(图片来自 阮一峰 的博客)
符号位是1位,还有 8 位表示 小数点的位置,后面的表示有效数字。为了证明一下,上面结构的数字是 0.15625,我用PHP写可一段脚本来验证了一下:
C:\Users\cykzl>php -a
Interactive mode enabled
<?php
$bin = "00111110001000000000000000000000";
$dec = bindec($bin);
$float = unpack("f", pack("L", $dec));
echo $float[1];
?>
^Z
0.15625
C:\Users\cykzl>
果然没有错。那么这个数字是怎么算出来的呢?
程序员的话,当然用伪代码表示算法比较清晰:
sign = 符号 = value >> 31
e = 指数 = ((value >> 23 ) & 0xFF)
m = 有效数字 = value & 0x007FFFFF
现在指数用 小e 来表示,表示,这还不是最终的指数, 还要做分类讨论, 我把最终的值设为 E 。
同样的有效数字部分也要做处理,我们暂时认为这个部分最终的处理结果为 M
最终一个 float(sign, E, M) 用科学计数法表示出来就是
float(sign, E, M)
IF sign == 1
return - M * pow(2, E)
ELSE
return M * pow(2, E)
现在我们看到,e的范围是:0 - 255,没有办法表示负指数,这在表示一个比较小的数字的时候很有用,其实要表示负数也很简单,
减去一个中间数127,就可以表示 -127 - 128 的范围了, E = e - 127, 不过还有意外情况,下面再讨论。
m有二十三位,能表示的范围是 0 – 8388607 换算成10进制的话,基本上就是7位有效数字。所以,一个单精度浮点数能表示
的有效数是7位。怎么得到这个 M(有效数字)呢?
首先,m部分要变成一个小数,这个很简单,decimals = m / (pow(2, 24)) 这样,decimals 的范围就是(用10进制表示)
0 – 0x007FFFFF / 0x00800000 = 0 - 0.99999988079071044921875.
二进制表示就是 0 – 0.11111111111111111111111 ,
这还没有完,中学里面学的科学计数法都是下面的形式:
2.88 * 10 ^ –2
小数点前面还要一位。这十进制,小数点前一位有10种可能性,可是这二进制只有两种可能性,这样的话,能不能
通过一个规则,不显式表示这一位,这样可以节约一位,这就是IEEE 754 的精华部分。
根据IEEE 754的规则是这样的:
e = 0 时 E = 1 – 127 = -126, M = 0 + decimals = decimals , 我们简单的记为 0.m
e > 0 时 E = e – 127, M = 1 + decimals , 我们简单的记为 1.m
至于e = 0 时,E 为什么不取为 0 –127 而是 1 – 127, 这是为了实现 一个平稳的过渡。简单的说,就是 e = 0 时最大的数,
和 e = 1 时最小的数要非常的接近。
e = 0时 最大的M 可以 0.99999988079071044921875 ,而最小的 e = 1 时,最小的 M = 1,这两个M是连续的(非常接近),必须保证指数是一样的时候,他们才会衔接的很好,这是IEEE 754 用的一点小技巧。
简单的说:
IF (e ==0)
E = –126
M = 0.m
return float(sign, E, M)
ELSE
E = e – 127
M = 1.m
return float(sign, E, M)
理论上这样就能计算了。不过 decimals 这个值的计算是不是有点大动干戈,采用10进制来计算:
比如:m = 10000000000000000000000 (二进制) = 0x00400000 (16进制)
变成小数就是 0x00400000 / 0x00800000 = 0.5
其实,采用二进制小数来计算有些时候更加方便(特别是手工计算的时候)。二进制小数可以直接这样计算
0.10000000000000000000000 = 1 * 2^-1 = 0.5
0.11000000000000000000000 = 1 * 2^-1 + 1 * 2^-2 = 0.5 + 0.25 = 0.75
好,我们就计算一下上图中的那个小数吧
sign = 0
e = 124
m = 010000000000000000000000 (二进制)
0.m = 0.010000000000000000000000(二进制) = 0.25 (十进制的小数)
E = 124 – 127 = –3
M = 1 + 0.m = 1.25
float(0, –3, 1.25) = 1.25 * 2^-3 = 1.25 / 8 = 0.15625
好了,这样就计算出来了。
至于double 规则全部一样,但是精度不一样:
sign 还是一位
e 11 位,能表示 0 - 2047, 中间数是 1023
m 52 位, double 表示10进制的精度可以到达15位有效数字
从float 到 double 的转换:
一个数字,不管是float 还是 double 。肯定都有一样的 sign E M
但是 sign e m 这三个的表示可能有所不同。
可以发现,sign肯定是相同的。
e 和 m 可能不同。
float 的e 我们记为 ef
double 的e 我们记为 ed
这样 ef – 127 = ed – 1023
ed = ef – 127 + 1023
m 从二进制上看更加直观, 他们表示的都是一个二进制的小数,所以,应该完全一致,才能表示出一样的M
利用上面的算法,我表示一下上图中的float 到double
0 01111111100 0100000000000000000000000000000000000000000000000000
如果你熟悉位运算,那么答案就呼之欲出了:
基本思路: 设置符号位,设置新的e,设置有效数字
考虑到一些语言没有 float 也 没有int64 ,完全就用int 来表示这个过程。
int buffer[2];
int sign = value >> 31;
int M = value & 0x007FFFFF;
int e = ((value >> 23 ) & 0xFF) - 127 + 1023;
//小尾的机器
buffer[1] = ((sign & 1) << 31) | ((e & 0x7FF) << 20) | (M >> 3);
buffer[0] = (M & 0x7) << 29;
//大尾的机器
buffer[0] = ((sign & 1) << 31) | ((e & 0x7FF) << 20) | (M >> 3);
buffer[1] = (M & 0x7) << 29;
最后一步是把 buffer, 拷贝到一个 double的空间里面去,你会惊奇的发现,这个double 和 float表示的数字完全一致。
下面是一个MQL4 的完整实现:
double unpack_float(int &pack[], int pos)
{
int value = unpack_int(pack, pos);
int buffer[2];
double d[1];
int sign = value >> 31;
int M = value & 0x007FFFFF;
int e = ((value >> 23 ) & 0xFF) - 127 + 1023;
buffer[1] = ((sign & 1) << 31) | ((e & 0x7FF) << 20) | (M >> 3);
buffer[0] = (M & 0x7) << 29;
memcpy(pd(d), pi(buffer), 8);
return (d[0]);
}
这门蛋疼的语言,没有char[], 所以,你会发现用了一个int[]
unpack_int 是unpack出一个int的表示
pd 取出 double 数组的指针(只有数组可以取地址,所以不得不用了一个 double[1], 来表示一个double)
pi 取出 int 数组的指针