為什麼要認真看浮點數#

在大學的多個課程中都會提到浮點數運算,但大多時候都只是草草帶過。為了確保我們能掌握程式可能出現的特例(Exception,例如 0.1 + 0.2 != 0.3),認真研究浮點數運算是必經之路。

讀完本章你會掌握:

  • IEEE-754 規範
  • 規範對工程發展的重要性
  • 浮點數的儲存格式
  • 用演算法改善浮點數運算誤差問題

IEEE-754 概觀#

如果說「方法/工具」是讓人能快速解決問題,那「規則」就是用來統一方法以避免百家爭鳴。電腦科學家在幾十年來嘗試各種在電腦中存放小數的方法,最後由 IEEE-754 拿下勝利,成為被世人廣泛接受的浮點數規範。

也因為浮點數方法本身會帶來捨入誤差,我們經常在寫程式時碰到奇怪的 case,例如:

>>> 0.1 + 0.2
0.30000000000000004

會出現這種問題並不是電腦的錯,而是我們不夠了解計算機的行為。

IEEE 二進位浮點數算術標準(IEEE 754)是 1980 年代以來最廣泛使用的浮點數運算標準,被許多 CPU 與浮點運算器採用。它定義了浮點數的格式(包括負零 -0)、反常值(denormal number)、特殊值(無窮 Inf 與非數值 NaN),以及這些數值的浮點數運算子;它也指明了四種數值修約規則和五種例外狀況(包括例外發生的時機與處理方式)。

單精度浮點數#

IEEE 754 規範了四種浮點數表達方式:

  • 單精確度(single precision)
  • 雙精確度(double precision)
  • 延伸單精確度(extended single)
  • 延伸雙精確度(extended double)

其中,只有單精確度是必備的,其他皆為選擇性實作。四種的剖析方法不會有太大差異,只是更多位元能表達更精確的數值。本章只針對單精度做介紹。

單精度浮點數佔 32 bits,分配如下:

欄位位元數說明
sign1 bit1 表示負數,0 表示正數
exponent8 bits8 bits 共可表示 256 種狀態,為了同時能表達正負指數,IEEE-754 以 127 作為「指數 0」的偏移基準(bias)。例如要表示指數 3 就會存 3 + 127 = 130,二進位是 10000010
fraction23 bits存放小數資料

把實際資料變成浮點數#

以 8.5 為例,要轉成 IEEE 754 單精度。

正規化#

先把 8.5 拆成 8 + 0.5 再轉為 2 進制:

8.5 = 2^3 + 1/2^1
2^3 + 1/2^1 = 1000.1

最後再讓整數部分僅剩個位數 1

1.0001 x 2^3

單精度表示#

1.0001 x 2^3 塞進 32 bits:

  • sign:8.5 是正數,sign bit 為 0
  • exponent:上面得到 exponent 是 3,加上 bias 127 後是 130,二進位 10000010
  • fraction:23 bits 中存放小數部分 .0001,補齊為 00010000000000000000000(IEEE-754 隱含 leading 1 不存)

組起來在記憶體中:

0 10000010 00010000000000000000000

IEEE-754 Floating Point Converter 是一個很棒的小工具,可以做浮點數轉換並查看記憶體模型。

浮點數為什麼叫做浮點數?#

浮點(floating point)是一種對於實數的近似值表現法,由一個有效數字(即尾數,significand)加上冪數(exponent)來表示,通常是乘以某個基數的整數次指數得到。利用浮點進行運算稱為浮點計算,這種運算通常伴隨著因為無法精確表示而進行的近似或捨入。

「浮點」之名來自小數點的位置會隨指數而移動。因為記憶體有限,若要完整存放多位小數的定點數會佔有非常大的記憶體空間,浮點數算是犧牲準確度換來空間使用率的折衷。

浮點數的特例與如何應對#

如何定義一個人很會寫程式?除了常見的設計思維、物件導向 ⋯ 真正的程式高手是能熟知各種情況下的特例並做出良好應對的。並不是每個程式開發者都知道程式語言中的小數問題是浮點數規範造成的,更不用說知道如何應對。

浮點數的特例#

