9.6 浮點數誤差

圖 9-2:浮點數的誤差從哪裡來

上一節種下的因,這一節收成。浮點數的誤差來自兩個層面,得分開認識。

誤差一:存進去的那一刻就不準了

尾數只有 52 位元,兩件事註定發生:

(1)多數小數存不準。 0.1 存進去就變 0.10000000000000000555\ldots(上一節示範過)。更驚人的是,誤差會在運算後浮上檯面

#include <iostream>
#include <iomanip>
using namespace std;

int main() {
    double a = 0.1 + 0.2;
    cout << (a == 0.3) << '\n';          // 你以為是 1……
    cout << fixed << setprecision(20);
    cout << a << '\n';                   // 掏出家底看看
    cout << 0.3 << '\n';
    return 0;
}

執行結果:

0
0.30000000000000004441
0.29999999999999998890

0.1 + 0.20.3 各自帶著不同的儲存誤差,加完根本是兩個不同的數——== 比較浮點數,從此列為禁招(正確做法馬上講)。

(2)太大的整數也存不準。 52 位元尾數能精確表示的整數只到 2^{53}(約 9 \times 10^{15});再往上,浮點數的「格子」間距比 1 還大,相鄰整數開始黏在一起:

#include <iostream>
#include <iomanip>
using namespace std;

int main() {
    double x = 9999999999999999.0;       // 16 個 9,超過 2^53 了
    cout << fixed << setprecision(0);
    cout << x << '\n';
    return 0;
}

執行結果:

10000000000000000

169 存進 double,印出來變成 10^{16}——它被「吸」到最近的可表示格子上了。這正是 9.4 那個 1e18 + 1 消失事件的原理,也是 9.10「大整數別碰浮點數」的鐵證。

誤差二:每一步運算都可能再磨掉一點

就算存得準,運算的結果也可能需要捨入。單步誤差極小(10^{-16} 等級),但迴圈會放大它

#include <iostream>
#include <iomanip>
using namespace std;

int main() {
    double a = 1e9;
    for (int i = 0; i < 1000000; i++) {
        a -= 1e-6;                       // 從 10^9 扣一百萬次 10^-6
    }
    cout << fixed << a << '\n';          // 數學上應是 999999999 整
    return 0;
}

執行結果(本站評測環境):

999999999.046326

數學上答案是 10^9 - 1 = 999999999,實際卻多了 0.046——每次相減,10^{-6} 這個本來就存不準的數在 10^9 的大數旁邊又被磨掉一些,一百萬次累積下來就看得見了。而且不同電腦、不同編譯環境跑出來的值可能不一樣——這也是浮點數題目常附誤差容忍的原因。

正確的比較方式:EPS

動手試試看:寫程式驗證 0.5 + 0.25 == 0.75 是否成立,再對照 0.1 + 0.2 == 0.3。回想上一節的動手試試看,解釋為什麼一個成立、一個不成立。