?? 本章包含一些自己的理解, 如有偏差還望指出.? 本章也是整個系列最重要的一章, 請耐心閱讀.
關(guān)于IEEE754標準中浮點數(shù)的精度是怎么計算的, 網(wǎng)上的資料眾說紛紜, 有些還彼此沖突, 我也看的很頭大...這里僅分享兩種個人覺得比較靠譜的說法.
一. 先說結(jié)論
打開Ieee754的維基百科玉吁,可以看到其中標注著, 單精度浮點數(shù)的精度是"Approximately 7 decimal digits"
有人把這句話翻譯為 "大約7位小數(shù)" , 把"decimal"翻譯成了"小數(shù)".
但個人理解, 這里 "decimal的" 含義應(yīng)該是 "十進制的" , 即32位浮點數(shù)的精度是 "大約7位十進制數(shù)" , 后文會說為什么這樣理解.
二. 在討論之前...
我們先來思考這樣一件事: 現(xiàn)在的計算機能存儲[1,2]之間的所有小數(shù)嗎?
稍想一下就知道: 不可以. 因為計算機的內(nèi)存或硬盤容量是有限的, 而1到2之間小數(shù)的個數(shù)是無限的.
極端一點, 計算機甚至無法存儲1到2之間的某一個小數(shù), 比如對于小數(shù) 1.00000.....一萬億個零.....00001, 恐怕很難用計算機去存儲它...
不過計算機卻能存儲[1, 10000]之間的所有整數(shù). 因為整數(shù)是"離散"的, [1, 10000]之間的整數(shù)只有10000個. 10000種狀態(tài), 很容易就能存儲到計算機中, 而且還能進行運算, 比如計算10000 + 10000, 也只是要求你的計算機能存儲20000種狀態(tài)而已...
這樣來看的話: 計算機可以進行數(shù)學概念中的整數(shù)運算的, 但卻難以進行數(shù)學概念中的小數(shù)運算. 小數(shù)這種"連續(xù)"的東西, 當前的計算機很難應(yīng)對...
事實上, 計算機為了進行小數(shù)運算, 不得不將小數(shù)也當成"離散"的值, 一個一個的, 就像整數(shù)那樣:
↑ 數(shù)學中的整數(shù)是一個一個的, 想象綠色指針必須一次走一格
↑ 數(shù)學中的小數(shù)是連續(xù)的, 想象綠色指針可以無極調(diào)節(jié), 想走到哪兒走到哪兒
↑ 計算機中存儲的小數(shù)是一個一個的, 綠色指針必須一次走一格, 就像整數(shù)那樣
這就引發(fā)了精度的問題, 比如上圖中, 我們無法在計算機中存儲0.3, 因為綠色指針只能一次走一格, 要么在0.234, 要么就到了0.468...
當然, 我們也可以增加計算機存儲小數(shù)的精度, 或者說縮小點與點之間的間隔:
IEEE754中的單精度浮點數(shù)和雙精度浮點數(shù)大體也是如此: 雙精度浮點數(shù)中的藍色小點更密集...
三. 理解角度1: 從"間隔"的角度理解
1. 鋪墊
從"間隔"的角度理解來"精度", 其實是這樣一種思路:
想象一個類似于上圖的圓形表盤, 表盤上有一些藍點作為刻度, 有一個綠色的指針用于指向藍點, 綠色指針只能一次走一格: 即只能從當前藍點移動到下一個藍點. 不能指向兩個藍點之間的位置.
假如表盤上用于表示刻度的藍點如下所示:
0.0000
0.0012
0.0024
0.0036
0.0048
0.0060
0.0072
0.0084
0.0096
0.0108
0.0120 (注意這里, 前一個數(shù)是108, 這個數(shù)是120, 先記住這一點)
0.0132
0.0144
...
即, 這是一組十進制數(shù), 這組數(shù)以 0.0012 的步長逐漸遞增... 假設(shè)這個表盤就就是你的計算機所能表示的所有小數(shù).
問: 我們能說這個表盤, 或者說這組數(shù)的精度達到了 4 位十進制數(shù)嗎(比如, 可以精確到1位整數(shù) + 3位小數(shù))?
分析: 如果說可以精確點1位整數(shù) + 3位小數(shù), 那我們就應(yīng)該可以說出下面這樣的話:
我們可以說, 當前指針正位于0.001x: 而指針確實可以位于0.0012, 屬于0.001x (x表示這一位是任意數(shù), 或說這對該位的精度不做限制)
我們可以說, 當前指針位于0.002x: 而指針確實可以位于0.0024, 屬于0.002x
我們可以說, 當前指針位于0.003x: 而指針確實可以位于0.0036, 屬于0.003x
...
我們可以說, 當前指針位于0.009x: 而指針確實可以位于0.0096, 屬于0.009x
我們可以說, 當前指針位于0.010x: 而指針確實可以位于0.0108, 屬于0.010x
我們可以說: 當前指針位于0.011x...但, 注意, 指針始終無法指向0.011x...在我們的表盤中, 指針可以指向0.0108, 或指向0.0120, 但始終無法指向0.011x
...
這就意味著: 對于當前表盤 (或者說對于這組數(shù)) 來說, 4位精度太高了...4位精度所能描述的狀態(tài)中, 有一些是無法用這個表盤表示的.
那, 把精度降低一些.
我們能說這個表盤, 或者說這組數(shù)的精度達到了 3 位十進制數(shù)嗎(比如, 可以精確到1位整數(shù) + 2位小數(shù))?
再來分析一下: 如果說可以精確點1位整數(shù) + 2位小數(shù), 那我們就應(yīng)該可以說出下面這樣的話:
我們可以說, 當前指針位于0.00xx: 而指針確實可以位于0.0012,? 0.0024, 0.0036...0.0098, 這些都屬于0.00xx
我們可以說, 當前指針位于0.01xx: 而指針確實可以位于0.0108, 0.0120...這些都屬于0.01xx
...
可以看出, 對于當前這個表盤 (或者說對于這組數(shù)) 來說, 它完全能"hold住"3位精度. 或者說3位精度所能描述的所有狀態(tài)在該表盤中都可以得到表示.
如果我們的機器使用這個表盤作為浮點數(shù)的取值表盤的話, 那我們就可以說:
我們機器的浮點數(shù)精度 (或者說這個表盤的浮點數(shù)精度), 能精確到3位十進制數(shù)(無法精確到4位十進制數(shù)).
而這個精度, 本質(zhì)上是由表盤間隔決定的, 本例中的表盤間隔是0.0012, 如果把表盤間隔縮小到0.00000012, 那相應(yīng)的表盤能表示的精度就會提升(能提升到 7 位十進制數(shù), 無法達到 8 位十進制數(shù))
通過這個例子, 希望大家能夠直觀的認識到 "表盤的間隔" 和 "表盤的精度" 之間, 存在著密切的關(guān)系. 這將是后文進行討論的基礎(chǔ).
事實上: ieee754標準中的32位浮點數(shù), 也可以被想象為一個 "藍點十分密集的浮點數(shù)表盤", 如果我們能分析出這個表盤中藍點之間的間隔, 那我們就能分析出這個表盤的精度.
注: 也可以用一句很簡單的話來解釋本小節(jié)的例子:? 假設(shè)浮點數(shù)表盤能提供4位精度控制, 比如能控制到1位整數(shù)+3位小數(shù), 這就要求它必須能控制到 0.001 這個粒度, 而 0.001 這個值小于該表盤的實際間隔 0.0012... 所以該表盤不能提供4位精度...
2. 32位浮點數(shù)的間隔
那怎么分析32位浮點數(shù)的間隔與精度呢, 有一個很笨的方法: 把32位浮點數(shù)能表示的所有小數(shù)都羅列出來, 計算間隔. 然后分析精度...
呃...我也確實準備用這個比較笨的方法...下面就開始吧...
注: 此處只分析規(guī)格數(shù)(normal number), 且先不考慮負數(shù)情況, 也就是說不考慮符號位為 1 的情況
32位浮點數(shù)能表示的最小規(guī)格數(shù)是 :
0 00000001 00000000000000000000000 (二進制)
(注意, 規(guī)格數(shù)的指數(shù)位最小為 00000001 , 不能為00000000. 這個在本系列的第二章中已經(jīng)討論過了, 以下不再贅述)
緊鄰的下一個數(shù)是:
0 00000001 00000000000000000000001 (二進制)
緊鄰的下一個數(shù)是:
0 00000001 00000000000000000000010 (二進制)
緊鄰的下一個數(shù)是:
0 00000001 00000000000000000000011(二進制)
...
這樣一步一步的往下走, 步之后, 我們將指向這個數(shù):
0 00000001 11111111111111111111111(二進制)
再走一步, 也就是2^23步之后, 我們將指向這個數(shù):
0 00000010 00000000000000000000000(二進制)
總結(jié)一下: 2^23次移動之后:
我們從起點:? ? 0 00000001 00000000000000000000000,
移動到了終點: 0 00000010 00000000000000000000000
現(xiàn)在可以求間隔了, 間隔? =? 差值 /? 移動次數(shù)? =? (終點對應(yīng)的值 - 起點對應(yīng)的值) / 2^23,
但是, 先別急著計算. 我們先仔細觀察一下, 可以發(fā)現(xiàn), 和起點相比, 終點的符號位和尾數(shù)位都沒變, 僅僅是指數(shù)位變了: 起點指數(shù)位00000001 → 終點指數(shù)位00000010, 終點的指數(shù)位, 比起點的指數(shù)位變大了1
而ieee754中浮點數(shù)的求值公式是: (先不考慮符號位)
這樣的話: 假如說起點對應(yīng)的值是 , 那終點對應(yīng)的值就應(yīng)該是
, 僅僅是指數(shù)位變大了1
把指數(shù)展開會看的更清晰一些:
假如說起點對應(yīng)的值是? ? 0.0000 0001 (8位小數(shù))
那終點對應(yīng)的值就應(yīng)該是 0.0000 001? (7位小數(shù))
那起點和終點的差值就是: (0.0000 001 - 0.0000 0001), 是一個非常小的數(shù)
那間隔就是: 差值 / 2^23
注意: 其實上面我們并沒有計算出真正的間隔, 只是假設(shè)了起點和終點的值分別是和
, 然后算出了一個假設(shè)的間隔. 但這個假設(shè)格外重要, 下文我們會繼續(xù)沿用這個假設(shè)進行分析 ?
廢話不多說, 現(xiàn)在我們繼續(xù)前進.
現(xiàn)在起點變成了: 0 00000010 00000000000000000000000
再走2^23步, 來到了: 0 00000011 00000000000000000000000
同樣: 符號位, 尾數(shù)位都沒有變, 指數(shù)位又變大了1
沿用上面的假設(shè), 此時起點對應(yīng)的值是, 則終點對應(yīng)的值應(yīng)該是
, 即, 還是指數(shù)位變大了1
再次計算差值: 0.0000 01(6位小數(shù)) - 0.0000 001(7位小數(shù))
再次計算間隔: 等于 差值 / 2^23(移動次數(shù))
不知道同學們有沒有體會到不對勁的地方, 沒有的話, 我們計算往前走:
現(xiàn)在起點變成了: 0 00000011 00000000000000000000000
再走2^23步, 來到了: 0 00000100 00000000000000000000000
同理, 終點相對起點, 還只是指數(shù)位變大了1
再次計算差值: (0.00001(5位小數(shù)) - 0.000001(6位小數(shù)))...
再次計算間隔: 等于 差值 / 2^23(移動次數(shù))
感受到不對勁了嗎? 繼續(xù)往前走...
現(xiàn)在起點變成了: 0 00000100 00000000000000000000000
再走2^23步, 來到了: 0 00000101 00000000000000000000000
再次計算差值: (0.0001(4位小數(shù)) - 0.00001(5位小數(shù)))...
再次計算間隔: 等于 差值 / 2^23(移動次數(shù))
...一路走到這兒, 感受到不對勁了嗎?
不對勁的地方在于: 終點和起點的差值! 差值在越變越大! 同理間隔也在越變越大!
不信的話我們來羅列一下之前的差值:
...
那差值就是: 0.0000 001 (7位小數(shù)) - 0.0000 0001(8位小數(shù)), 差值等于0.0000 0009
...
那差值就是: (0.000001 (6位小數(shù)) - 0.0000001(7位小數(shù))), 差值等于0.0000 009
...
那差值就是: (0.00001 (5位小數(shù)) - 0.000001(6位小數(shù))), 等于 0.0000 09
...
那差值就是: (0.0001 (4位小數(shù)) - 0.00001(5位小數(shù))),? 等于 0.0000 9
差值的小數(shù)點在不斷向右移動, 這樣走下次, 總有一天, 差值會變成9, 變成90, 變成90000...
而 移動次數(shù)始終 = 2^23, 間隔始終 = 差值/2^23....差值在越變越大, 間隔也會跟著越變越大...
到這里, 你發(fā)現(xiàn)了ieee754標準的一個重要特性: 如果把ieee754所表示的浮點數(shù)想象成一個表盤的話, 那表盤上的藍點不是均勻分布的, 而是越來間隔越大, 越來越稀疏:
大概就像這樣:
你可以直接在c語言中驗證這一特性:
3. 32位浮點數(shù)的間隔表
開頭我們說過: 知道了表盤的間隔, 就能計算表盤的精度了.
復(fù)雜的地方在于, ieee754這個表盤, 間隔不是固定的, 而是越來越大.
幸運的地方在于, wiki已經(jīng)幫我們總結(jié)好了間隔數(shù)據(jù):
對于這張表的數(shù)據(jù), 我們只關(guān)注右側(cè)的三列即可, 它是在告訴我們: [最小值, 最大值]范圍間的間隔是多少
比如: 下面這一行告訴我們, 8388608 ~ 16777215這個范圍之間的數(shù), 間隔是1
所以32位浮點數(shù)可以存儲8388608, 也可以存儲8388609, 但無法存儲8388608.5, 因為間隔是1
而第二行在說: 1 ~ 1.999999880791這個范圍之間的數(shù), 間隔是: 1.19209e-7
去翻一下c語言float.h的源碼, 會發(fā)現(xiàn)這樣一句:
#define FLT_EPSILON? ? 1.192092896e-07F? ? // smallest such that 1.0+FLT_EPSILON != 1.0
↑ 定義常量FLT_EPSILON, 其值為1.192092896e-07F
這個 1.192092896e-07F , 其實就是我們表格中看到的間隔: 1.19209e-7
源碼中說: 32位浮點數(shù)1.0, 最少也要加上FLT_EPSILON這個常量, 才能不等于1.0.
換句話說, 如果 1.0 加上一個小于 FLT_EPSILON 的數(shù) N, 就會出現(xiàn)1.0 + N == 1.0 這種"詭異的情況".
因為對于 1 ~ 1.999999880791 這個范圍中的32位浮點數(shù), 至少要加上 FLT_EPSILON, 或者說至少要加上該范圍對應(yīng)的間隔, 才能夠把指針從當前藍點, 移動到緊鄰的下一個藍點
注意: 如果不是1 ~ 1.999999880791之間的數(shù), 則不一定要加上 1.19209e-7 啊. 準確來說應(yīng)該是: 某個區(qū)間中的數(shù), 至少要加上該區(qū)間對應(yīng)的間隔, 才能從當前藍點移動到下一個藍點.
仔細看一看一下上面那張間隔表, 相對你對c語言的浮點數(shù)運算會更胸有成竹.
注意: 其實上面的解釋中, 存在著一個不大不小的問題. 不過這里先擱置不談, 等我們理解的更深刻一些時, 再拐回來重新探討這個問題.
注: 64位浮點數(shù)的間隔表, 也可以參見 IEEE754 WIKI
4. 32位浮點數(shù)的精度
那, 為什么說32位浮點數(shù)的精度是7位十進制數(shù)呢?
首先要說明的是: 32位浮點數(shù)的精度是: Approximately 7 decimal digits, 是大約7位十進制數(shù)
事實上, 對于有些8位十進制數(shù), 32位浮點數(shù)容器也能對其精確保存, 比如, 下面兩個數(shù)都能精確保存
那所謂的精度是7位十進制數(shù)到底是什么意思呢? 探討這個之前, 我們需要先了解一些更本質(zhì)的東西
I. 浮點數(shù)只能存儲藍點位置對應(yīng)的值
正如前文所說, 32位浮點數(shù)會形成一個表盤, 表盤上的藍點逐漸稀疏. 綠色指針只能指向某個藍點, 不能指向兩個藍點之間的位置. 或者換句話說: 32位浮點數(shù)只能保存藍點對應(yīng)的值.
如果你要保存的值不是藍點對應(yīng)的值, 就會被自動舍入到離該數(shù)最近的藍點對應(yīng)的值. 舉例:
在0.5 ~ 1這個范圍內(nèi), 間隔約為5.96046e-8, 即約為 0.00000005.96046
也就是說: 表盤上有一個藍點是0.5
下一個藍點應(yīng)該是: 當前藍點 + 間隔 ≈ 0.5 + 0.00000005.96046 ≈ 0.5000000596046
那, 如果我們要保存 0.50000006, 也就是我們要保存的這個值, 稍大于下一個藍點:
因為綠色指針必須指向藍點, 不能指向藍點之間的位置, 所以綠色指針會被"校準"到0.5000000596046, 或者說我們要保存的0.50000006, 會被舍入為0.5000000596046
實測一下:
事實上, 每個32位浮點數(shù)容器中, 存儲的必然是一個藍點值
驗證一下, 首先求出從0.5開始的藍點值:
第一個藍點: 0.5
第二個藍點: ≈ 0.5 + 0.0000000596046 ≈ 0.5000000596046
第三個藍點: 第二個藍點 + 0.0000000596046 ≈ 0.0000001192092
第四個藍點: 第三個藍點 + 0.0000000596046 ≈ 0.0000001788138
然后看下面的代碼, 發(fā)現(xiàn)變量中實際存儲的, 其實都是藍點值:
這是我們需要著重理解的東西
說句題外話, 其實學到現(xiàn)在, 我們就能大體解釋為什么32位浮點數(shù)中的 0.99999999 會被存儲為1.0了, 因為 0.99999999 不是一個藍點值, 且離他最近的藍點值是1.0, 然后綠色指針被自動"校準"到了離他最近的藍點1.0.
II. 理解32位浮點數(shù)的精度是7位十進制數(shù)
對此我是這樣理解的:
例1:
查表, 發(fā)現(xiàn) 1024 ~ 2048 范圍中的 間隔 約為 0.000122070
如下圖: 想要精確存儲到小數(shù)點后4位, 卻發(fā)現(xiàn)做不到, 其實只能精確存儲到小數(shù)點后3位:
在這個1024 ~2048這個范圍內(nèi), 能精確保存的數(shù)是 4位十進制整數(shù) + 3位十進制小數(shù) = 7位十進制數(shù)
例2:
查表, 發(fā)現(xiàn) 8388608 ~ 16777215 范圍中的 間隔 為 1
如下圖: 想要精確存儲到小數(shù)點后一位, 卻發(fā)現(xiàn)做不到, 其實只能精確存儲到小數(shù)點后零位, 或者說只能精確存儲到個位數(shù)(因為最小間隔為1):
在這個8388608 ~ 16777215 這個范圍內(nèi), 能精確保存的數(shù)是 7或8位十進制整數(shù) + 0位十進制小數(shù) = 7或8位十進制數(shù)
是的, 32位浮點數(shù)也能精確保存小于等于 16777215? 的 8 位十進制數(shù), 所以說其精度大約是7位十進制數(shù)
例3:
查表, 發(fā)現(xiàn) 1 ~ 2 范圍中的間隔為 1.19209e-7
如下圖: 想要精確存儲到小數(shù)點后7位, 卻發(fā)現(xiàn)做不到, 其實只能精確存儲到小數(shù)點后6位:
在這個1 ~ 2這個范圍內(nèi), 能精確保存的數(shù)是 1位十進制整數(shù) + 6位十進制小數(shù) = 7位十進制數(shù)
所謂的32位浮點數(shù)的精度是7位十進制數(shù), 大概就是這樣算出來的. 基本上 整數(shù)位 + 小數(shù)位 最多只能有7位, 再多加無法確保精度了 (注意這不是wiki給出的計算方法, wiki給出的算法見下文)
如果你不喜歡這種理解方式, 不妨退一步, 僅記住如下三點即可:
1. 32位浮點數(shù)其實只能存儲對應(yīng)表盤上的藍點值
而不能存儲藍點與藍點之間的值
2. 藍點不是均勻分布的, 而是越來越稀疏. 或者說藍點與藍點之間的間隔越來越大, 或者說精度越來越低.
這也是為什么到1.xxxxxxx時還能精確到小數(shù)點后6位, 到1024.xxx時只能精確到小數(shù)點后3位, 到8388608 時只能精確到個位數(shù)的原因. 因為藍點越來越稀疏了, 再往后連個位數(shù)都精確不到了...
5. 注意事項
I. 區(qū)分32位浮點數(shù)的存儲精度 & 打印效果
在c語言中, 使用 %f 打印時, 默認打印 6 位小數(shù)
32位浮點數(shù)的有效位數(shù)是 7 位有效數(shù)
這兩者并不沖突, 比如:
原始值? ? 1234.878 89
打印效果 1234.878 906
可見打印效果中只有 前7位 和 原始值 是一致的
"原始值" vs "打印出來的值" , 其實就是 "你想要存儲的值" vs "實際存儲的值"
你想要存儲1234.878 89, 但實際存儲的是1234.878 906... 因為1234.878 906...才是個"藍點值", 才能真正的被綠色指針所指向, 才能真正的被32位浮點數(shù)容器所存儲.
雖然不能精確存儲你想要保存的值, 但32位浮點數(shù)能保證精確存儲你想要保存的值的前 7 位. 所以打印效果中的前7位和 原始值 是一致的.
%f 默認打印到6位小數(shù), 打印出來的是實際存儲的藍點值. 但, 藍點值可不一定是7位小數(shù), 可能有十幾位小數(shù), 只是 %f 會默默的將其舍入為7位小數(shù)并打印出來
II. 有時候精度不是7位
可能的原因有很多, 比如:
1. 打印時, %f發(fā)生了舍入:
此時可以設(shè)置打印更多的小數(shù)位
2. 好像精確度不止7位
注意, 浮點數(shù)中能存儲的都其實是藍點值
所以, 如果 你要存儲的值 和 藍點值 完全一樣, 那你要存儲的值是能夠被完全精確的存儲下來的.
如果 你要存儲的值 和 藍點值 非常非常靠近, 就會體現(xiàn)出超乎尋常的精度. 詳見下例:
3. 好像精確度不到7位
舉例:
對此, 個人理解是:
對于 1024 ~ 2048之間的數(shù), 32位浮點數(shù)確實有能力精確到7位有效數(shù)
當你要存儲的值不是一個藍點值時, 會發(fā)生舍入, 自動舍入到離它最近的一個藍點值
所以, 1024.001, 會舍入到離它最近的藍點1024.000976..., 體現(xiàn)的好像精度不足7位
而1024.0011, 就會舍入到離它最近的藍點1024.00109..., 體現(xiàn)的好像精度又足7位了...
只是說: 32位浮點數(shù)確實有精確到7位有效數(shù)的能力, 但舍入規(guī)則使得它有時好像無法精確到7位...
從這個角度去理解的話, 前面我們討論過的一個話題就有點站不住腳了:
前面我們說過: :
?c語言的float.h中有這樣一行代碼:
#define FLT_EPSILON? ? 1.192092896e-07F? ? // smallest such that 1.0+FLT_EPSILON != 1.0
↑ 定義常量FLT_EPSILON,?其值為1.192092896e-07F
這個?1.192092896e-07F?, 其實就是我們1 ~ 2范圍中的間隔:?1.19209e-7
源碼中說: 32位浮點數(shù)1.0,?最少也要加上FLT_EPSILON這個常量, 才能不等于1.0
?換句話說, 如果?1.0?加上一個小于 FLT_EPSILON 的數(shù) N, 就會出現(xiàn)1.0 + N == 1.0?這種"詭異的情況".
等等, 這里好像忽略掉了舍入規(guī)則:?1 ~ 2范圍中, 兩個藍點之間的間隔是:?1.19209e-7, 但這并不意味著想從當前藍點走到下一個藍點需要走滿一個間隔啊, 因為有舍入規(guī)則的存在, 其實你只要走大半個間隔就行了, 然后舍入規(guī)則會自動把你舍入到下一個藍點...
在c語言中驗證一下:
可見,? 因為有舍入機制的存在, 一個藍點想移動到下一個藍點: 大體上只需移動間隔的一半多一點即可.
而c語言中的這行注釋:
?#define FLT_EPSILON? ? 1.192092896e-07F? ? // smallest such that 1.0+FLT_EPSILON != 1.0
其實也不太對,?1.0?也不需要加上?FLT_EPSILON 這一整個間隔才能 != 1.0?(或者說才能到下一個藍點),? 大體上只需加上?FLT_EPSILON的一半多一點?就能 !=1.0 了(或者說就能到下一個藍點了).
不過這也只是個人理解...
III. 0.xxxxxxx 到底能精確到小數(shù)點后幾位
或者說, 32位浮點數(shù)能記錄7位有效數(shù), 那對于 0.xxxxxxx 這種格式的數(shù), 到底是能精確到小數(shù)點后7位, 還是小數(shù)點后6位. 或者說, 此時整數(shù)部分的0算不算有效數(shù)...
個人理解, 對于0.xxxxxxx這也的小數(shù), 其實能精確到小數(shù)點后7位, 即0不算一位有效數(shù)
以0.5 ~ 1這個范圍為例, 此時的間隔是間隔是5.96046e-8, 約等于0.0000 0006
下面嘗試精確到小數(shù)點后8位, 發(fā)現(xiàn)不行.
但精確到小數(shù)點后7位確是綽綽有余:
而0 ~ 0.5之間, 間隔則會更小, 精度則會更高 (因為浮點數(shù)表盤上的藍點是越來越稀疏, 精度越來越差的. 如果靠后的0.5 ~ 1范圍中能精確到小數(shù)點后7位, 那更靠前的0 ~ 0.5中只會更精確, 或者說藍點只會更密集, 間隔只會更小)
舉例:
總之還是那就話: 大體來說, 32位浮點數(shù)的精度是7位有效數(shù).
事實上, 浮點數(shù)中只能存儲藍點, 藍點越靠近0就越密集, 精度就越高. ←7位有效數(shù)是對這一現(xiàn)象的總結(jié)和概括性描述
最后說一點: 有些同學可能會錯誤的認為32位浮點數(shù)類型的精度是: 始終能精確到小數(shù)點后6位, 比如能精確存儲999999.123456, 但不能精確存儲999999.1234567
相信讀到這里, 大家都能找出這種理解的錯誤之處了: 32位浮點數(shù)的精確度是7位有效數(shù), 大體來說, 這7位有效數(shù), 指的是 整數(shù) + 小數(shù)一共7位, 而不是說始終能精確到小數(shù)點后六位...
IV. 深入理解間隔表
我們再回頭看看這張wiki上的間隔表. 其實它主要就是在告訴我們: 某個范圍中, 兩個藍點間的間隔是多少.
比如在1 ~ 2范圍中, 兩個藍點間的間隔約是1.19209e-7
在 8388608 ~ 16777215范圍中, 兩個藍點間的間隔是1
這里其實有幾個注意事項:
1. 每個范圍中, 都有2^32個藍點, 或者每個區(qū)間都被等分為2^23個間隔
比如范圍1~2會被等分為2^23個間隔, 范圍 8388608 ~ 16777215 也會被等分為2^23個間隔
2. 范圍的劃分由指數(shù)決定
所謂的范圍1~2會被等分為2^23個間隔, 準確來說應(yīng)該是范圍2^0 ~ 2^1會被等分為2^23個間隔
所謂的范圍8388608 ~ 16777215會被等分為2^23個間隔, 準確來說應(yīng)該是范圍2^23 ~ 2^24會被等分為2^23個間隔
每次指數(shù)位變更, 都會劃分出新的范圍. 這其實很好理解:
比如, 現(xiàn)在我們位于起點: 0 00000010 00000000000000000000000
往前移動2^23 - 1步, 或者說往前移動2^23 - 1個間隔, 對應(yīng)的其實就是把尾數(shù)從00000000000000000000000, 一步步變成11111111111111111111111
再往前走一步, 也就是共往前移動了2^23個間隔, 我們來到了終點: 0 00000011 00000000000000000000000
可見終點相對起點, 僅指數(shù)位增長了1
終點到起點, 就確定了一段范圍. 該范圍被等分成了2^23個間隔. (終點 - 起點) / 2^23就是每個間隔的長度.
再往前走2^23個間隔, 就來到了0 00000100 00000000000000000000000, 同樣是指數(shù)變大了1...
這就不難看出: 指數(shù)位的變更用于劃分范圍, 尾數(shù)位的變更用于往前一步步移動.
有多少個尾數(shù)位, 決定了每個范圍中可以劃分出多少個間隔, 比如有23個尾數(shù)位, 就意味著每個范圍中可以劃分出2^23個間隔
有多少指數(shù)位, 決定我們可以囊括多少的范圍. 比如有8個指數(shù)位 (可表示的指數(shù)范圍是[-127, 128]), 那我們的范圍劃分就是這樣的:
2^-127 ~ 2^-126是一個范圍
2^-126 ~ 2^-125是一個范圍
...
2^0 ~ 2^1 是一個范圍
2^1 ~ 2^2 是一個范圍
...
2^127 ~ 2^128 是一個范圍
上面每個范圍都會被尾數(shù)位等分為2^23份間隔
增大指數(shù)位不會增大精度: 比如, 如果將指數(shù)位增大到16位(可表示的指數(shù)范圍是[-32767, 32768]), 那我們的范圍劃分是這樣的
2^-32767 ~ 2^-32766是一個范圍
2^-32766 ~ 2^-32765是一個范圍
...
2^-127 ~ 2^-126是一個范圍
2^-126 ~ 2^-125是一個范圍
...
2^0 ~ 2^1 是一個范圍
2^1 ~ 2^2 是一個范圍
...
2^32767 ~ 2^32768是一個范圍
上面每個范圍依舊會被尾數(shù)位等分為2^23份間隔
注意: 2^0 ~ 2^1, 這個范圍還是被等分為2^23份間隔, 2^-126 ~ 2^-125, 這個范圍還是被等分為2^23份間隔...
每個范圍的精度都沒有任何提升.
增大尾數(shù)位才會增大精度: 比如, 將尾數(shù)位增大為48. 則每個范圍會被等分為2^48份間隔. 這樣每個范圍中的間隔才會變小, 藍點才會變密集, 精度才會提升.
總結(jié): 指數(shù)位的多少控制著能囊括多少個范圍, 尾數(shù)位的多少控制著每個范圍的精度, 或者說控制著每個范圍中間隔的大小, 藍點的密度.
希望這能讓你對ieee754標準中的指數(shù)位, 尾數(shù)位的 具體作用, 控制什么 有更好的理解.
四. 理解角度2: WIKI中的計算方法
理解角度2倒是相當簡單.
我們說過, 32位浮點數(shù)在內(nèi)存中是這樣表示的: 1位符號位, 8位指數(shù)位, 23位尾數(shù)位
事實上尾數(shù)位是24位, 因為在尾數(shù)位前還隱藏了一個整數(shù)部分1. 或 0. (可以參見本系列的第一篇文章)
仔細想一下, 浮點數(shù)內(nèi)存的三個部分中:
符號位: 用于控制正負號
指數(shù)位: 控制指數(shù), 其實也就是控制小數(shù)點的移動:
就好像在十進制中:
1.2345e2 = 123.45
1.2345e3 = 1234.5, 指數(shù)位+1只是把小數(shù)點向后移動了一位. 二進制中也是一樣的, 指數(shù)位也僅僅用于控制小數(shù)點的移動. 比如0.01 → 0.001 (小數(shù)點向左移動了一位)
尾數(shù)位: 其實真正控制精度的, 或者說真正記錄狀態(tài)的, 只有尾數(shù)位.
在24位尾數(shù)中
從: 0.0000 0000 0000 0000 0000 000
到: 0.0000 0000 0000 0000 0000 001
...
一直到: 1.1111 1111 1111 1111 1111 111
共包含2^24種狀態(tài), 或者說能精確記錄2^24種不同的狀態(tài):
0.0000 0000 0000 0000 0000 000 是一種狀態(tài),
0.0000 0000 0000 0000 0000 001 又是一種狀態(tài),
1.0010 1100 0100 1000 0000 000 又是另一種狀態(tài)
...
如果你準備記錄2^24 + 1種狀態(tài), 那尾數(shù)就不夠用了. 或者說就不能滿足你對精度的需求了.
在這種視角下: 精度 和 可表示的狀態(tài)數(shù) 之間畫上了等號.
總結(jié)一下: 32位浮點數(shù)一共能記錄2^24種狀態(tài) (符號位用于控制正負, 指數(shù)位用于控制小數(shù)點的位置. 只有尾數(shù)位用于精確記錄狀態(tài))
對于 float f = xxx; 其中xxx是個數(shù)值, 不管xxx你是用什么進制書寫, 只要是使用32位浮點數(shù)作為容器, 就最多只能精確記錄2^24種狀態(tài), 就好像一個32位浮點數(shù)大樓中一共有2^24個房間一樣.
事實上, xxx我們一般用10進制書寫,
而2^24 = 16 777 216(十進制), 即32位浮點數(shù)容器最多只能存儲16 777 216(十進制)種狀態(tài)
16 777 216 是個8位數(shù)
所以32位浮點數(shù)的精度最多是7位十進制(0 - 9 999 999), 共10 000 000種狀態(tài)
如果32位浮點數(shù)的精度是8位十進制的話(0 - 99 999 999), 這一共是100 000 000種狀態(tài), 大于了32位浮點數(shù)能存儲的狀態(tài)上限16 777 216...所以說精度到不了8位十進制數(shù).
到這里就分析完畢了.
如果你更喜歡數(shù)學表達式的話, 那么 "32位浮點數(shù)的精度最多是N位十進制" , N是這樣算出來的:
下面是wiki中對該算法的描述:
The number of decimal digits precision is calculated via number_of_mantissa_bits * Log10(2). Thus ~7.2 and ~15.9 for single and double precision respectively.
如wiki中所說, 32位浮點數(shù)的精度大約是7位十進制數(shù), 64位浮點數(shù)的大約是16位十進制數(shù).
注: 對于這兩種理解角度: 理解角度2更簡單一些, 可以直接用數(shù)學公式計算出精度. 理解角度1(也就是從間隔的角度去理解)的解釋性更強一些, 細節(jié)更豐富, 能解釋的現(xiàn)象也更多一些.
五. 總結(jié)
本章大體總結(jié)了 "32位浮點數(shù)的精度是7位十進制數(shù)" 的兩種計算方法. 關(guān)于這一話題, 網(wǎng)上的資料比較混亂, 所以這里加入了一些自己的理解. 如有錯誤還望指出.
下一章會講解一下IEEE754標準中的一些非規(guī)格數(shù)與特殊數(shù), 包括 ±0, ±INFINITY, 和NaN