IEEE754標準: 三, 為什么說32位浮點數(shù)的精度是"7位有效數(shù)"

?? 本章包含一些自己的理解, 如有偏差還望指出.? 本章也是整個系列最重要的一章, 請耐心閱讀.

關(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(二進制)

...

這樣一步一步的往下走, 2^{23} -1步之后, 我們將指向這個數(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ù)的求值公式是: 尾數(shù) * 2 ^ {指數(shù)}(先不考慮符號位)

這樣的話: 假如說起點對應(yīng)的值是 1.0 * 2^{-8}, 那終點對應(yīng)的值就應(yīng)該是1.0 * 2^{-7}, 僅僅是指數(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è)了起點和終點的值分別是1.0 * 2^{-8}1.0 * 2^{-7}, 然后算出了一個假設(shè)的間隔. 但這個假設(shè)格外重要, 下文我們會繼續(xù)沿用這個假設(shè)進行分析 ?

廢話不多說, 現(xiàn)在我們繼續(xù)前進.

現(xiàn)在起點變成了: 0 00000010 00000000000000000000000

再走2^23步, 來到了: 0 00000011 00000000000000000000000

同樣: 符號位, 尾數(shù)位都沒有變, 指數(shù)位又變大了1

沿用上面的假設(shè), 此時起點對應(yīng)的值是1.0 * 2^{-7}, 則終點對應(yīng)的值應(yīng)該是1.0 * 2^{-6}, 即, 還是指數(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語言中驗證這一特性:

與16777216緊鄰的藍點是16777218, 兩數(shù)差值為2, 32位浮點數(shù)無法表示出16777217


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)變量中實際存儲的, 其實都是藍點值:

看打印出來的東西, 可以發(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位:

試圖精確存儲到小數(shù)點后4位, 卻發(fā)現(xiàn)實際上無法存儲1024.0005, 因為表盤上沒有這個1024.0005xxxxxxxx這個級別的數(shù)

在這個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):

小數(shù)點后一位無法精確存儲, 只能精確存儲到個位

在這個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位:

想精確存儲到小數(shù)點后7位, 卻發(fā)現(xiàn)實際上無法存儲1.0000012, 表盤上沒有這個1.0000012xxxx? 這個級別的數(shù)

在這個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語言中驗證一下:

測試環(huán)境: win10 1909, gcc version 6.3.0 (MinGW.org GCC-6.3.0-1)

可見,? 因為有舍入機制的存在, 一個藍點想移動到下一個藍點: 大體上只需移動間隔的一半多一點即可.

而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是這樣算出來的:

N = log_{10}2^{24} ≈ 7.2

下面是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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市边篮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖示绊,帶你破解...
    沈念sama閱讀 223,002評論 6 519
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異暂论,居然都是意外死亡耻台,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,357評論 3 400
  • 文/潘曉璐 我一進店門空另,熙熙樓的掌柜王于貴愁眉苦臉地迎上來盆耽,“玉大人,你說我怎么就攤上這事扼菠∩阍樱” “怎么了?”我有些...
    開封第一講書人閱讀 169,787評論 0 365
  • 文/不壞的土叔 我叫張陵循榆,是天一觀的道長析恢。 經(jīng)常有香客問我,道長秧饮,這世上最難降的妖魔是什么映挂? 我笑而不...
    開封第一講書人閱讀 60,237評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮盗尸,結(jié)果婚禮上柑船,老公的妹妹穿的比我還像新娘。我一直安慰自己泼各,他們只是感情好鞍时,可當我...
    茶點故事閱讀 69,237評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般逆巍。 火紅的嫁衣襯著肌膚如雪及塘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,821評論 1 314
  • 那天锐极,我揣著相機與錄音笙僚,去河邊找鬼。 笑死灵再,一個胖子當著我的面吹牛味咳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播檬嘀,決...
    沈念sama閱讀 41,236評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼槽驶,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了鸳兽?” 一聲冷哼從身側(cè)響起掂铐,我...
    開封第一講書人閱讀 40,196評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎揍异,沒想到半個月后全陨,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,716評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡衷掷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,794評論 3 343
  • 正文 我和宋清朗相戀三年辱姨,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片戚嗅。...
    茶點故事閱讀 40,928評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡雨涛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出懦胞,到底是詐尸還是另有隱情替久,我是刑警寧澤,帶...
    沈念sama閱讀 36,583評論 5 351
  • 正文 年R本政府宣布躏尉,位于F島的核電站蚯根,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏胀糜。R本人自食惡果不足惜颅拦,卻給世界環(huán)境...
    茶點故事閱讀 42,264評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望教藻。 院中可真熱鬧距帅,春花似錦、人聲如沸怖竭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,755評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽痊臭。三九已至哮肚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間广匙,已是汗流浹背允趟。 一陣腳步聲響...
    開封第一講書人閱讀 33,869評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鸦致,地道東北人潮剪。 一個月前我還...
    沈念sama閱讀 49,378評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像分唾,于是被迫代替她去往敵國和親抗碰。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,937評論 2 361

推薦閱讀更多精彩內(nèi)容