# 初步解讀浮點數
> 資料整理: [jserv](https://wiki.csie.ncku.edu.tw/User/jserv)
> 續集: [留意浮點數運算的陷阱](https://hackmd.io/@sysprog/floating-point)
## 前言
本文探討:
- 為什麼 `float` 加總會失準
- FP32 (IEEE 754 single precision) 在整數區間的可表示性
- `roundTiesToEven` 造成的誤差型態與週期
- Kahan summation algorithm 的補償機制與限制
## 問題的根源
```c
#include <stdio.h>
int main(void) {
float sum = 0.0f;
for (int i = 0; i < 10000; ++i) sum += i + 1;
printf("Sum: %f\n", sum);
}
```
執行結果是 `Sum: 50002896.000000`,與 $1 + 2 + 3 + \cdots + 10000$ 的精確結果 $50005000$ 相差 $2104$。
並非加法有誤,而是:
- `float` 只能保存有限位元 (FP32 有效精度約 24 個二進位位元,含 hidden bit)
- 並非每個整數都能以 `float` 精確表示
- 每次相加都可能發生捨入 (rounding)
- 誤差在長迴圈中逐步累積
## IEEE 754 單精度浮點數格式
單精度浮點數 (`float`) 以 32 位元儲存,各資料欄位規劃如下:
```
31 30 23 22 0
┌────┬──────────┬──────────────────────────────────┐
│ S │ Exponent │ Mantissa │
└────┴──────────┴──────────────────────────────────┘
1b 8b 23b
```
數值由以下公式決定(適用於 normalized finite values,即 $1 \leq E \leq 254$;$E = 0$ 為 subnormals,$E = 255$ 保留給 infinities 與 NaN):
$$
\text{value} = (-1)^{S} \times 1.\text{mantissa} \times 2^{E - 127}
$$
指數欄位 $E$ 以 127 為 bias 儲存 (stored exponent = actual exponent + 127)。公式中「1.」前的隱含 1 稱為 hidden bit,不佔任何欄位,但使有效數字 (significand) 實際達到 24 位元精度 (1 hidden bit + 23 mantissa bits)。
以整數 $10$ 為例:
$$
(10)_{10} = (1010)_{2} = 1.01_{2} \times 2^{3}
$$
$$
S = 0,\quad E = 3 + 127 = 130 = (10000010)_{2},\quad \text{mantissa} = 01\underbrace{000\cdots0}_{21}
$$
## 整數的精度邊界
並非每個整數都能以單精度浮點數精確表示。24 位元的有效精度意味著,若某整數的二進位表示在最高有效位 (MSB) 與最低有效位 (LSB) 之間相距超過 23 個位元,低位無法完整保留,會依捨入規則近似為鄰近的可表示值。
精度邊界位於 $2^{24} = 16777216$:
```
0 ................................. 2^24-1 2^24 2^24+1
|-----------------------------------|---------|--------|
all representable exactly from here, not every integer is representable
```
在區間 $[2^{24},\ 2^{25})$ 中,FP32 的整數間距 (ULP; units of least precision) 變為 2,只有偶數能精確表示:
```
... 16777216 16777218 16777220 16777222 ...
● ● ● ●
| ✗ | ✗ | ✗ |
16777217 16777219 16777221 16777223 (odd numbers not representable)
```
以下程式展示精度臨界附近的轉換行為:
```c
#include <stdio.h>
int main(void) {
int nums[4] = {0x400001, 0x800001, 0x1000001, 0x8000007};
float f;
for (int i = 0; i < 4; ++i) {
f = nums[i];
printf("nums[%d] = %d, f = %.2f\n", i, nums[i], f);
}
}
```
參考輸出:
```
nums[0] = 4194305, f = 4194305.00 ← MSB-LSB gap = 22, representable exactly
nums[1] = 8388609, f = 8388609.00 ← gap = 23, right at the precision limit
nums[2] = 16777217, f = 16777216.00 ← gap = 24, exceeds limit; trailing 1 lost, rounds down
nums[3] = 134217735, f = 134217728.00 ← gap = 27, exceeds limit; low 4 bits lost, rounds down
```
以 `nums[2]` 和 `nums[3]` 為例,以二進位觀察可直接看出精度損失的原因:
```
0x1000001 = 16777217 = 2^24 + 1:
位元位置: 24 23 22 ··· 2 1 0
二進位值: 1 0 0 ··· 0 0 1
│ └────── mantissa 23b ──┘ ← trailing 1 lost, rounds down
└── hidden bit
stored: 1.00000000000000000000000₂ × 2^24 = 16777216
```
```
0x8000007 = 134217735 = 2^27 + 7:
位元位置: 27 26 25 ··· 4 3 2 1 0
二進位值: 1 0 0 ··· 0 0 1 1 1
│ └──── mantissa 23b ────┘ ←4b→ overflow, rounds down
└── hidden bit
stored: 1.00000000000000000000000₂ × 2^27 = 134217728
```
### 連續整數的精確表示範圍
ULP 隨數值增大而成倍放大,這也是為什麼加總愈來愈大時,後續加入的小量難以發揮作用:
```
interval integer gap (ULP)
[2^23, 2^24) 1
[2^24, 2^25) 2
[2^25, 2^26) 4
[2^26, 2^27) 8
[2^k, 2^(k+1)) 2^(k-23)
```
- $[0,\ 2^{24}]$ 範圍內的所有整數均可精確表示,最多需要 24 個有效位元,恰在精度上限之內。
- $[2^{k},\ 2^{k+1}-1]$ 範圍內,只有 $2^{k-23}$ 的倍數可精確表示。
## roundTiesToEven 捨入規則
IEEE 754 §4.3.1 定義 `roundTiesToEven`:
> The floating-point number nearest to the infinitely precise result shall be delivered; if the two nearest floating-point numbers bracketing an unrepresentable infinitely precise result are equally near, the one with an even least significant digit shall be delivered.
無法精確表示的數值,取最近的可表示值;若兩側距離相等,則取 mantissa LSB 為 `0` 的那一方。此設計使大量運算中的捨入誤差期望值趨近於零。
以 $16777219 = (1000000000000000000000011)_{2}$ 為例:可表示的相鄰值為 $16777218$ 和 $16777220$,距離均為 $1$。$16777218$ 的 mantissa LSB 為 `1`,$16777220$ 的 mantissa LSB 為 `0`,依 `roundTiesToEven` 取 $16777220$。
## 誤差的規律性
在 $[2^{24},\ 2^{25}-1]$ 範圍內,ULP = 2,奇數均被捨入至相鄰偶數,捨入誤差以 $[0, -1, 0, +1]$ 為週期循環:
```
16777216 → 16777216 ( 0)
16777217 → 16777216 ( -1)
16777218 → 16777218 ( 0)
16777219 → 16777220 ( +1) ← roundTiesToEven: equidistant, pick the value with LSB = 0
16777220 → 16777220 ( 0)
16777221 → 16777220 ( -1)
16777222 → 16777222 ( 0)
16777223 → 16777224 ( +1) ← roundTiesToEven: equidistant, pick the value with LSB = 0
```
在 $[2^{25},\ 2^{26}-1]$ 範圍內,ULP = 4,捨入誤差以 $[0, -1, -2, +1, 0, -1, +2, +1]$ 為週期循環:
```
33554432 → 33554432 ( 0)
33554433 → 33554432 ( -1)
33554434 → 33554432 ( -2) ← roundTiesToEven: equidistant, pick the value with LSB = 0
33554435 → 33554436 ( +1)
33554436 → 33554436 ( 0)
33554437 → 33554436 ( -1)
33554438 → 33554440 ( +2) ← roundTiesToEven: equidistant, pick the value with LSB = 0
33554439 → 33554440 ( +1)
33554440 → 33554440 ( 0)
```
## 累加誤差的起始位置
$\sum_{i=1}^{n} i = \frac{n(n+1)}{2}$ 首次超過 $2^{24}$ 時,浮點數加法開始失準。臨界點估算:
$$
\frac{n(n+1)}{2} \leq 2^{24} \implies n \approx \sqrt{2 \times 2^{24}} \approx 5792.6
$$
驗算:
- $n = 5792$:$\dfrac{5792 \times 5793}{2} = 16776528 < 2^{24}$,仍在精確範圍。
- $n = 5793$:精確值 $\dfrac{5793 \times 5794}{2} = 16782321$,浮點數加總實際得到 $16782320$,誤差首次出現。
因此,迴圈執行至第 $5793$ 次加法時開始失準,之後誤差逐步累積,最終造成上述 $2104$ 的偏差。
## Kahan Summation Algorithm
Kahan summation algorithm 的目標不是讓 `float` 變成精確數學,而是把每一步加法中因捨入丟掉的低位誤差暫存起來,在下一輪嘗試補回:
```
每一輪的資料流:
x(本輪加數)
|
v
[x - corr] = y ─────────────────────┐
│
sum ─────────────────────────────> (+) ──→ t(含捨入的實際結果)
↑ │
│ ↓
└────────── corr ←── (t - sum) - y
(實際加入量 - 預期加入量 = 本輪被捨去的低位)
```
```c
float sum = 0.0f;
float corr = 0.0f; /* corrective value for rounding error */
for (int i = 0; i < 10000; ++i) {
float x = (float)(i + 1);
float y = x - corr; /* apply previous correction to current addend */
float t = sum + y; /* low bits may be rounded away */
corr = (t - sum) - y; /* recover lost bits */
sum = t;
}
```
各變數的語意:
* `x` : 本輪的原始加數。
* `y` : 本輪欲加入的值,已扣除上輪被捨去的低位量
* `t` : 加法後的實際結果,若 `sum` 遠大於 `y`,`y` 的低位在相加時可能被捨去
* `corr` : 本輪實際加入量 `(t - sum)` 與預期加入量 `y` 的差,即本輪被捨去的低位部分,用於下輪修正,也就是說,將上一輪加法的捨入誤差補回到本輪加數中,這是補償 (compensation)
corr = (t - sum) - y 記錄的是「上一輪實際加進去的量」和「原本預期加的量」之差 (有正有負),`corr` 的計算式寫為 `(t - sum) - y` 而非 `t - (sum + y)`,原因在於 `sum + y` 本身即觸發 rounding error 的運算,直接計算 `t - (sum + y)` 通常會得到 `0.0`,補償機制完全失效。這與以 `L + (R - L) / 2` 代替 `(L + R) / 2` 迴避整數溢位的技巧性質相同:數學等價的算式,在有限精度運算中可能產生截然不同的結果。
此補償機制在 `|sum| >= |y|` 時能最穩定地捕捉誤差,對應 Fast2Sum 的精確分解條件。在本文的情境中,`sum` 隨迴圈推進逐漸增大,進入失準區間後遠大於每個加數,`(t - sum) - y` 能有效捕捉被捨去的低位;即便初期 `sum` 尚小,Kahan 仍有補償效果,只是提取精度略有差異。`|sum| >= |y|` 是讓補償最穩定的有利條件,而非 Kahan 演算法運作的絕對前提。
## 演算法的限制
### 無法創造不存在的可表示數
若最終結果本身就無法以 `float` 精確表示,Kahan 也無法讓 `sum` 突然變得可表示。例如,將迴圈初始值改為 `float sum = 1.0f`,預期結果 $50005001$ 超出 `float` 的表示能力,無論補償機制多精確,輸出仍為 $50005000$。
### 加數本身已失真時,補償能力下降
Kahan 的補償前提是 `y = x - corr` 本身不發生 rounding error。若加數 `x` 在進入迴圈前就已因精度不足而失真,那些更低位的資訊根本沒有進入運算流程,`corr` 自然也無從補救。
當 `i >= 0x1000000`(約 $1.67 \times 10^7$)時,ULP 已增大到 2 以上,`(i + 1)` 本身不再保證可精確表示,演算法無法保證結果的正確性。使用前必須明確限制輸入範圍。
### 最後一輪 `corr != 0` 時的殘差
若迴圈結束後 `corr` 不為零,該誤差不會自動加回 `sum`。即便在迴圈外補做 `sum -= corr`,這一步仍是浮點運算,若目標值不可表示,最終仍會被捨入到鄰近值。
### 編譯器最佳化可能使補償機制失效
在 C/C++ 中,開啟激進的浮點最佳化選項 (如 GCC/Clang 的 `-ffast-math` 或 `-Ofast`) 時,編譯器假設浮點加法符合結合律,可將 `t = sum + y` 視為精確,進而把整個序列 `corr = (t - sum) - y` 等價展開為 `(sum + y - sum) - y`,直接折疊成 `0.0f`,補償機制完全失效。保護方式是對含有 Kahan 的翻譯單元單獨不加 `-ffast-math` / `-fassociative-math`,而非依賴 `#pragma GCC optimize` 等效果因編譯器版本而異的機制。
## 實務建議
```
需求 建議
─────────────────────────────────────────────
一般統計、資料處理 至少用 double 累加
大量 float 輸入 float 輸入 + double accumulator
高可靠數值計算 Kahan / Neumaier / pairwise summation
整數金額、計數 優先整數或定點
```
Kahan 適合長序列加總、大小量混合但未嚴重超出格式精度極限的場景。若面對極端動態範圍、嚴重消去 (catastrophic cancellation),或需要可重現的高精度科學計算,應考慮更高精度格式或專門的數值方法。
---
## Linux 核心中的補償機制實例
把被截去的微小誤差暫存,再遞延補回並非 Kahan summation 獨有的手法。在 Linux 核心空間 (kernel space) 中,因保存與還原 FPU 暫存器對 context switch 帶來顯著延遲,核心程式碼原則上不使用浮點數。相同的補償思想因此以整數形式出現在多個關鍵子系統中。
### 時鐘子系統的整數補償:`xtime_nsec`
Linux 核心的時間轉換公式為:
```
nanoseconds = (cycles × mult) >> shift
```
右移 `shift` 位等同於整數除法,每次計算都會丟棄餘數。若直接累加:
```c
/* naive approach: right-shift each time; remainder is permanently lost */
system_ns += (delta_cycles * mult) >> shift;
```
在每秒數百萬次時鐘中斷下,這些微小的截斷誤差逐步累積,最終導致可量測的時間漂移。
Linux 核心的對策是在 `struct tk_read_base` 中引入 `xtime_nsec`,以 fixed-point 形式儲存未經右移的完整積,只在需要讀取奈秒數值時才執行位移,低位的次奈秒分數位留在原處繼續參與下一輪累加:
```c
/* kernel/time/timekeeping.c conceptual pseudocode */
tk->tkr_mono.xtime_nsec += delta_cycles * mult; /* accumulate the full fixed-point product, no truncation */
/* shift only when needed: extract whole nanoseconds; sub-ns fraction carries over */
nsec_delta = tk->tkr_mono.xtime_nsec >> shift; /* whole nanoseconds */
tk->tkr_mono.xtime_nsec &= (1ULL << shift) - 1; /* retain the fixed-point fraction */
```
對照 Kahan 演算法的作用:
```
Kahan's corr ↔ low-order bits in xtime_nsec
low bits lost each round ↔ sub-ns fixed-point fraction truncated each shift
carried forward next round ↔ next delta_cycles accumulates into the same field
```
`xtime_nsec` 相當於整數版本的 `corr`,確保即使是次奈秒級的時間碎片,也不會因頻繁中斷而被累計遺失。
### tools/perf 中的 Welford's Online Algorithm
Linux 核心原始程式碼的 `tools/perf` 運行於使用者空間 (user space),可合法使用 `double`。在統計效能事件 (IPC、cache miss 率等) 的變異數時,演算法的選擇直接決定數值計算的可靠性。
#### 基礎公式與其隱患
教科書常見的單遍 (one-pass) 母體變異數計算公式:
$$
\text{Var} = \frac{\sum x_i^2}{N} - \left(\frac{\sum x_i}{N}\right)^2
$$
此式等價於:
$$
N \cdot \text{Var} = \sum x_i^2 - \frac{(\sum x_i)^2}{N}
$$
只需一輪走訪即可同時累積 $\sum x_i$ 與 $\sum x_i^2$,看似高效。問題在於最後一步:當取樣值本身很大、彼此差異卻極小時,$\sum x_i^2$ 與 $(\sum x_i)^2 / N$ 都是天文數字,且極為接近。相減後的差值,亦即我們真正需要的變異數分子遠小於二個運算元本身。一旦超出浮點格式的有效精度,相近的大數相減會使所有有效位被消去,即 catastrophic cancellation (災難性消去)。樣本變異數只需將分母改為 $N - 1$,但二者共用危險的中間值 $\sum x_i^2 - (\sum x_i)^2 / N$,該陷阱同樣無從迴避。
#### 數值危機
取三筆效能計數:$x_1 = 10001$,$x_2 = 10002$,$x_3 = 10003$,均值 $\bar{x} = 10002$(母體變異數 $= 2/3$,樣本變異數 $= 1$)。
基礎公式的關鍵中間計算:
$$
\sum x_i^2 - N\bar{x}^2 = (10001^2 + 10002^2 + 10003^2) - 3 \times 10002^2 = 300120014 - 300120012 = 2
$$
以 `float` (mantissa 23b,ULP 在此量級為 $2^{28-23} = 32$) 計算時:
```
300120014 → 300120000(捨入至最近的 32 倍數,距離 14 < 16)
300120012 → 300120000(同樣被捨入,距離 12 < 16)
相減:300120000 − 300120000 = 0 ← 精確差值 2 被完全吃掉
```
$300120014$ 與 $300120012$ 的差距 2 遠小於此區間的 ULP 32,兩者被捨入至同一個可表示值,相減後精確答案蕩然無存。
`double` 擁有更高精度(有效位約 $10^{15}$),但效能工具量測的 CPU 週期計數動輒達 $10^9$,對數百萬筆取樣累積平方和時,$\sum x_i^2$ 可輕易逼近 $10^{24}$;`double` 在此量級的整數解析度約為 $10^8$,相減結果誤差可能跨越數個量級,最終導致變異數嚴重失真,甚至出現負值。
#### Welford's Online Algorithm:從根本迴避龐大中間值
`tools/perf/util/stat.c` 改用 [Welford's online algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance),只維護二個隨樣本遞增更新的變數:
* `mean` : 走訪到目前為止的執行均值。
* `M2` : 各取樣值對均值的平方偏差累積量;最終樣本變異數 $= M2 / (n - 1)$。
每接收一筆新值 $x$,按以下順序更新:
1. $\delta_1 = x - \text{mean}_\text{old}$(對舊均值的偏差)
2. $\text{mean}_\text{new} = \text{mean}_\text{old} + \delta_1 / n$
3. $\delta_2 = x - \text{mean}_\text{new}$(對新均值的偏差)
4. $M2 \mathrel{+}= \delta_1 \times \delta_2$
$M2$ 每步增加的是二個偏差量的乘積,遠小於 $x^2$,從根本上消除讓中間值膨脹到超出精度範圍的可能。
Linux 原始程式碼:
```c
/* tools/perf/util/stat.c */
void update_stats(struct stats *stats, u64 val)
{
double delta;
stats->n++;
delta = val - stats->mean; /* delta1: deviation from the old mean */
stats->mean += delta / stats->n; /* update the mean */
stats->M2 += delta * (val - stats->mean); /* delta1 * delta2; M2 increment */
}
double variance_stats(struct stats *stats)
{
if (stats->n < 2)
return 0.0;
return stats->M2 / (stats->n - 1); /* sample variance */
}
```
注意第三行:`val - stats->mean` 用的是已更新後的 `mean`($\text{mean}_\text{new}$),計算的是 $\delta_2$,而非重複使用 `delta`($\delta_1$)。順序不可顛倒:必須先更新均值,再用新均值計算第二個偏差,才符合 Welford 公式的數學等價性。
#### 與 Kahan 的結構類比
`delta = val - mean` 類比 Kahan summation 的 `y = x - corr`,都是先求出與已知基準量的差,再把微小量疊加回去:
```
Kahan Welford
────────────────────────────────────────────────────────────────
y = x - corr delta = val - mean_old
t = sum + y mean += delta / n
corr = (t - sum) - y M2 += delta × (val - mean_new)
(corrects truncated bits after the fact) (avoids large intermediate values upfront)
```
Kahan 在加法已完成後回補被截去的低位;Welford 則是從一開始就拒絕讓中間值膨脹。前者是事後補救,後者是事先預防,但主體精神相同:永遠只操作差值,而非直接操作累積的大數。
## 延伸練習
### 練習: 整數補償累加器 (對標 timekeeping)
情境:在無法使用 FPU 的 Linux 核心環境中,以 `u64` 實作一個帶有餘數補償的累加器。
任務:給定陣列 `u32 loads[]` 與縮放因子 `shift = 10`。先演示每次迴圈執行 `sum += loads[i] >> shift` 所造成的截斷誤差,再改寫程式碼,在迴圈內維護 `remainder` 變數,展示重構後如何達成零誤差。
傳統截斷法,每次直接丟棄低 10 位元,誤差逐輪累積:
```c
u64 sum = 0;
for (int i = 0; i < N; ++i)
sum += (u64)loads[i] >> 10; /* discard the low 10 bits */
```
核心補償法,模擬 `xtime_nsec` 的「先累積、再提取」策略:
```c
u64 sum = 0;
u64 remainder = 0; /* Kahan-style corr: carry the bits truncated in the previous iteration */
for (int i = 0; i < N; ++i) {
remainder += (u64)loads[i]; /* accumulate the full raw value */
sum += remainder >> 10; /* extract the integer part ready to carry over */
remainder &= (1ULL << 10) - 1; /* keep the low 10 bits; carry over to the next iteration */
}
```
補償法截斷總量恆為零。
### 練習: 編譯器最佳化對 Kahan 的破壞 (-ffast-math)
情境:驗證 Kahan 演算法在 `-ffast-math` 下的失效機制。
1. 撰寫本文的 Kahan summation 程式,分別以下列選項編譯並比較結果:
```
gcc -O2 kahan.c -o kahan_safe
gcc -O3 -ffast-math kahan.c -o kahan_broken
```
2. 檢視組合語言輸出 (`gcc -S -ffast-math kahan.c`),說明 `-ffast-math` 允許編譯器將浮點數視為精確實數,進而把 `corr = (t - sum) - y` 中的 `t = sum + y` 展開代入,得出 `(sum + y - sum) - y`,經代數相消折疊成 `0.0f`
以下為標準 Kahan 函式的 AArch64 參考輸出 (GCC):
```c
float kahan_sum(const float *a, int n) {
float sum = 0.0f, corr = 0.0f;
for (int i = 0; i < n; ++i) {
float y = a[i] - corr;
float t = sum + y;
corr = (t - sum) - y;
sum = t;
}
return sum;
}
```
`gcc -O2`,嚴格 IEEE 754,`corr` 正常更新:
```c
kahan_loop: ; s0 = sum, s1 = corr
ldr s2, [x0], #4 ; s2 = *a++ (a[i])
fsub s3, s2, s1 ; s3 = a[i] - corr (y)
fadd s4, s0, s3 ; s4 = sum + y (t)
fsub s5, s4, s0 ; s5 = t - sum
fsub s1, s5, s3 ; s1 = (t-sum) - y (new corr)
fmov s0, s4 ; s0 = t (new sum)
subs w1, w1, #1
b.ne kahan_loop
```
`gcc -O3 -ffast-math`,`corr` 可能被代數化簡消去 (實際輸出因 GCC 版本與目標而異):
```asm
kahan_loop: ; s0 = sum, s1 degraded to a load register
ldr s1, [x0], #4 ; s1 = *a++
fadd s0, s0, s1 ; s0 = sum + a[i] (compensation path eliminated)
subs w1, w1, #1
b.ne kahan_loop
```
FP 算術指令從 4 條(3 × `fsub` + 1 × `fadd`)降為 1 條(`fadd`);`s3`、`s5` 及所有 `fsub` 消去,`s1` 不再保存 `corr` 而退化為單純的載入暫存器,補償機制在此版本中失效。
3. 對抗 `-ffast-math` 破壞 Kahan 的可靠做法是對該翻譯單元明確禁用 `-fassociative-math`;`READ_ONCE()` / `WRITE_ONCE()` 則是不同層面的工具:底層為 `volatile` 轉型,主要約束編譯器對記憶體存取的合併、重讀與撕裂行為,無法可靠地阻止代數化簡。查閱 Linux 核心 `Makefile` 中對 `-ffast-math` 的明確排除 (`-fno-fast-math` / `-fno-associative-math`),說明 Linux 核心開發者為何對此施加約束,以及這與 `READ_ONCE()`、`barrier()` 防止編譯器重排記憶體存取的設計哲學有何共通之處——兩者都是「在特定語意邊界上約束編譯器的最佳化假設」,只是目標層次不同。
## 結語
浮點誤差的本質不是真正的錯誤,而是肇因於在有限位元下的捨入。Kahan summation algorithm 的價值,在於每步把被捨掉的低位誤差記錄下來,盡量在後續計算中補回,但它仍受限於浮點格式本身的可表示能力,無法突破格式的精度上限。這個思想並不侷限於浮點數:從 Linux 核心時鐘子系統的 `xtime_nsec` 到 `tools/perf` 的 Welford 演算法,補償被截斷的微小誤差、遞延到後續還清,是驅動當代系統軟體底層演算法設計的反覆出現的主題。
以整數逼近浮點衰減的 EWMA,則以另一種視角體現同樣的工程哲學:在整數的約束下,以合理的近似保留最有價值的資訊。相關設計與 PELT 在排程器中的應用。