IEEE浮點數的設計缺陷

在生物化學中,“信息”是研究物質的2個基本視角之一,另外一個是“能量”。因爲信息和能量都是抽象出來的東西,以它們爲視角研究現實世界的成本非常低,比如計算機專業的學生做實驗只需要一臺電腦就可以了(搞深度學習的除外),不像其他理工科類,他們做一次實驗得花好多錢準備設備和材料,實驗做得少就會大大降低學習效率,這樣比起來,我們計算機專業的從業者有明顯的學習優勢。

編碼的2個基本原則

之前一篇關於信息論的文章《序列化的極限》裏好像提過一次相關原則:

今天把這2個原則重新整理一下,原則一叫做“無歧義”,原則二叫做“無冗餘”。

信息論要求編碼值(序列化的二進制值)與實際含義一一對應,才能將信息壓縮至最小,而打破一一對應關係的情況分爲2種:

  • 歧義:同一種編碼有多個不同的含義

  • 冗餘:多種編碼對應同一個含義

用一句話概括,編碼值(0和1的bit流)和實際含義要一一對應,不多不少,才能達到最優編碼。

令人尷尬的IEEE浮點數

最近幫公司開發了一套序列化格式,花了很多時間在“如何存儲小數”這個問題上,好像當年比爾蓋茨和喬布斯也在這個問題上糾結過很久。爲什麼存儲小數這麼難呢?因爲小數和整數不同在於:整數關心的是數值的大小,小數關心的是“精度”,整數的編碼體積和數值大小成比例,但小數的體積只和精度高低成比例。體積、大小、進度的下限都是0,上限無窮。

IEEE浮點數是如何存儲小數的呢?

計算機的發展歷史上,用二進制存儲自然數本是最“自然”的選擇,後來爲了存儲負數,就出現了原碼、2補碼、zigzag等編碼,再後來爲了存儲浮點數,就出現了3個主流的IEEE標準:

  • 半精度half:16bit

  • 單精度single:32bit

  • 雙精度double:64bit

  • ......

其實還有四精度、八精度等喪心病狂無止境的精度類型,以最簡單的半精度(binary16)爲例:

這裏面有符號位(sign),指數位(exponent),有效位(fraction),解碼公式很有趣:


(−1)sign × 2exponent−15 × 1.fraction2


我們來研究一下這個公式。

知道爲什麼後面有效位(significantbits)前面是“1.”開頭而不是“0.”開頭嘛?IEEE浮點數缺省補“1”是爲了滿足之前的原則“無冗餘”:二進制小數部分至少有一個“1”,不可能全是0,否則就是整數了。既然這個第1個“1”是確定存在的,那我們編碼的時候就可以省掉一個“1”,讓解碼器給我們加上就行了。這是一種節省空間,或者“偏移”的技巧:IEEE半精度浮點數顯性存儲10位有效位,但這10位是“面值”,它還要加上一個隱性的“1”纔得到實際值。

精度佔比較高的小數更常見,地位更高

IEEE浮點數還有一個明智的做法,在公式中,爲什麼“1.”要放在fraction之前而不是放在之後呢?如果放在之後就是fraction.1,然後指數位就不用-15了。但顯然並沒有這麼做。科學計數法的習慣並不是解釋IEEE浮點數放棄這種簡單編碼方式的理由,它背後真實的原因是千百年來人類對小數的使用習慣:精度佔比較高的小數總是更常見。

舉個例子,走直覺的話,12345.6和1.23456這2個小數哪個更“好”?我拍個腦袋,感覺後面那個更好,因爲12345.6中整數部分遠遠大於小數部分,這就讓我們開始質疑存儲這個0.6的意義何在,直接近似存12346這樣一個整數不就行了嘛?但1.23456中,精度部分的佔比遠大於整數部分,從統計意義上,存儲1.23456更有意義,存儲12345.6“意義不大”。

可以把這個結論作爲一個公理,由此我們在實數範疇出現了4個“更常見”:

  • 整數                      >     非整數

  • 絕對值小的數        >     絕對值大的數

  • 正數                      >     負數

  • 精度佔比高的數    >     精度佔比低的小數

注:大於號比較的是“常見度”。

可是我最後還是放棄了用IEEE浮點數來編碼我的小數,因爲它有一個致命的缺點:不純粹。不知出於什麼目的,IEEE浮點數保留了NaN和±infinity這兩個在現代編程語言中幾乎毫無意義的常量,而NaN和編碼不是一對一的,也就是說有一堆不同的binary16都表示NaN,這打破了信息論“無冗餘”原則,打破這一原則還有它的±0。

ExponentSignificand = zeroSignificand ≠ zeroEquation
000002zero, −0subnormal numbers(−1)signbit × 2−14 × 0.significantbits2
000012, ..., 111102normalized value(−1)signbit × 2exponent−15 × 1.significantbits2
111112±infinityNaN (quiet, signalling)

這張表格是Wikipedia上的,主要揭露了半精度浮點數的特殊情況:強行“騰出”的NaN、±infinity、-0。

無奈,只得重新設計小數編碼,需要明確的是,浮點數只是小數的編碼之一,而IEEE浮點數又是常規浮點數的一種變體,因爲它還兼容整數。我希望設計一種編碼純小數的無冗餘完美編碼,目前可以想到的做法有三種:

  • 浮點數編碼:存儲【指數,有效部位】

  • 分數編碼:存儲【分子,分母】

  • 小數點分隔式:存儲【整數部分,小數部分】

分數編碼的好處在於能夠精確地編碼任意進制的小數,比如十進制的0.1就能存儲爲【1, 10】。但分數編碼也造成了冗餘,因爲分子和分母各乘以一個數,分數值不變,所以分數編碼被淘汰掉了。

經過漫長的思考,我終於想出了一套基於小數點分隔式的小數編碼,暫時命名爲“精度反轉算法”,名字一聽就很有噱頭,下一章將具體分析精度反轉算法的理論依據。

自上次吐槽Utf8以後又把IEEE浮點數給罵了一遍,真爽。


騰飛大廈

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章