double型別中可精確表達的最大正整數

看热闹的咸鱼發表於2024-04-13

之前在專案中,使用 rediszset來實現排行榜,由於 zset中的分數使用了 double型別,而我們排行的數值都是整數,所以引起一個問題:

  • double中,能精確表示的,不會丟失精度的最大正整數是多少呢?

先說結論:是 \({2}^{53}-1\) ,即 9,007,199,254,740,991

1. IEEE 754標準

IEEE 754標準中,規定了浮點數的二進位制科學計數法,一個64位浮點數在記憶體中分為三個部分:

  • 符號位(Sign): 0表示正數,1表示負數
  • 指數位(Exponent): 科學計數法中的指數,採用移位儲存
  • 尾數部分(Mantissa): 有效數字

根據 IEEE754標準,一個 double型別共64位(0~63),其中最高位(63)儲存符號位,指數位共11位(52~62),尾數部分52位(0~51)。

雖然有了 IEEE 754 標準,但是各家在實現上還是有一些區別,尤其是舍入規則上。這導致了跨平臺,尤其是跨 CPU 架構的情況下,執行同一個浮點數計算得到的結果可能不一樣

2. 二進位制的科學計數法

十進位制的科學計數法,數字可以寫成 \({a}\times{10^n}\),例如 2074390000可以寫成 \({2.07439}\times10^9\)

類似的,二進位制也可以這樣表示,例如,1101.101可以寫成 \({1.101101}\times2^3\)

那麼,帶有小數的二進位制,和十進位制之間是怎麼互相轉換的呢?

二進位制轉十進位制比較簡單,例如,1101.101= \({1}\times{2^3} + 1\times{2^2}+0\times{2^1}+1\times{2^0} +1\times{2^{-1}}+ 0\times{2^{-2}}+ 1\times{2^{-3}}\)=13.625

而十進位制轉二進位制,要分為整數部分和小數部分,以上面的13.625為例,分為整數部分13和小數部位0.625

整數部分,透過不停地除以2直到0,取餘數來得到:

小數部分,透過不停地乘以2直到0,取整數部分來得到:

對於整數部分來說,不停地整除2,總是能到達0的,可以完整地轉成二進位制,但對於小數來說,不停地乘2減1,有可能永遠也得不到0,這時候只能儲存一定的精度了。

尾數保留精度時,並不是直接將第53位丟棄。IEEE 754 標準中定義了幾種常見的舍入模式,但標準並沒有具體規定程式語言或編譯器必須採用哪種舍入模式作為預設行為,這也是導致不同平臺或環境下的浮點數運算結果不一致的原因之一。

3. double在記憶體中的表示

根據 IEEE標準和二進位制科學計數法,你可能覺得 double0.3,用二進位制科學計數法寫成 \({1.00110011001}\times2^{-2}\),那在記憶體中應該是這樣的:

但實際上是下面這樣:

  • 首先,這個指數位的-2,和整數在記憶體中的表示不同,並不使用最高位作為符號位,而是使用偏移演算法:儲存的資料=後設資料+1023,所以-2在指數位中儲存的是-2+1023=1021,即01111111101,11位二進位制範圍是02047,所以指數位範圍是-10221023(0和2047被用作特殊值處理,見下面)
  • 其次,有效數字的表示,由於二進位制的科學計數法總是寫成\({1.x}\times2^n\),第一位總是1,所以在記憶體中乾脆不儲存這一位,這樣有效數字可以多表示一位。

4. 非規約形式的浮點數

除了規約浮點數,IEEE754-1985標準採用非規約浮點數,用來解決填補絕對意義下最小規格數與零的距離。
如果浮點數的指數部分的編碼值是0,分數部分非零,那麼這個浮點數將被稱為非規約形式的浮點數。
這種情況下,尾數部分沒有前導的1,即非規約浮點數的尾數小於1且大於0,此時表示的是非常接近0的小數。
除此之外,還有三種特殊值:

  • 如果指數是0並且尾數的小數部分是0,這個數是±0(和符號位相關)
  • 如果指數是2047(全為1)並且尾數的小數部分是0,這個數是±無窮大(同樣和符號位相關)
  • 如果指數是2047(全為1)並且尾數的小數部分不是0,這個數是非數字(NaN)

5. double所能表示的精確的最大正整數

我們將尾數部分全都置1,再加上隱藏的1,可以形成一個53位的二進位制數:11111111111111111111111111111111111111111111111111111
再把指數部分設成52+1023,形成的數,就是 \({2}^{53}-1\) ,這就是double所能表示的精確的最大正整數。

而如果再加1,會怎樣呢?尾數部分為變成0,而指數加1,變成\({2}^{53}\),但這個數已經不精確了,我們用python來測試一下:

>>> a = 9007199254740991.0
>>> a - 1
9007199254740990.0
>>> a + 1
9007199254740992.0
>>> a + 2
9007199254740992.0
>>> a + 3
9007199254740994.0
>>> a - 1 == a
False
>>> a + 1 == a + 2
True

可以看到,這裡無法區分出90071992547409929007199254740993這兩個數,因為它們在記憶體中的表現是一樣的,如下圖所示,最後黑色的0和1是被丟棄的,而只保留了52位尾數之後,兩個數字就變成一樣的,無法精確區分這兩個數字了。

參考資料

  • 維基百科 IEEE 754

相關文章