浮點數運算與數值表示

災難性抵消

假設你量得兩個長度為 1.00000 公尺與 0.99998 公尺,各準到小數五位,而你想求兩者之差。答案是 0.00002 公尺——但此刻你那五位可靠的數字已塌縮成僅僅一位,因為所有領先數字都抵消了,只剩不確定的尾巴存活下來。相減兩個幾近相等的數並不「製造」誤差,而是殘酷地「暴露」原本就潛伏在它們末位數字中的誤差。這種有效數字的驟然損失就是災難性抵消(catastrophic cancellation)。

在浮點數中其機制很尖銳。兩個輸入各自已被捨入,各帶約 u 的相對誤差。當你相減在領先位元上相符的數時,結果很小,但其「絕對」誤差不變——所以「相對」誤差會以「輸入大小對微小差值之比」爆炸性放大。相減這一步本身是精確的(由所謂 Sterbenz 引理,兩個相近浮點數之差會被精確計算);損害早在更早就已注定,相減不過將其放大。修正之道幾乎從不是「用更高精度」,而是「重排公式,讓危險的相減根本不發生」。

教科書的解藥是穩定的二次公式。標準根 x = (-b + sqrt(b^2 - 4ac)) / (2a) 在 b > 0 且 4ac 很小時會遭抵消,因為 -b 與 +sqrt(b^2-4ac) 幾近抵消。改為先算「安全」根 q = -(b + sign(b)*sqrt(b^2-4ac))/2,再由 x1*x2 = c/a 取另一根,即第二根為 c/(a*q)。沒有幾近相等的相減,沒有損失的數字。同樣的技巧——乘以共軛、用泰勒展開、或重排各項——可拯救像 0 附近的 (1 - cos x)/x^2 與小 x 時的 1 - sqrt(1+x) 這類表達式。

在雙精度中解 x^2 + 1e8*x + 1 = 0。天真公式給出一根為 (-1e8 + sqrt(1e16 - 4))/2;sqrt(1e16-4) 會捨回幾乎恰為 1e8,所以分子抵消到接近 0,那個微小的根算出來大錯特錯(常為 0)。改用 x_small = c/(a*x_large) = 1/( -1e8 ) 則能以完整準確度復原出正確的 -1e-8。

重排二次公式以避開那個會抵消的相減。

抵消並不「製造」新誤差——相近浮點數之差本身是精確的——它揭露的是輸入中早已存在的誤差。所以增加精度鮮少有用;重排代數以避開那個相減才有用。

又称
loss of significancesubtractive cancellation減法抵消有效位數損失