目錄
一,浮點數精度丟失?
二,整數的二進制表示
三,浮點數的二進制表示
四,iEEE 754浮點數的手動轉換
五,四舍六入五去偶
一,浮點數精度丟失?
在iOS開發中,我們時常會使用 NSString 的 +方法,用格式化字符串將一個浮點數包裹為字符串,如下面代碼所示:
接著在需要使用基本數據類型的地方,再將字符串轉為基本數據類型,使用 NSString 的 - 方法 doubleValue 或 floatValue 可以輕松幫我們做到這一點,如下面代碼所示:
一切都看起來很美好,不是嗎?16542.7 變為了字符串 @"16542.70",當我們需要使用基本數據類型來參與運算時,比如要計算總和,計算利率,再將 @"16542.70" 轉換為 浮點數的 16542.70,完美!perfect!輕松加愉快!好,先別高興太早,讓我們看看將字符串轉為基本數據類型后的結果:
的確如我們所期,打印出了我們一開始定義的兩個浮點數值,但這個直白且 “毋庸置疑” 的打印結果,其實僅僅是個煙霧彈,我們換一種方式,對格式化限定符稍加改動,再來看打印結果:
結果似乎仍然正確顯示。
別慌,我們再稍加改動,再看打印結果:
怎么會這樣??
浮點數似乎鬧了點小脾氣,他在我們預料的 “正確結果” 上發生了細小的偏差,至此你可能會恍然大悟,因為 C 語言中,格式化字符串默認 "%f" 默認保留到小數點后第6位,也就是說,即使浮點數的值不是你所期望的 16542.7 而是 16542.70000000001,我們在打印時默認讓他保留到了小數點后6位,那么這個 0.0000000001,也就理所當然被省略掉了。同樣道理,28732.599999999999 也因為這樣的舍入,變為我們所看到的正確結果 28732.600000,而通過限制保留到小數點后到具體位數,我的得以看到這個浮點數真實的面目。
問題到底出在哪里?
我的浮點數精度定義時分明是 16542.7 和 28732.6 被你搞這么一同方法調用,精度卻似乎是丟失了,具體是哪個步驟讓他發生了這種預期外的變化???
二,整數的二進制表示
在計算機內部,所有數據類型均是以二進制的方式存儲,比如 char 型變量 c = 'a',字符'a'對應的ASCALL編碼是97,則它可以用二進制表示為 1100 0001,比如 int 型變量 s = 255,則它可以用二進制表示為1111 1111,我們用以下打印佐證這一事實:
以下是該打印函數的實現體和測試用例,隨機數種子取固定值100,以
便你使用時能和我產生相同的結果。
我們知道,將整數映射到二進制的方式為補碼,簡單說來,對于一臺64位的機器
(可以簡單理解為內存地址最大表示的上限是64個bit位,也就是8個字節,你可以使用 sizeof( int * ) 觀察輸出來佐證這個理解,你將觀察到,64位機器指針是8個字節,而在32位機器上,指針是4個字節)
char 類型是 1 個字節,8 個 bit 位,則 0001 0100 表示為 1 * 2^2 + 1 * 2^4 = 20。
最大的 char 值是 0111 1111, 即 ( 2 << 7 ) - 1 也就是 127, 你可能會有所疑惑,如果最高位占 1 , 這樣不就比 127 還要大了嗎?記住,最高位是符號位,在 C 家族 的世界中,數據類型分為有符號和無符號,而這個最左邊也就是最高位的 bit 位,代表一個數據類型的符號,0 代表正數,1代表負數。
最小的 char 值是 1000 000, 即 -( 2 << 8 ) 也就是 -128, 在補碼表示中,最高位符號位為 1 代表負權重,所以 1001 0101 的有符號值就是 -(128) + 16 + 4 + 1 = -107,我們用以下代碼示例佐證該結論:
你可以通過右側二進制表示反推 char 值,加深對補碼表示的理解
三,浮點數的二進制表示
終于到了本篇文章的主題——浮點數,在計算機內,浮點數的存儲也不例外,仍然使用二進制位來存儲,但將浮點數映射為二進制的方式卻與整數表達大相徑庭,下面的打印使用了有意為之的空格作為隔斷,請觀察以下打印結果
乍看似乎毫無規律可循,其實你只用記住,當今世界絕大多數計算機采用的浮點數編碼方式都遵守 IEEE 754 標準,這個標準描述了這樣一種浮點數的定義方式:
浮點數值 = (-1) ^ S * ( 2 ^ E) * M
S 是符號位,E為移碼 (階碼 + 偏置量),M是尾數
單精度浮點數 符號位占 1 bit, 移碼占 8 bit,尾數占23 bit。上述打印采用了相同的格式的空格隔斷。
可以用下圖來形象的記憶單精度浮點數 ( float ) 在內存中的結構
因此,我們采用定義一個用位域分割的結構體,來表示單精度浮點數的內存結構,如下代碼所示
接著定義一個聯合,讓這個結構體和一個單精度浮點數共享一塊內存空間,我們會發現,這樣做是直觀且便于理解的。
這里用了 yh 的前綴只是為了解決系統已經有了 float_t 定義產生的名字沖突。
接下來就完成浮點數二進制格式打印函數的定義
四,iEEE 754浮點數的手動轉換
下面我們執行一些手動的轉換,并利用工具函數驗證結果,加深對浮點數的理解。
例1 :float a = -128.625
首先將十進制128.625轉換成二進制小數
128 -> 2^7 -> 10000000
0.625 -> 2^-1 + 2^-3 -> 0.101
128.625 -> 10000000.101
然后將二進制小數表示為 IEEE 754標準的格式
10000000.101 -> 1.0000000101 * 2^7
-> (-1) ^ 0 * (2 ^ 7) *(0.0000000101 + 1)
階碼的轉換公式為 : E = e - 2 ^ (k - 1) (k 為階碼位數)
對于單精度浮點數而言,階碼是 8 個 bit 位
e = E + 127 = 7 + 127 = 134
將其表示為二進制即 1000 0110
故 -128.625 的 IEEE 754標準 浮點數格式為
符號位 --------- 階碼 ------------------------------ 尾數
1 ------------- 1000 0110 -------------- 00000001010000000000000
用我們自己寫的工具函數來佐證這一結果:
例2 :float c = 1.1
在對 1.1 進行 IEEE 754 標準轉換前,我們先打印出 2^-1 ~ 2^-23 的精確值
- 1 0.5
- 2 0.25
- 3 0.125
- 4 0.0625
- 5 0.03125
- 6 0.015625
- 7 0.0078125
- 8 0.00390625
- 9 0.001953125
-10 0.0009765625
-11 0.00048828125
-12 0.000244140625
-13 0.0001220703125
-14 0.00006103515625
-15 0.000030517578125
-16 0.0000152587890625
-17 0.00000762939453125
-18 0.000003814697265625
-19 0.0000019073486328125
-20 0.00000095367431640625
-21 0.000000476837158203125
-22 0.0000002384185791015625
-23 0.00000011920928955078125
對照上面的數值,接下來開始轉換 0.1
如果尾數有5位
0.0625 + 0.03125 = 0.9375 -> 0.00011
如果尾數有6位
0.0625 + 0.03125 = 0.9375 -> 0.00011 因為如果加上第6位的1,就是 0.109375 超出了0.1
如果尾數有7位
0.625 + 0.03125 = 0.9375 -> 0.00011
如果尾數有8位
0.625 + 0.03125 + 0.00390625 = 0.09765625 -> 0.00011001
如果尾數有9位
0.625 + 0.03125 + 0.00390625 + 0.001953125 = 0.099609375 -> 0.000110011
如果尾數有10位和11位
0.6252 + 0.03125 + 0.00390625 + 0.001953125 = 0.099609375 -> 0.000110011
如果尾數是12位
0.6252 + 0.03125 + 0.00390625 + 0.001953125 + 0.000244140625 = 0.099853515625 -> 0.000110011001
如果尾數是13位
0.6252 + 0.03125 + 0.00390625 + 0.001953125 + 0.000244140625 + 0.0001220703125 = 0.0999755859375 -> 0.0001100110011
如果尾數是14位和15位
0.6252 + 0.03125 + 0.00390625 + 0.001953125 + 0.000244140625 + 0.0001220703125 = 0.0999755859375 -> 0.0001100110011
如果尾數是16位
0.6252 + 0.03125 + 0.00390625 + 0.001953125 + 0.000244140625 + 0.0001220703125 + 0.0000152587890625 = 0.0999908447265625 -> 0.0001100110011001
如果尾數是17位
0.6252 + 0.03125 + 0.00390625 + 0.001953125 + 0.000244140625 + 0.0001220703125 + 0.0000152587890625 + 0.00000762939453125 = 0.09999847412109375 -> 0.00011001100110011
如果尾數是18位和19位
0.6252 + 0.03125 + 0.00390625 + 0.001953125 + 0.000244140625 + 0.0001220703125 + 0.0000152587890625 + 0.00000762939453125 = 0.09999847412109375 -> 0.00011001100110011
如果尾數是20位
0.6252 + 0.03125 + 0.00390625 + 0.001953125 + 0.000244140625 + 0.0001220703125 + 0.0000152587890625 + 0.00000762939453125 + 0.00000095367431640625 = 0.09999942779541015625 -> 0.00011001100110011001
如果尾數是21位
0.6252 + 0.03125 + 0.00390625 + 0.001953125 + 0.000244140625 + 0.0001220703125 + 0.0000152587890625 + 0.00000762939453125 + 0.00000095367431640625 + 0.000000476837158203125 = 0.099999904632568359375 -> 0.000110011001100110011
如果尾數是22位和23位,結果都將是
0.6252 + 0.03125 + 0.00390625 + 0.001953125 + 0.000244140625 + 0.0001220703125 + 0.0000152587890625 + 0.00000762939453125 + 0.00000095367431640625 + 0.000000476837158203125 = 0.099999904632568359375 -> 0.000110011001100110011
恭喜你!如果你仔細用紙筆執行完上述繁瑣的計算,相信你對浮點數已經有了一些體會,當然,我肯定是沒手動做這些計算,盡管我能夠對天發四,上述計算都是絕對精確的,它們的實現方式如下代碼所示
這里采用鏈式編程為高精度算法在調用上提供了輕便的支持,使冗余的代碼變得簡潔,如果你對鏈式編程以及iOS內置的高精度算法庫比較熟悉,可以自己進行封裝。當然,對不熟悉的讀者,封裝方法也會在下一篇文章中講到。
從上述的轉換過程中可以發現,十進制 0.1 轉成 二進制表示的過程中似乎顯得無窮無盡,并且 0.1 的二進制表示中不斷重復地出現 0011 這一形式,你可能不禁想問,這個轉換過程真的是無窮無盡嗎?的確是這樣的,對于單精度浮點數而言,因為尾數只有23位,超出部分無法容納,轉換似乎是停止了。但你也可以看到,我們盡力而為的二進制表示結果 0.000110011001100110011 再轉換成 十進制 后是 0.099999904632568359375,顯然這是一個趨近值,如果尾數部分能容納的范圍再增長一些,這個轉換過程還將持續幾個來回,但這也僅僅只對向 0.1 的趨近中貢獻了微不足道的一些力量,實際上無論尾數有多長,都無法精確表示 0.1 (double 類型浮點數 的符號位占 1 bit,移碼占 11 bit,尾數占 52 bit)。
整理我們剛才全部轉換過程,可以得到:
1.000110011001100110011
整理成 iEEE 754 標準格式
(-1) ^ 0 * 2 ^ 0 * 0.000110011001100110011
根據 階碼 = 移碼 E + 偏置量 (2 ^ (k - 1)) k 表示階碼 bit 位數,單精度是 8 bit,雙精度是 12 bit
e = E + 127 = 127 -> 01111111
得到 1.1 轉換為 iEEE 754 標準編碼的浮點數
符號位 --------- 階碼 ------------------------------ 尾數
1 ------------- 0111 1111 -------------- 00011001100110011001100
用我們自己寫的工具函數來佐證這一結果:
等等!
細心你的也許會發現,這兩個結果是存在細微差別的!
用工具函數打印出來的浮點數尾數是
0 0011 0011 0011 0011 0011 0 "1"
最后一位是1
而經過剛才的手工計算,得到的尾數是
0 0011 0011 0011 0011 0011 0 "0"
最后一位是 0
好吧,如果你真能發現這一點,那我不得不對你的細心五體投地。
這里之所以產生如此細微的差別,原因在于操作系統內部實現的浮點數編碼時,默認是向偶數舍入的,為了說明什么是向偶數舍入,以及還有哪些舍入方式,我們來考慮下面尾數為3位的情況
五,四舍六入五去偶
如果我們對 0.1001 只能提供 3 個 bit 位用于表示,顯然,第三位是最低有效位,我們只能忍痛“截斷”第3位往后的數據,此時我們發現,0.0001 是 0.001的一半,在這種情況進行截斷時,操作系統默認采用舍入到偶數的方式,操作系統會認為最低有效位為0是偶數,為1就是奇數,所以 操作系統將 0.1001 舍入為 0.100 以保證最低有效位是偶數 0,而將 0.1011 舍入為 0.110 以保證最低有效位是偶數 0。
讓我們看兩個向偶數舍入的例子(保留到小數點后兩位),10.11100 采用向偶數舍入的方式變為 11.00,10.10100 采用向偶數舍入的方式變為 10.10。
需要注意的是,如果最低有效位后的小數總和大于最低有效位的一半,將采用向上舍入,把1進位到最低有效位,如果最低有效位后的小數總和小于最低有效位的一半,將會把最低有效位后的所有小數部分舍棄掉,讓我們再來看兩個向上舍入的例子(保留到小數點后兩位),10.01101 將會向上舍入為 10.10,0.1111 將會向上舍入為 1.00。
再看兩個向下舍入的例子(保留到小數點后兩位),0.1001 將會向下舍入為 0.10,0.0101 將會向下舍入為 0.01
回到我們的剛才轉換的 1.1,轉換后結果為
0 0111 1111 00011001100110011001100 1100...
可以看到,最低有效位往后的小數總和大于末尾的一半,所以采用向上舍入的方式,向最低有效位進 1,最終得到
符號位 --------- 階碼 ------------------------------ 尾數
1 ------------- 0111 1111 -------------- 00011001100110011001101
到此,你應該對很早不知何時何地聽到的
浮點數是無法精確表示大部分實數的
這句話有更佳深刻的體會,的確,能被精確表示的只是很少的一部分,再回過頭看開頭的例子,你也許會豁然開朗。
并非在 [NSString stringWithFormat:...] 或者 [string doubleValue] 中發生了浮點數精度的丟失,而是 iEEE 754 標準定義的浮點數本身就無法精確表示一些實數,這就好比十進制無法精確表示 (1 / 3)這個無限不循環小數。
既然從一開始就是不精確的,又何來精度丟失之談呢。