浮點數的加減
1.335.toFixed(2) // '1.33' 這真是一個尷尬的錯誤
情境描述
用 Javascript 來做加減乘除,如果是有小數點的數字的話,跑出來的數字會非常奇特,例如:
const number1 = 5.1;
const number2 = 100.0;
console.log(number1 * number2);
// 509.99999999999994
原因
發現主要問題在於 Javascript 是用二進制來計算數值,之後再轉換成十進制,舉個例子,0.1 + 0.2,在 Javascript 會是這樣子計算的。
// 0.1 和 0.2 都轉化成二進位制後再進行運算
0.00011001100110011001100110011001100110011001100110011010
+
0.0011001100110011001100110011001100110011001100110011010
=
0.0100110011001100110011001100110011001100110011001100111
// 轉成十進位制正好是 0.30000000000000004
精度遺失 (Loss of Precision)
由於位元數有限,浮點數只能提供一定範圍的有效數字(Significant Digits)。
- 單精度 (float): 大約 7 位有效數字。
- 雙精度 (double): 大約 15-16 位有效數字。
超過這個範圍的數字會被丟棄或變得不準確。
大數吃小數 (Absorption / Loss of Significance)
當一個「非常大的數」與一個「非常小的數」進行加減運算時,小的數可能會完全消失。
- 原理:浮點數運算時需要對齊指數(Exponent)。為了讓小數的指數與大數對齊,小數的尾數(Mantissa)會不斷右移,直到被移出有效位數範圍,變成 0。
- 例子: $$1,000,000,000 + 0.000000001 = 1,000,000,000$$ (那個 $0.000000001$ 在運算過程中被忽略了)
嚴重相消 (Catastrophic Cancellation)
當兩個「非常接近」的大數相減時,有效位數會大幅減少,剩下的結果可能只是「雜訊」。
- 例子:$A = 1.23456789$,$B = 1.23456788$ $A - B$ 的結果理應是 $0.00000001$。但在電腦內部,因為之前的捨入誤差,相減後的剩餘部分可能包含巨大的相對誤差。
結合律失效 (Non-Associativity)
在數學上,$(A + B) + C$ 等於 $A + (B + C)$。但在浮點數運算中,這不一定成立。
- 原因:因為「大數吃小數」的問題,運算的順序會影響結果。
- 建議:若要加總一系列數字,最好先將數字從小到大排序,先加總小的數字,最後再加大的數字(Kahan Summation Algorithm)。
note
永遠不要使用 == 來判斷兩個浮點數是否相等。 錯誤寫法:if (a == b) ... 原因:因為微小的捨入誤差,兩個理應相等的數字可能在二進位中差了最後一個 bit。
解決方法
-
外部套件
引用 Math.js 或者 BigDecimal.js 等,均可以順利解決浮點數運算的問題。