1. 程式人生 > >IEEE 754——計算機中浮點數的表示方法

IEEE 754——計算機中浮點數的表示方法

楔子

#include <iostream>
int main(int, char**)
{
    std::cout.precision(20);
    float   a = 123.45678901234567890;
                // warning C4305: “初始化”: 從“double”到“float”截斷
                // 也即賦值號右端是double雙精度型別
                // 賦值號左部是float單精度型別
    double  b = 123.45678901234567890;
    std::cout << a << std
::endl; // 123.456787109375 // 123.45678 7109375 std::cout << b << std::endl; // 123.45678901234568 // 123.4567890123456 8 float c = 9123.45678901234567890; // 9123.45703125 // 9123.45 703125
double d = 9123.45678901234567890; // 9123.4567890123453 // 9123.456789012345 3 return 0; }

侯捷老師曾說“原始碼之下,了無祕密”,今天我說,“原理之下,水落石出”。下面即是 float與double(C語言中最重要的原生資料型別)的最本質最根上的區別。

原理

計算機中是如何儲存和表達數字的?對於整數,情況比較簡單,直接按照數學中的進位制轉換方法處理即可,即連續除以2取餘(比如十進位制的10轉化為二進位制形式,11除以2得5 餘1,5除以2得2 餘1

,2除以2得1 餘0,1除以2得0 餘11011 即是最終的二進位制形式)。這並不是難點,真正的難點在於小數(即浮點數)是如何轉換為二進位制碼的。

當然,從數學的角度來講,十進位制的小數可以轉換為二進位制小數(整數部分連續除2,小數部分連續乘2),例如125.125D=1111101.001B,但問題在於計算機根本就不認識小數點“.”,更不可能認識1111101.001B。那麼計算機是如何處理小數的呢?

歷史上電腦科學家們曾提出過多種解決方案,最終獲得廣泛應用的是 IEEE 754 標準中的方案,目前最新版的標準是 IEEE std 754-2008。該標準提出數字系統中的浮點數是對數學中的實數(小數)的近似(「數學」與「數字系統」,「近似」而非「相等」,請見 Python 中的浮點數 ),同時該標準規定表達浮點數的 0、1 序列被分為三部分(三個域):


這裡寫圖片描述

32位單精度浮點數為例(float),其具體的轉換規則是:首先把二進位制小數(補碼)用二進位制科學計數法表示,比如上面給出的例子 1111101.001=1.111101001×26。符號位sign表示數的正負(0為正,1為負),故此處填0。exponent表示科學計數法的指數部分,請務必注意的是,這裡所填的指數並不是前面算出來的實際指數,而是等於實際指數加上一個數(指數偏移),偏移量為2^(e-1)-1,其中e是exponent的寬度(位數)。對於32位單精度浮點數,exponent寬度為8,因此偏移量為127,所以exponent的值為133,即10000101。之後的fraction表示尾數,即科學計數法中的小數部分11110100100000000000000(共23位)。因此32位浮點數125.125D在計算機中就被表示為01000010111110100100000000000000。

對於32位單精度浮點數(float),sign是1位,exponent是8位(指數偏移量是127),fraction是23位。對於64位雙精度浮點數(double),sign是1為,exponent是11位(指數偏移量是1023),fraction是52位。

需要指出的是125.125D的轉換結果實際上是規約形式的浮點數,即exponent的數值大於0且小於2^e-1,預設科學計數法中整數部分為1,因此尾數只保留了小數部分。但當數值非常接近於0時,可能出現exponent的數值等於0,且科學計數法中整數部分為0的情況,這就稱為非規約形式的浮點數。對此IEEE std 754-2008規定:非規約形式浮點數的exponent值等於同種情況下規約形式浮點數的exponent再加1。比如exponent=1,顯然這是規約形式浮點數,其實際指數應該是-126(1-127);而exponent=0,這是非規約形式浮點數,(若按照規約形式浮點數計算,其實際指數應為-127(0-127))那麼根據前面提到的標準可知這個非規約形式浮點數的實際指數也是-126。所有的非規約浮點數比規約浮點數更接近0。

對於二進位制小數,長度為 k 時的最大值為 12k,好比對於二進位制整數,長度為 k 時的最大值為 2k1,如:

  • 一位時:0.1B=121=.5
  • 二位時:0.11B=122=0.75
  • 四位時:0.1111B=124=0.9357

對於32位單精度浮點數而言,最大的非規約數是 (1223)21261.181038,最小的非規約數是 2232126=21491.401045。對於 64 為雙精度浮點數而言,最大的非規約數是 (1252)210222.2210308,最大的非規約數是 252210224.9410324

由上面的內容可以知道,浮點數能表示的範圍其實是有限的,它只能表示整條數軸中的三部分

  • 某個很大的負數到某個很接近於0的負數、
  • 0、
  • 某個很接近於0的整數到某個很大的正數。

此外,由數學分析的知識可知實數是“稠密”(dense)的,可以證明在任意兩個不相等的實數之間總有無窮多個兩兩不等的實數;但浮點數不是這樣,浮點數是“稀疏”的,兩個浮點數之間只有有限個浮點數,並且兩個“相鄰”的浮點數之間的距離可能是巨大的,這就會帶來精度方面的一系列問題。

譬如兩個“相鄰”的32位單精度浮點數,它們的符號位和指數位都相同,尾數位的前22位都相同,只有最後一位相差1,那麼這兩個浮點數之間的差值可能是非常驚人的。例如01111110100000000000000000000001和01111110100000000000000000000000(指數部分,253-127=126),在32位單精度情況下,它們是“相鄰”的,但它們之間的差值竟高達1.014*10^31。換句話說,在32位單精度浮點數中,處於這段差值以內的數都無法表示。如果以相對誤差來討論的話,32位單精度浮點數的尾數只有23位,第24位及其後的值會被舍入,可以近似認為其相對誤差為 2231.20107。這對於某些需要上億甚至百億次迭代的程式而言是無法接受的。而64位雙精度浮點數的相對誤差可以近似認為是 2522.221016,比32位單精度浮點數的精度高出不少。可見,64位雙精度浮點數不僅表示數的範圍擴大了,而且它所刻畫的浮點數分佈更加“細密”,相對誤差更小。並且,對於64位線寬度的計算機而言,處理64位雙精度浮點數與處理32位雙精度浮點數所需的開銷相同,並不需要額外的迴圈移位,因此還是建議使用64位雙精度浮點數

當然,浮點數位數越多,其相對誤差也就越小,只要它的精度滿足程式執行需要就可放心使用。但無論如何,浮點數終究只是實數的粗糙近似,浮點數不可能完全刻畫實數,因為浮點數的位數終究是有限的,換句話說它所能表示的總是有限個有理數,而根據數學分析的知識,在實數軸中雖然無理數和有理數都是無限多的,但無理數集是不可數的,而有理數集卻是可數的

除了上面的內容以外,在程式設計中需要特別注意的有兩點:
一、浮點數都是帶符號的,不存在unsigned double和unsigned float;
二、兩個浮點數之間不能用==來判斷是否相等,因為浮點數是對實數的近似,所以計算機中兩個浮點數不可能完全相等,最多也只能保證其差值小於使用者規定的誤差限度。(詳細論述及解決方案,請見請見 Python 中的浮點數

模擬

最後提供一段c++程式碼(vs2013),用來進行單精度浮點數和雙精度浮點數與其對應的IEEE 754二進位制位的轉換

std::bitset<32> float2bits(float n)
{
    _ULonglong nMem = *(_ULonglong* )&n;
                    // typedef unsigned long long _ULonglong;
    return std::bitset<32>(nMem);
}
            // float2bits(125.125)
            // 01000010111110100100000000000000
std::bitset<64> double2bits(double n)
{
    _ULonglong nMem = *(_ULonglong* )&n;
    return std::bitset<64>(nMem);
}
            // double2bits(125.125)
            // 0100000001011111010010000000000000000000000000000000000000000000

float bits2float(std::bitset<64> bs)
{
    return *(float* )&bs;
}
            // bits2float(float2bits(125.125))
            // 125.125
double bits2double(std::bitset<64> bs)
{
    return *(double* )&bs;
}
            // bits2double(double2bits(125.125))
            // 125.125

References