1989 - Kahan 的 IEEE 754(浮點數的統一標準)#
案件摘要#
1985 年(標準公布;Kahan 1989 年獲圖靈獎),IEEE 754 浮點數標準誕生—— William Kahan(1933–) 是主要的設計者:
Kahan 的三大貢獻: 1. 統一標準——在此之前,每家電腦的浮點數格式互不相容; 2. 正確的捨入(rounding)——最近偶數捨入(round-to-nearest-even); 3. 特殊值: \(\pm\infty\) 、NaN(非數)、非正規數(subnormal)。 1989 年,Kahan 獲得圖靈獎—— 今天的所有電腦(CPU、GPU、手機)都用 IEEE 754。
前因 -- 為什麼會有這個案子#
- 浮點數的混亂(1950s–1980s): 每家電腦廠商有自己的浮點數格式:
- 同一個程式在不同機器上算出不同結果;
- 有些機器的除法會「吃掉」有效位(Cray 的除法誤差)。
- Kahan 的背景(1960s–1980s): Kahan 是加州大學柏克萊分校的數學教授(數值分析), 他對浮點數的誤差深惡痛絕:
著名案例:1980 年代的愛國者飛彈(1991)因浮點累積誤差攔截失敗(28 人死亡); 1996 年 Ariane 5 火箭因數值溢位爆炸(5 億美元損失)。 - Intel 8087 的機會(1980): Intel 開發 8087 浮點協同處理器(1978–1980), 邀請 Kahan 擔任顧問—— Kahan 把他的「理想浮點數」設計進了 8087, 8087 的設計後來成為 IEEE 754。 - 標準化的過程(1980–1985): - Kahan 與 Intel 的工程師(包括 John Palmer)起草; - 與 DEC 的競爭:DEC 的 VAX 也有提案—— Kahan 的版本在「例外處理」上更完整; - 1985 年:IEEE 754 正式公布。
線索與推理 -- 數學式、程式、理論#
第一條線索:浮點數的格式(IEEE 754)#
32 位元單精度: | 位元 | 31 | 30–23 | 22–0 | |------|----|----|------| | 內容 | 符號 \(s\) | 指數 \(e\) (偏移 127) | 尾數 \(f\) (23 位) |
64 位元雙精度:1 + 11 + 52 位。 特殊值: | 指數 | 尾數 | 意義 | |------|------|------| | 全 0 | 0 | \(\pm 0\) (有符號零!) | | 全 0 | ≠ 0 | 非正規數(subnormal,漸進下溢) | | 全 1 | 0 | \(\pm\infty\) | | 全 1 | ≠ 0 | NaN(Not a Number) |
第二條線索:捨入的數學(Kahan 的關鍵設計)#
最近偶數捨入(round-to-nearest-even): - 正好在中間(如 1.5)時,捨入到偶數(2 而非 1); - 為什麼?無偏性——統計上捨入誤差會互相抵消(不累積偏差)。 例子: - 1.5 → 2(偶數);2.5 → 2(偶數!);3.5 → 4(偶數)。
對照「四捨五入」:永遠進位——統計上偏大(累積偏差)。
第三條線索:浮點數誤差的災難(案例)#
浮點數不能精確表示:
災難案例: | 案例 | 年份 | 誤差 | 後果 | |------|------|------|------| | 愛國者飛彈 | 1991 | 0.1 秒的累積誤差 | 攔截失敗,28 人死亡 | | Ariane 5 火箭 | 1996 | 64 位元轉 16 位元溢位 | 爆炸,5 億美元 | | 溫哥華證交所 | 1982 | 捨入誤差累積 | 指數錯 50%! |
Python:IEEE 754 的數值驗證#
import numpy as np
import struct
# 1) 浮點數不能精確表示(二進位的悲劇)
print("浮點數的表示(二進位):")
print(f" 0.1 + 0.2 = {0.1 + 0.2!r}")
print(f" 0.1 + 0.2 == 0.3?{0.1 + 0.2 == 0.3}")
print(f" 0.1 + 0.2 - 0.3 = {0.1 + 0.2 - 0.3!r}(誤差殘留)\n")
# 2) IEEE 754 的位元結構(32 位元單精度)
def float_bits(x):
"""顯示 IEEE 754 的位元(32 位元)"""
[b] = struct.unpack('>I', struct.pack('>f', x))
sign = (b >> 31) & 1
exp = (b >> 23) & 0xFF
frac = b & 0x7FFFFF
return sign, exp, frac
print("IEEE 754 位元結構(32 位元單精度):")
for x in [1.0, 2.0, 0.5, -1.0, 0.1]:
s, e, f = float_bits(x)
print(f" {x}: 符號 = {s}, 指數 = {e} (127+{e-127}), 尾數 = {f:023b}")
print()
# 3) 特殊值:∞、NaN、非正規數
print("特殊值(Kahan 的設計):")
inf = np.float64('inf'); nan = np.float64('nan')
print(f" 1.0 / 0.0 = {np.float64(1.0) / np.float64(0.0)}(+∞)")
print(f" -1.0 / 0.0 = {np.float64(-1.0) / np.float64(0.0)}(−∞)")
print(f" 0.0 / 0.0 = {np.float64(0.0) / np.float64(0.0)}(NaN)")
print(f" ∞ - ∞ = {inf - inf}(NaN)")
print(f" NaN == NaN?{nan == nan}(NaN 不等於自己!)")
# 非正規數
smallest_normal = np.finfo(np.float64).tiny
smallest_subnormal = np.nextafter(0.0, 1.0)
print(f" 最小正規數 = {smallest_normal!r}")
print(f" 最小非正規數 = {smallest_subnormal!r}(漸進下溢)\n")
# 4) 最近偶數捨入(Kahan 的關鍵設計)
print("最近偶數捨入(round-to-nearest-even):")
# 2.5 應捨入到 2(偶數),不是 3
import decimal
from decimal import Decimal, ROUND_HALF_EVEN
for x in ['1.5', '2.5', '3.5', '0.5']:
d = Decimal(x).quantize(Decimal('1'), rounding=ROUND_HALF_EVEN)
print(f" {x} → {d}(捨入到偶數)")
print(" → 2.5 → 2(不是 3!):統計無偏(誤差不累積)\n")
# 5) 捨入誤差的累積(災難的模擬)
print("捨入誤差的累積(Kahan 總和法 vs 天真總和):")
def naive_sum(arr):
"""天真總和(誤差累積)"""
s = 0.0
for x in arr:
s += x
return s
def kahan_sum(arr):
"""Kahan 總和法(補償求和,誤差極小)"""
s = 0.0
c = 0.0 # 補償項
for x in arr:
y = x - c
t = s + y
c = (t - s) - y # 捕捉丟失的位元
s = t
return s
# 大量小數的總和
n = 10**7
arr = [0.1] * n # 0.1 不能精確表示
naive = naive_sum(arr)
kahan = kahan_sum(arr)
print(f" {n} 個 0.1 的總和:")
print(f" 天真總和 = {naive!r}")
print(f" Kahan 總和 = {kahan!r}")
print(f" 理論值 = {n*0.1!r}")
print(f" 天真誤差 = {abs(naive - n*0.1):.2e}")
print(f" Kahan 誤差 = {abs(kahan - n*0.1):.2e}")
print(" → Kahan 總和法把誤差降到最低(1989 圖靈獎的方法)")
浮點數的表示(二進位):
0.1 + 0.2 = 0.30000000000000004
0.1 + 0.2 == 0.3?False
0.1 + 0.2 - 0.3 = 5.551115123125783e-17(誤差殘留)
IEEE 754 位元結構(32 位元單精度):
1.0: 符號 = 0, 指數 = 127 (127+0), 尾數 = 00000000000000000000000
2.0: 符號 = 0, 指數 = 128 (127+1), 尾數 = 00000000000000000000000
0.5: 符號 = 0, 指數 = 126 (127+-1), 尾數 = 00000000000000000000000
-1.0: 符號 = 1, 指數 = 127 (127+0), 尾數 = 00000000000000000000000
0.1: 符號 = 0, 指數 = 123 (127+-4), 尾數 = 10011001100110011001101
特殊值(Kahan 的設計):
1.0 / 0.0 = inf(+∞)
-1.0 / 0.0 = -inf(−∞)
0.0 / 0.0 = nan(NaN)
∞ - ∞ = nan(NaN)
NaN == NaN?False(NaN 不等於自己!)
最小正規數 = 2.2250738585072014e-308
最小非正規數 = 5e-324(漸進下溢)
最近偶數捨入(round-to-nearest-even):
1.5 → 2(捨入到偶數)
2.5 → 2(捨入到偶數)
3.5 → 4(捨入到偶數)
0.5 → 0(捨入到偶數)
→ 2.5 → 2(不是 3!):統計無偏(誤差不累積)
捨入誤差的累積(Kahan 總和法 vs 天真總和):
10000000 個 0.1 的總和:
天真總和 = 999999.9998389754
Kahan 總和 = 1000000.0
理論值 = 1000000.0
天真誤差 = 1.61e-04
Kahan 誤差 = 0.00e+00
→ Kahan 總和法把誤差降到最低(1989 圖靈獎的方法)
結案 -- 後果與影響#
- 統一標準(1985):
- 所有 CPU(x86、ARM、RISC-V、PowerPC)用 IEEE 754;
- 所有 GPU(NVIDIA、AMD)用 IEEE 754(部分擴展);
- 所有語言(C、Python、Java、JS)用 IEEE 754。
- 浮點數的「2008 修訂」(IEEE 754-2008):
- 半精度(16 位元):AI 訓練的加速(Tensor Core 用半精度);
- 四精度(128 位元):高精度科學計算;
- 十進位浮點(decimal):金融計算(避免二進位的 0.1 問題)。
- Kahan 總和法的應用(1965–):
- NumPy 的 pairwise summation(同思想的變種);
- BLAS 函式庫:矩陣運算的誤差控制;
- 金融:大額資金的計算(十進位 + Kahan)。
- 災難的教訓(1991/1996):
- 愛國者飛彈(1991):0.1 秒的累積誤差 → 28 人死亡;
- Ariane 5(1996):64→16 位元溢位 → 5 億美元;
- 現代:靜態分析工具(Polyspace、Astrée)檢查浮點誤差—— 安全關鍵系統(航空、醫療)的標準流程。
- AI 時代的浮點(2010s–):
- 半精度(FP16)與 bfloat16:深度學習的訓練加速—— Tensor Core 用 FP16/BF16,速度 2–8 倍;
- 混合精度訓練(mixed precision):前向用半精度、累積用全精度—— Kahan 的「誤差控制」思想;
- FP8(2022,NVIDIA):更激進的壓縮。
- Kahan 的其他貢獻(1989 圖靈獎):
- iNaN(indeterminate):不確定值的處理;
- Berkeley 的數值分析課程——Kahan 教了數十年的「浮點數的陷阱」;
- Kahan 的名言: > 「浮點數的算術是『巫術』——除非你懂它的誤差模型。」
- 歷史定位:Kahan 是「浮點數的守護者」——
一句話:沒有 IEEE 754,就沒有可信的科學計算與 AI。
關鍵人物與文獻#
- W. Kahan:IEEE 754 的主要設計者;Kahan 總和法;1989 圖靈獎。
- IEEE:IEEE Standard 754-1985;754-2008(修訂)。
- D. Goldberg:What Every Computer Scientist Should Know About Floating-Point Arithmetic(1991)——浮點數的經典文章。
- J. Palmer:Intel 8087 的工程師——IEEE 754 的推手。
- 相關案件:
1948-香農資訊理論.md、1973-UNIX作業系統.md、2018-深度學習三巨頭.md。