浮點數系列之:把 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 數組的指針


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM