為什么 0.1 + 0.2不等于0.3?浮點數在計算機中是如何存儲的?
為什么 0.1 + 0.2 不等于 0.3?這篇文章,我們將通過這個示例來分析浮點數在計算機中是如何存儲的?
一、定點數
1. 定義
定點數,比較簡單,從字面上理解為小數點固定的數。比如,100,3.14,200.08等等都可以看成是定點數。
通常意義上,定點數表示整數或小數,可以分為以下三種情況:
- 純整數:例如,400,小數點在最后一位,可以忽略
- 純小數:例如,0.68,小數點固定在最高位
- 整數+小數:例如,3.14、9.18,小數點在指定某個位置
接下來,我們一起看看定點數的十進制和二進制的相互轉換。
2. 十進制整數轉二進制
將十進制數轉換為二進制數,通過不斷地除以 2,直到商為 0。步驟:
- 將十進制數除以2,記錄商和余數
- 將商再次除以2,記錄新的商和余數
- 重復這個過程,直到商為 0為止
- 從下往上讀取所有的余數,就得到了轉換后的二進制數
比如:將十進制的 38轉換成二進制:
38 / 2 = 19 余 0
19 / 2 = 9 余 1
9 / 2 = 4 余 1
4 / 2 = 2 余 0
2 / 2 = 1 余 0
1 / 2 = 0 余 1
所以,(38)?? 的二進制是 (100110)?
3. 二進制整數轉十進制
將二進制數轉換為十進制數,根據權值展開法,將每一位上的數字與其對應的權值相乘,然后將所有結果相加。權值是 2的冪,從右向左依次增加。
例如,將二進制數 100110轉換成十進制:
1*2? + 0*2? + 0*23 + 1*22 + 1*21 + 0*2?
= 32 + 0 + 0 + 4 + 2 + 0
= 38
所以,(100110)? 的十進制是 (38)??
4. 十進制小數轉二進制
十進制小數轉二進制分兩部分:整數部分轉換上面已經講解了,小數部分采用“乘2取整,從上到下順序排列”。
例如,十進制小數 10.75 轉為二進制小數:
# 整數部分
10 / 2 = 5 余 0
5 / 2 = 2 余 1
2 / 2 = 1 余 0
1 / 2 = 0 余 1
# 小數部分
0.75 * 2 = 1.5 取整數部分 1
0.5 * 2 = 1.0 取整數部分 1
所以,(10.75)?? 的二進制是 (1010.11)?
5 二進制小數轉十進制
十進制小數轉二進制分兩部分,整數部分轉為二進制上面已經講解了,小數部分采用“乘2取整,從上到下順序排列”。
例如,二進制小數轉十進制小數:
# 整數部分
1*23 + 0*22 + 1*21 + 0*2?
= 8 + 0 + 2 + 0 + 2 + 0 = 10
# 小數部分
1*2?1 + 1*2?2
= 0.5 + 0.25
= 0.75
# 整數部分 + 小數部分
10 + 0.75 = 10.7
所以,(1010.11)? 的十進制是 (10.75)??
6. 定點數的優缺點
(1) 優點
定點數的精度是固定的,可以根據需求進行靈活調整,這使得它們在需要固定精度的應用中非常有用。
(2) 缺點
定點數的精度是固定的,這意味著在處理大范圍或者需要高精度的數據時,可能會丟失精度或者溢出。
比如,以 1個字節(8 bit)為例,假如約定前 4位表示整數部分,后 4位表示小數部分,因此,可以表達的最大數是:(1111.1111)?=(15.9375)??。
如果想要表示更大范圍的值,怎么辦?
- 增加 bit:比如,使用 2個字節(16 bit)、4個字節(32bit),這樣整數部分和小數部分都增加了,表示的數字范圍也變大了。
- 小數點右移:小數點右移,整數范圍就變大了,因此整個數字范圍就變大了,但是小數部分的精度就會越來越低。
因此,對于表示大范圍或者高精度的數,定點數存在其局限性,這時候“浮點數”就派上用場了。
二、IEEE 754
在講解浮點數之前,我們需要先了解一個很重要的標準:IEEE 754,它是 20世紀80年代以來最廣泛使用的浮點數運算標準,為許多 CPU、浮點運算器和編程語言(比如 Java)所采用。
這個標準定義了以下規范:
- 表示浮點數的格式(包括負零-0)與反常值(Denormal number);
- 一些特殊數值(無窮 Inf與非數值 NaN),以及這些數值的浮點數運算;
- 指明了四種數值修約規則和五種例外狀況(包括例外發生的時機與處理方式);
另外,在 IEEE 754標準推出之前,各個計算機公司對于浮點數的表示沒有一個業界通用的標準,這給數據交換、計算機協同工作造成了極大不便,直到 1985年,IEEE 組織推出了 IEEE 754浮點數標準,才結束了這混亂的局面。
三、浮點數
1. 什么是浮點數?
浮點數,是相對定點數而言的,從字面上可以解釋為:小數點在浮動的數。在計算機科學中,浮點數是一種用于表示帶有小數點的實數的數據類型,通常采用了科學計數法表示。
如下例子,十進制小數 300.14,用科學計數法表示,可以有多種方式,整體看上去,小數點好像在浮動。
300.14 = 30.014 × 101
300.14 = 3.0014 × 102
300.14 = 0.31004 × 103
在 IEEE 754標準中,浮點數在內存中以二進制形式存儲,分為四個部分:符號位、基數、指數部分和尾數部分。
- 符號位(Sign bit):用于表示數值的正負。0表示正數,1表示負數。
- 指數部分(Exponent):用于表示數值的大小范圍。該部分存儲的是一個無符號整數,通常采用偏移表示,即用實際指數加上一個固定偏移值。
- 基數(Base):也稱為進制或底數,常見的基數包括十進制(基數為10)、二進制(基數為2)、八進制(基數為8)、十六進制(基數為16)等。
- 尾數部分(Mantissa/Significand):用于表示數值的精度和小數部分。在IEEE 754中,尾數總是處于[1,2)之間的一個小數,這樣可以省略掉小數點前面的 1,從而節省了一個 bit。注意:significand 和 mantissa 都是用來指代浮點數中的尾數部分,只不過 mantissa是早期計算機的叫法。兩個術語可以互換,用來表示浮點數中的尾數部分。
下圖以 300.14為例展示了一個浮點數的構成:
2. 浮點數的精度
在IEEE 754標準中規定了 4種主要的浮點數類型:單精度浮點數(float)、雙精度浮點數(double)、延伸單精確度以及延伸雙精確度。
(1) 單精度浮點數(float)
單精度浮點數占用 32位(4個字節),其中,符號占 1位,指數部分占 8位,尾數部分占 23位。指數部分可以表示的范圍是 0~255,因為存在偏移量,因此指數的實際范圍是從-126~+127,即 2?12?~212?,轉換成十進制,范圍大約為 ±3.4x10?3? 到 ±3.4x103?之間。
(2) 雙精度浮點數(double)
雙精度浮點數占用 64位(8個字節),其中,符號占 1位,指數部分占 11位,尾數部分占 52位。指數部分可以表示的范圍是 0~2047,因為存在偏移量,因此指數的實際范圍是從-1022~+1023,即 2?1?22~ 21?23,轉換成十進制,范圍大約為 ±1.7x10?3?? 到 ±1.7x103??之間。
(3) 延伸單精確度
因為不常用,所以本文不進行講解。
(4) 延伸雙精確度
因為不常用,所以本文不進行講解。
關于單精度浮點數和雙精度浮點數的二進制表示如下圖:
在了解完定點數和浮點數的一些基本概念之后,接下來講解浮點數使用IEEE 754標準在內存是如何使用二進制存儲的, 這里是IEEE 754標準的精華部分,也是比較難理解的部分,我會通過實際的例子結合圖形進行分析。
3. 浮點數的IEEE754轉換
(1) 十進制轉二進制
這里以十進制轉 IEEE754單精度浮點數并且無精度丟失為例,核心流程包含 5個步驟:
① 確認 Sign符號位
比如,19.59375的符號為 0(正數),-1.1的符號為 1(負數),將結果值(0/1)填充到二進制的 Sign bit區域。
② 將十進制轉成純二進制
這個步驟,只是純粹地將十進制轉換成二進制。
- 對于整數部分,通過不斷地除以 2,直到商為 0;
- 對于小數部分,采用逐步乘 2取整,直到小數部分為 0或達到尾數所需的精度;
需要注意,對于小數部分處理結束的條件有 2個:小數部分為0 或 達到尾數所需的精度。只要滿足一個就OK,這里也是為什么小數會丟精度的關鍵所在,在下面的例子會進行講解。
對于沒有精度丟失的轉換,結束的條件是:小數部分為 0。
比如,0.25轉成二進制
0.25 x 2 = 0.5 0
0.5 x 2 = 1.0 1 小數部分為0,結束
③ 標準化以確定尾數和無偏移指數
根據 IEEE 754標準,需要將二進制小數點放在最左邊的 1 之后,比如,100.101需要左移 2位,變成 1.00101x22,無偏移指數是 2;0.0011需要右移 3位,變成 1.1x2?3,無偏移指數是 -3。
這個步驟其實就是 IEEE 754標準的一個硬性規定,解決了上面提到的浮點數漂浮不定的問題。
④ 確認偏移指數
偏移指數,即用無偏移指數(步驟3 產生的結果)加上固定的偏移值127(2?-1=127),再轉換成二進制。
假如,無偏移指數是 4,那么,偏移指數就等于4+127=131,轉換成二進制就是10000011,然后,將二進制結果值10000011填充到 Exponent指數區域。
⑤ 移除尾數的前導 1
在步驟3中,需要將二進制小數點放在最左邊的 1 之后,因此,每個小數點前面的值都是固定的 1(也叫做前導 1),在 IEEE 754標準中,會將這個前導 1移除,從而節省了 1個bit,再將結果值填充到二進制的 Significand尾數區域,不足部分填 0。
比如,1.001110011 移除小數點前固定的 1 變成了001110011,然后將結果值001110011填充到 Significand尾數區域。
為了更好的解釋上面 5個步驟,這里以十進制19.59375轉換成IEEE 754二進制為例進行講解,整個過程如下圖:
(2) 十進制轉二進制
這里以十進制轉 IEEE754單精度浮點數并且有精度丟失為例,核心流程包含 6個步驟:
① 確認 Sign符號位
比如,19.59375的符號為 0(正數),-1.1的符號為 1(負數),將結果值(0/1)填充到二進制的 Sign bit區域。
② 將十進制轉成純二進制
這個步驟,只是純粹地將十進制轉換成二進制。
- 對于整數部分,通過不斷地除以 2,直到商為 0;
- 對于小數部分,采用逐步乘 2取整,直到小數部分為 0或達到所需的精度;
需要注意,對于小數部分處理結束的條件有 2個:小數部分為0 或 達到尾數所需的精度。
對于有精度丟失的轉換,結束的條件是:達到所需的精度。
比如,0.3轉成二進制
0.3 x 2 = 0.6 0
0.6 x 2 = 1.2 1
0.2 x 2 = 0.4 0
0.4 x 2 = 0.8 0
0.8 x 2 = 1.6 1
0.6 x 2 = 1.2 1 開始進入循環,只能達到所需的精度后按需舍入結束
0.2 x 2 = 0.4 0
③ 標準化以確定尾數和無偏移指數
根據 IEEE 754標準,需要將二進制小數點放在最左邊的 1 之后,比如,100.101需要左移 2位,變成 1.00101x22,無偏移指數是 2;0.0011需要右移 3位,變成 1.1x2?3,無偏移指數是 -3。
這個步驟其實就是 IEEE 754標準的一個硬性規定,解決了上面提到的浮點數漂浮不定的問題。
④ 確認偏移指數
偏移指數,即用無偏移指數(步驟3 產生的結果)加上固定的偏移值127(2?-1=127),再轉換成二進制。
假如,無偏移指數是 4,那么,偏移指數就等于4+127=131,轉換成二進制就是10000011,然后,將二進制結果值10000011填充到 Exponent指數區域。
⑤ 移除尾數的前導 1
在步驟3中,需要將二進制小數點放在最左邊的 1 之后,因此,每個小數點前面的值都是固定的 1(也叫做前導 1),在 IEEE 754標準中,會將這個前導 1移除,從而節省了 1bit,再將結果值填充到二進制的 Significand尾數區域,不足部分填0。
比如,1.001110011 移除小數點前固定的 1 變成了001110011,然后將結果值001110011填充到 Significand尾數區域。
⑥ 按需向上或者向下舍入
在步驟2中,小數部分轉換成二進制的時候不是因為乘 2使得小數部分為 0結束,而是因為產生了循環,導致達到了尾數所需的精度(單精度 23bit,雙精度 52bit),對于超出的精度范圍,需要如何處理?
答案:按需向上或者向下舍入
為了更好的解釋上面 6個步驟,這里以十進制-123.3轉換成 IEEE 754二進制為例進行講解,整體流程如下圖:
注意:截圖中步驟6黃色字體1001,是指超出尾數 23bit范圍的二進制數,需要被舍入
通過上面兩個例子的分析,相信大家還是會有困惑:為什么是二進制中存儲的是偏移指數而不是指針?為什么偏移指數是通過指數加上一個固定的偏移值?這個固定的偏移值是怎么計算的?為什么尾數需要把前導 1移除?IEEE 754 的舍入規則是什么?下面我們就一一解答。
(3) 二進制轉十進制
為了幫助大家更好地理解 IEEE 754的轉換,這里還提供了 2個 IEEE 754單精度浮點數二進制轉十進制的逆向例子,如下圖:
(4) 偏移指數
偏移指數,也叫指數偏移值(exponent bias),即浮點數表示法中指數域的編碼值,等于指數的實際值加上某個固定的偏移值,IEEE 754標準規定該固定值偏移為2 ??1-1,其中 e為存儲指數的 bit長度,因此,單精度浮點數的固定偏移值是2??1-1=127,雙精度浮點數的固定偏移值是211?1-1=1023。
(5) 為什么需要偏移指數?
從 1.00101 x 22 和 1.1 x 2?3 可以看出來,指數有正負數的區分,即有符號的區分,因此,IEEE 754標準中的偏移指數主要解決兩個問題:
- 表示負指數:在使用二進制表示浮點數的指數時,如果采用純粹的二進制表示,那么需要額外的符號位來表示指數的正負。采用偏移指數的方式,可以將指數全部看作非負數,因為將偏移量添加到指數部分后,所有的指數都是正數,0則表示了最小的指數。
- 排序浮點數:使用偏移指數的方式可以更容易地對浮點數進行排序。因此第 1點已經把指數全部轉換成了無符號,所以,浮點數的比較直接變成了對二進制的自然排序比較,不需要單獨處理符號位和指數部分的符號。
(6) 固定值偏移值如何計算?
① 單精度浮點數
對于單精度浮點數,它的指數域是 8個bit,表示的有符號范圍是-127~128(-2?-1 ~ 2?),如何讓這個范圍 >=0 ?
答案:加上 127。所以,IEEE 754標準把 127設定為單精度浮點數的固定偏移值。
② 雙精度浮點數
同理,對于雙精度浮點數,它的指數域是 11個bit,表示的有符號范圍是-1023~1024(-21?-1 ~ 21?),如何讓這個范圍 >=0 ?
答案:加上 1023。所以,IEEE 754標準把 1023設定為雙精度浮點數的固定偏移值。
具體信息如下圖:
(7) 為什么要移除尾數的前導 1?
在講解浮點數構成時提過,浮點數的小數點是浮動的,因此,IEEE 754標準定義了一套固定的格式:在二進制數中,通過移位,將小數點前面的值固定為 1,IEEE754 稱這種形式的浮點數為規范化浮點數。
因此,對于規范化浮點數,既然尾數的前導永遠是 1,那干脆不存儲,尾數其實比實際的多 1位,也就是說單精度的是 24位,雙精度是 52位。
(8) IEEE 754 的舍入規則
關于舍入,IEEE 754標準提供了 4種方法:
① 舍入到最接近
這是 IEEE 754標準的默認方式,將結果舍入為最接近且可以表示的值,但是當存在兩個數一樣接近的時候,則取其中的偶數(在二進制中是以0結尾的)。
取偶數最關鍵的步驟是找到一個中間值,先確定要保留的有效數字,找到要保留的有效數字最低位的下一位。如果這位是進制的一半,而且之后的位數都為 0,則這個值就是中間值。
這里以二進制為例,有效位數保留到小數點后 2位:
- 10.00011,中間值為 10.00100,小于中間值,向下舍入為 10.00
- 10.00110,中間值為 10.00100,大于中間值,向上舍入為 10.01
- 10.11100,中間值為 10.11100,等于中間值,要保留的最低有效位 1 為奇數,向上舍入為 11.00
- 10.10100,中間值為 10.10100,等于中間值,要保留的最低有效位 0 為偶數,向下舍入為 10.10
因此,上述十進制-123.3轉換成 IEEE 754二進制例子的舍入方式,采用向上舍入,即最后一位 +1。
② 朝 +∞方向舍入
3個要點:
- 正數多余位不全為 0,進位1
- 正數多余位全為 0,直接截尾
- 負數直接截尾
這里以二進制為例,有效位數保留到小數點后 3位:
- 0.0011001,正數,從小數點后 4位起,不全為0,則向上進位(最后一位 +1),結果值為 1.010,
- 0.0010000,正數,從小數點后 4位起,全為0,則直接截尾(從 4位起全部舍棄),結果值為 0.001
- -0.0011010,負數直接截尾(從 4位起全部舍棄),結果值為 -0.001
③ 朝 -∞方向舍入
3個要點:
- 正數直接截尾
- 負數多余位全為0,直接截尾
- 負數多余位不全為 0,進位1
這里以二進制為例,有效位數保留到小數點后 3位:
- 0.0011001,正數,則直接截尾(從 4位起全部舍棄),結果值為 0.001
- -0.001000,負數,從小數點后 4位起全為 0,則直接截尾(從 4位起全部舍棄),結果值 -0.001
- -0.001101,負數,從小數點后 4位起不全為 0,則向上進位(最后一位 +1),結果值-0.010
④ 朝 0方向舍入
2個要點:
- 正數直接截尾
- 負數直接截尾
數學上有 4舍5入,計算機中 0舍1入,因此,朝 0方向舍入就是直接舍棄。
這里以二進制為例,有效位數保留到小數點后 3位:
- 0.001100,正數,則直接截尾(從 4位起全部舍棄)棄,結果值 0.001
- -0.001100,負數,則直接截尾(從 4位起全部舍棄)棄,結果值 -0.001
四、非規范浮點數
上文提到了規范化浮點數,既然有規范化浮點數,是不是也存在非規范化浮點數?非規范化浮點數又是什么呢?
在 IEEE 754標準中,將“指數部分全是0,尾數部分非0”這樣的浮點數稱為非規范化浮點數,一般用于表示 0或者無限接近 0的很小的數字。
另外,IEEE 754標準還規定:非規范化浮點數的指數偏移值比規范化浮點數的指數偏移值小 1。
例如,最小的規范化單精度浮點數的指數部分編碼值為1,指數的實際值為-126;而非規范化的單精度浮點數的指數域編碼值為0,對應的指數實際值也是 -126 而不是-127。實際上非規范化浮點數仍然是有效可以使用的,只是它們的絕對值已經小于所有的規約浮點數的絕對值;即所有的非規范化浮點數比規約浮點數更接近0。規約浮點數的尾數大于等于1且小于2,而非規范化浮點數的尾數小于1且大于0。
下圖展示了非規范化單精度浮點數的二進制表示:
五、特殊值
另外,IEEE 754中還定義4個特殊值,如下表:
- 正負無窮大:指數全是 1,尾數全是 0,代表這個數是正負無窮大(±∞),正負取決于 S符號位。
- NaN:指數全是 1,尾數非 0,代表這不是一個數字(NaN,Not a Number)。
- 0:指數全是 0,尾數全是0,代表這個數是±0,正負取決于 S符號位。
- 很小的值:指數全是 0,尾數不全是0,代表這個數是一個很小的非規范化浮點數。
六、浮點數的比較和運算
有了 IEEE 754標準,浮點數的比較就很簡單,基本上可以按照符號位、指數域、尾數域的順序作字典比較。顯然,所有正數大于負數;正負號相同時,指數的二進制表示法更大的其浮點數值更大。
浮點數的運算和函數包含以下幾種:
- 加減乘除(Add、subtract、multiply、divide)。在加減運算中負零與零相等:?0.0=0.0
- 平方根(Square root):
- 浮點余數。返回值 x-(round(x/y)*y)。
- 近似到最近的整數 ?? ?? ?? ?? ??(??)。如果恰好在兩個相鄰整數之間,則近似到偶數。
- 比較運算. -Inf <負的規范化浮點數數<負的非規范化浮點數< -0.0 = 0.0 <正的非規范化浮點數<正的規范化浮點數< Inf;
- 特殊比較:-Inf = -Inf, Inf = Inf, NaN與任何浮點數(包括自身)的比較結果都為假,即 (NaN ≠ x) = false.
七、為什么 1.0-0.9 != 0.1?
先看 Java中的一個例子:
通過運行結果可以發現:在 Java中,不管是單精度 float,還是雙精度 double,1.0 - 0.9 != 0.1,為什么?
從上面的講解,我們已經知道浮點數 IEEE 754標準在內存中的存儲方式,這里我們再簡單的分析1.0 - 0.9場景:
1.0 可以精確轉換成IEEE 754標準二進制為:0 01111111 00000000000000000000000
0.9 轉換成IEEE 754標準二進制為:
符號位:0
偏移指數:-1 + 127 = (126)?? = (0111 1110)?
尾數:
0.9 x 2 = 1.8 1
0.8 x 2 = 1.6 1
0.6 x 2 = 1.2 1
0.2 x 2 = 0.4 0
0.4 x 2 = 0.8 0
0.8 x 2 = 1.6 1 開始進入循環,只能達到所需的精度后按需舍入結束
0.6 x 2 = 1.2 1
在0.9 轉換成 IEEE 754標準的二進制時,出現了循環,這樣的話,不管是單精度還是雙精度,0.9轉換成二進制之后都有精度損失,所以,對于 float 或者 double浮點數 0.9,轉換后其真實值已經不是 0.9,因此,1.0 - 0.9 != 0.1。
當然,除了這個例子,還有很多經典的例子,比如 w s m 為什么等于 0.30000000000000004?
八、總結
本文講解了定點數和浮點數,重點分析了浮點數以及IEEE 754標準下浮點數是如何存儲的。
- 浮點數一般用科學計數法表示
- IEEE 754標準的浮點數二進制實際包含:符號位,偏移指數,尾數 3部分。
- 十進制小數在轉換為二進制時,如果可以精確轉換,則不存在精度丟失。
- 十進制小數在轉換為二進制時,如果無法精確轉換(存在循環或者超出尾數的范圍),則存在精度丟失的問題。
- IEEE 754舍入規則有 4種方法:舍入到最接近、朝 +∞方向舍入、朝 -∞方向舍入以及朝 0方向舍入。