在 IEEE-754 中,為了保留正/負無窮等特殊值,正規化整數的 exponent 範圍是 1 ~ 254(共 254 種狀態),最大、最小極限的 exponent(0 與 255)被保留來表達特例:

特例exponentfraction備註
±000
±∞2550
NaN(Not a Number)255非 0又分 Quiet NaN 與 Signaling NaN 兩類
X111 1111 1AXX XXXX XXXX XXXX XXXX XXXX

可以透過從低位數來的第 23 個位元(上面 A 所在的位置)判斷 NaN 的種類:A = 1 為 Quiet NaN,否則為 Signaling NaN。

兩者的差別參考 Intel manual:

SNaNs are typically used to trap or invoke an exception handler. They must be inserted by software; that is, the processor never generates an SNaN as a result of a floating-point operation.

也就是說:Signaling NaN 不會由處理器運算產生,且對其做運算會發出訊號進入異常處理。相反的,Quiet NaN 經由處理器運算後並不會造成程式終止。

非正規數(Denormal / Subnormal)#

當 exponent 為 0 且 fraction 不為 0 時,該浮點數為非正規數(denormalized number)。它的目的是填補絕對值最小規格數與 0 之間的距離

非正規浮點數源於 1970 年代末 IEEE 浮點數標準化專業技術委員會醞釀浮點數二進位標準時,Intel 對「漸進式下溢位(gradual underflow)」的力薦。當時十分流行的 DEC VAX 機採用「突然式下溢位(abrupt underflow)」。如果沒有漸進式下溢位,那麼 0 與絕對值最小的浮點數之間的距離(gap)將大於相鄰的小浮點數之間的距離。

Kahan Summation:補償累加誤差#

每次浮點運算都會產生一定的捨入誤差。Kahan 教授為了解決誤差累積問題,提出 Kahan Summation,對每次運算做補償以修正這個問題。

準確度與精密度#

學 Kahan Summation 之前,必須先了解 **Precision(精密度)**和 **Accuracy(準確度)**的差別。

以 3.14159 為例(標準取小數點後 6 位):

  • Accurate && Precise:3.14159
  • Accurate && Imprecise:3.14
  • Inaccurate && Precise:3.14128
  • Inaccurate && Imprecise:3.22

使用更多位元表達浮點數是為了取得更高的準確度;Kahan Summation 則是為了修正浮點數運算的精密度誤差。

演算法#

function KahanSum(input)
    var sum = 0.0                    // Prepare the accumulator.
    var c = 0.0                      // A running compensation for lost low-order bits.

    for i = 1 to input.length do
        var y = input[i] - c         // c is zero the first time around.
        var t = sum + y              // Alas, sum is big, y small, so low-order digits of y are lost.
        c = (t - sum) - y            // (t - sum) cancels the high-order part of y;
                                      // subtracting y recovers negative (low part of y)
        sum = t                      // Algebraically, c should always be zero.
                                      // Beware overly-aggressive optimizing compilers!
    next i

    return sum

為什麼有效?#

當有效位數有限,且大數與小數做運算時,會因有效位數不足而導致小數遺失:

10000.0 + 3.14159 = 10003.14159

假設有效位數只有 6,運算結果會變成:

10000.0 + 3.14159 = 10003.1

Kahan Summation 的做法是算出這些遺失的值,等到下一輪運算時再補償回去:

/* 求出誤差值 */
c = (10003.1 - 10000.0) - 3.14159  // -0.04159

/* 補償下一輪輸入 */
y = 3.00001 - (-0.04159)           // 假設下一輪輸入是 3.00001

透過 Kahan Summation,在新一輪輸入與上一輪誤差量級接近時,可以幾乎完全補償浮點數運算造成的精密度下降。

實際上,Kahan Summation 只能盡可能補償誤差。如果新一輪輸入與上一輪誤差的量級差距過大,補償效果就不會這麼顯著。

重點整理#

  • IEEE-754 把「實數」用三段式(sign / exponent / fraction)近似編碼,並以 bias 統一處理正負指數。
  • ±0、±∞、NaN(Quiet / Signaling)、denormal 是必須認得的四類特例。
  • 浮點誤差不是 bug 是規範,知道誤差來源後可以靠 Kahan Summation 等演算法補償。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10266532