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),例如125.125D=1111101.001B,但問題在於計算機根本就不認識小數點“.”,更不可能認識1111101.001B。那麼計算機是如何處理小數的呢?
歷史上電腦科學家們曾提出過多種解決方案,最終獲得廣泛應用的是 IEEE 754 標準中的方案,目前最新版的標準是 IEEE std 754-2008。該標準提出數字系統中的浮點數是對數學中的實數(小數)的近似(「數學」與「數字系統」,「近似」而非「相等」,請見 Python 中的浮點數 ),同時該標準規定表達浮點數的 0、1 序列被分為三部分(三個域):
以32位單精度浮點數為例(float),其具體的轉換規則是:首先把二進位制小數(補碼)用二進位制科學計數法表示,比如上面給出的例子
對於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。
對於二進位制小數,長度為
- 一位時:
0.1B=1−2−1=.5 - 二位時:
0.11B=1−2−2=0.75 - 四位時:
0.1111B=1−2−4=0.9357
對於32位單精度浮點數而言,最大的非規約數是
由上面的內容可以知道,浮點數能表示的範圍其實是有限的,它只能表示整條數軸中的三部分:
- 某個很大的負數到某個很接近於0的負數、
- 0、
- 某個很接近於0的整數到某個很大的正數。
此外,由數學分析的知識可知實數是“稠密”(dense)的,可以證明在任意兩個不相等的實數之間總有無窮多個兩兩不等的實數;但浮點數不是這樣,浮點數是“稀疏”的,兩個浮點數之間只有有限個浮點數,並且兩個“相鄰”的浮點數之間的距離可能是巨大的,這就會帶來精度方面的一系列問題。
譬如兩個“相鄰”的32位單精度浮點數,它們的符號位和指數位都相同,尾數位的前22位都相同,只有最後一位相差1,那麼這兩個浮點數之間的差值可能是非常驚人的。例如01111110100000000000000000000001和01111110100000000000000000000000(指數部分,253-127=126),在32位單精度情況下,它們是“相鄰”的,但它們之間的差值竟高達1.014*10^31。換句話說,在32位單精度浮點數中,處於這段差值以內的數都無法表示。如果以相對誤差來討論的話,32位單精度浮點數的尾數只有23位,第24位及其後的值會被舍入,可以近似認為其相對誤差為
當然,浮點數位數越多,其相對誤差也就越小,只要它的精度滿足程式執行需要就可放心使用。但無論如何,浮點數終究只是實數的粗糙近似,浮點數不可能完全刻畫實數,因為浮點數的位數終究是有限的,換句話說它所能表示的總是有限個有理數,而根據數學分析的知識,在實數軸中雖然無理數和有理數都是無限多的,但無理數集是不可數的,而有理數集卻是可數的。
除了上面的內容以外,在程式設計中需要特別注意的有兩點:
一、浮點數都是帶符號的,不存在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