• 浮点数系列之:把 float 转成 double


        大多数语言都提供从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 数组的指针
  • 相关阅读:
    Redis12:客户端
    Redis11:事件
    Redis10:RDB持久化与AOF持久化
    Redis09:过期时间与删除策略、redis中的特殊线程
    Redis08:redis中的对象与存储形式
    Redis07:底层:基数树radix tree
    Redis06:底层:跳跃链表skiplist
    C++基础知识:异常处理
    C++基础知识:STL简介
    C++基础知识:泛型编程
  • 原文地址:https://www.cnblogs.com/niniwzw/p/2542944.html
Copyright © 2020-2023  润新知