關於浮點數誤差與IEEE-754
在程式語言中,浮點數基本都是用 float 與 double來表示,但都會存在誤差
1 2 3 4 5 6 7 8 9 10 11 12 |
float t1 = 0.69 * 10; cout << setprecision(32) << t1 << endl; if(t1 == 6.9) cout << "相等" << endl; else cout << "不相等" << endl; // output // => 6.900000095367431640625 // => 不相等 |
正常6.9 * 10 應該要等於6.9,但是答案卻是不相等?
這個原因跟浮點數的儲存原理有關,讓我們開始吧!
何謂IEEE-754
自電腦發明以來,曾出現過各種不同的浮點數表示法,但目前最通用的是IEEE二進制運算標準(IEEE Standard for Binary Floating-Point Arithmetic , 簡稱IEEE-754)
在IEEE-754標準中定義了四種浮點數格式,但我只講基本的兩種,分別為單精準度float(32bit)和雙精準度double(64bit)。其中單精準度有24位有效儲存數字,而雙精準則有53位有效數字,相對於十進位來說,分別是7位(224 = 107)和16位(253 = 1016)。
為了方便說明,所以先解釋什麼是正規化
正規化就像是數學中的科學記號,如123456通常會表示成1.23456 x 105,而指數部分也有可能是負的,如 0.123456 就會變成 1.23456 x 10-1。
二進位的正規化 :
這邊以13.125為例,先13轉換為2進制,可得1101,再將0.125轉為2進制
- 0.125 x 2 = 0.25 … 整數為0 -> 0
- 0.25 x 2 =0.5 … 整數為0 -> 0
- 0.5 x 2 = 1 … 整數為1 -> 1
所以13.125 = 1101.001,經過正規化後可得 1.101001 x 23
在IEEE-754中,浮點數通常由三個部分組成 :
- 符號(S) : 用來表示正/負(0/1)。
- 指數(E) : 正規化後的次方數,採用超127格式,即將原本的次方數加上127,因為次方數有可能是負的,加上在電腦中要表示負號時,必須拿一個位元來表示,所以就將-128~+127改為0~255,所以基準點就從0變成127。
- 尾數(M) : 正規化後的小數點。
以下範例皆為單精準度 :
浮點數與10進制的轉換
以剛剛的 13.125 轉浮點數為例 :
- 由於13.125為正,所以符號(S) = 0
- 先將數值轉成二進位並正規化 13.125 = 1101.001 = 1.101001 x 23
- 計算指數(E) = 127 + 3 = 01111111 + 11 = 10000010
- 計算尾數(M) = 101001,因為正規化後一定是1.xxxx,所以不需要儲存個位數
- 將各個數值填入浮點數規格中
S——–E———————M———————–
0 10000010 101001 0000 0000 0000 0000 0
這樣就就完成了10進制轉IEEE-754浮點數
而浮點數轉10進制也是一樣
將剛剛的0 10000010 10100100000000000000000轉10進制 :
- 由於S = 0,所以此數為正
- 中間8位元的超127指數(E)為 100000102,將其還原130 – 127 = 100000102 – 01111112 可得 3 = 112
所以要將尾數乘上23 - 最右邊23個為位元值為101001……,將隱藏的個位數還原,可得1.101001……
- 最後將還原後的尾數乘上指數 1.1010012 x 23,並轉為10進位,即可得到 13.125
所以我們可以知道,以32bit的單精度浮點數來說,可以儲存的最大位數為 尾數 23+隱藏個位數 1 = 24位。
關於浮點數的精度
因為有些10進制小數無法完美的用2進制表示,只能用無限的位數來趨近於10進制小數,當我們以24位數為上限時,在儲存時就會省略一些位數,導致還原時的數字不夠精準。
以0.01為例,將它轉為單純的二進制可得
0.000000 101000111101011100001010 0011110101 ….
正規化後得 1.01000111101011100001010 * 2-7,在將它存入32bit的IEEE754規格中。
但是單精度浮點數最多只能存24位數,代表後面一串位數都必須省略,所以當我們再還原時,就不再是0.01了
而是 0.0099999997764826
在浮點數中,最能逼近1的數為 2-1 x 2-2 x 2-3 x 2-4 + ……
根據等比級數計算,所以單精度浮點數能表達的最大小數就是 :
\(S_{24} = 1 – (\frac{1}{2})^{24}\)= 1 – 0.000000059604…. (誤差)
= 0.99999994039
從上面的計算可以知道,float總共7位數在儲存時不會被誤差影響(含整數部分),這就是為什麼程式中的float的準度為7位,double為15位的原因。
如果想要有更精確的科學運算的話,只能透過陣列來搭建一個小數運算,這時你要多精確都沒有問題,之後有機會再寫一篇關於大數運算的文章。
如有錯誤請指證
感謝網友”Hsu Wei-yuan”指正:
精確位數並不局限於小數後,而是float的整數+小數部分從最前面開始算起,前7位數是準確的。
並且float前後誤差不超過原本數字的 1/(2^24),除了0以外的數字,二進位科學記號的個位數一定是1,也就是一定會變成 (1.xxx * 2^x )
所謂浮點誤差就是尾數的地方裝不下了,以float為例,就是1.xxx後面的第24個x存不起來,這時不管是進位還是捨棄,影響力都有限
尾數被浮點誤差影響最多的情況是
1.000000000000000000000001變成
1.00000000000000000000000或
1.00000000000000000000001
這時前後誤差是原本數字的1/(2^24),其他情況因為尾數本身更大,所以尾數24位以後造成的誤差對整體比例只會更少
從以下範例可以得知精確的數字總共7位數
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <iostream> #include <iomanip> using namespace std; int main() { float t1 = 0.69 * 10; cout << setprecision(32) << t1 << endl; //output => 6.900000095367431640625 float t2 = 0.1234567; cout << setprecision(32) << t2 << endl; //output => 0.12345670163631439208984375 float t3 = 123.4567; cout << setprecision(32) << t3 << endl; //output => 123.45670318603515625 float t4 = 16777216, t5=0.5; float t6 = t4 + t5; cout << setprecision(32) << t6 << endl; //output => 16777216 return 0; } |
參考資源
WIKI IEEE-754 : here
IEEE 浮點運算標準 : here
C 語言取出/設定浮點數正規化欄位 : here
C/C++ 浮點數特殊值 : here
[C&C++] 浮點數精準度 (Floating-Point Precision) : here
IEEE-754 浮點數的表示法 : here
MSDN IEEE 浮點表示 : here
Binary floating point and .NET : here
What Every Computer Scientist Should Know About Floating-Point Arithmetic : here
C语言浮点型数据存储结构 : here