目錄
一乾戏,浮點(diǎn)數(shù)精度丟失?
二三热,整數(shù)的二進(jìn)制表示
三鼓择,浮點(diǎn)數(shù)的二進(jìn)制表示
四,iEEE 754浮點(diǎn)數(shù)的手動(dòng)轉(zhuǎn)換
五就漾,四舍六入五去偶
一呐能,浮點(diǎn)數(shù)精度丟失?
在iOS開發(fā)中,我們時(shí)常會(huì)使用 NSString 的 +方法,用格式化字符串將一個(gè)浮點(diǎn)數(shù)包裹為字符串摆出,如下面代碼所示:
接著在需要使用基本數(shù)據(jù)類型的地方朗徊,再將字符串轉(zhuǎn)為基本數(shù)據(jù)類型,使用 NSString 的 - 方法 doubleValue 或 floatValue 可以輕松幫我們做到這一點(diǎn)偎漫,如下面代碼所示:
一切都看起來很美好爷恳,不是嗎?16542.7 變?yōu)榱俗址?@"16542.70"象踊,當(dāng)我們需要使用基本數(shù)據(jù)類型來參與運(yùn)算時(shí)温亲,比如要計(jì)算總和,計(jì)算利率杯矩,再將 @"16542.70" 轉(zhuǎn)換為 浮點(diǎn)數(shù)的 16542.70栈虚,完美!perfect史隆!輕松加愉快魂务!好,先別高興太早逆害,讓我們看看將字符串轉(zhuǎn)為基本數(shù)據(jù)類型后的結(jié)果:
的確如我們所期头镊,打印出了我們一開始定義的兩個(gè)浮點(diǎn)數(shù)值,但這個(gè)直白且 “毋庸置疑” 的打印結(jié)果魄幕,其實(shí)僅僅是個(gè)煙霧彈相艇,我們換一種方式,對(duì)格式化限定符稍加改動(dòng)纯陨,再來看打印結(jié)果:
結(jié)果似乎仍然正確顯示坛芽。
別慌,我們?cè)偕约痈膭?dòng)翼抠,再看打印結(jié)果:
怎么會(huì)這樣咙轩??
浮點(diǎn)數(shù)似乎鬧了點(diǎn)小脾氣阴颖,他在我們預(yù)料的 “正確結(jié)果” 上發(fā)生了細(xì)小的偏差活喊,至此你可能會(huì)恍然大悟,因?yàn)?C 語(yǔ)言中量愧,格式化字符串默認(rèn) "%f" 默認(rèn)保留到小數(shù)點(diǎn)后第6位钾菊,也就是說,即使浮點(diǎn)數(shù)的值不是你所期望的 16542.7 而是 16542.70000000001偎肃,我們?cè)诖蛴r(shí)默認(rèn)讓他保留到了小數(shù)點(diǎn)后6位煞烫,那么這個(gè) 0.0000000001,也就理所當(dāng)然被省略掉了累颂。同樣道理滞详,28732.599999999999 也因?yàn)檫@樣的舍入,變?yōu)槲覀兯吹降恼_結(jié)果 28732.600000,而通過限制保留到小數(shù)點(diǎn)后到具體位數(shù)料饥,我的得以看到這個(gè)浮點(diǎn)數(shù)真實(shí)的面目蒲犬。
問題到底出在哪里?
我的浮點(diǎn)數(shù)精度定義時(shí)分明是 16542.7 和 28732.6 被你搞這么一同方法調(diào)用稀火,精度卻似乎是丟失了暖哨,具體是哪個(gè)步驟讓他發(fā)生了這種預(yù)期外的變化赌朋?凰狞??
二沛慢,整數(shù)的二進(jìn)制表示
在計(jì)算機(jī)內(nèi)部赡若,所有數(shù)據(jù)類型均是以二進(jìn)制的方式存儲(chǔ),比如 char 型變量 c = 'a'团甲,字符'a'對(duì)應(yīng)的ASCALL編碼是97逾冬,則它可以用二進(jìn)制表示為 1100 0001,比如 int 型變量 s = 255躺苦,則它可以用二進(jìn)制表示為1111 1111身腻,我們用以下打印佐證這一事實(shí):
以下是該打印函數(shù)的實(shí)現(xiàn)體和測(cè)試用例,隨機(jī)數(shù)種子取固定值100匹厘,以
便你使用時(shí)能和我產(chǎn)生相同的結(jié)果嘀趟。
我們知道,將整數(shù)映射到二進(jìn)制的方式為補(bǔ)碼愈诚,簡(jiǎn)單說來她按,對(duì)于一臺(tái)64位的機(jī)器
(可以簡(jiǎn)單理解為內(nèi)存地址最大表示的上限是64個(gè)bit位,也就是8個(gè)字節(jié)炕柔,你可以使用 sizeof( int * ) 觀察輸出來佐證這個(gè)理解酌泰,你將觀察到,64位機(jī)器指針是8個(gè)字節(jié)匕累,而在32位機(jī)器上陵刹,指針是4個(gè)字節(jié))
char 類型是 1 個(gè)字節(jié),8 個(gè) bit 位欢嘿,則 0001 0100 表示為 1 * 2^2 + 1 * 2^4 = 20衰琐。
最大的 char 值是 0111 1111, 即 ( 2 << 7 ) - 1 也就是 127, 你可能會(huì)有所疑惑,如果最高位占 1 际插, 這樣不就比 127 還要大了嗎碘耳?記住,最高位是符號(hào)位框弛,在 C 家族 的世界中辛辨,數(shù)據(jù)類型分為有符號(hào)和無符號(hào),而這個(gè)最左邊也就是最高位的 bit 位,代表一個(gè)數(shù)據(jù)類型的符號(hào)斗搞,0 代表正數(shù)指攒,1代表負(fù)數(shù)。
最小的 char 值是 1000 000, 即 -( 2 << 8 ) 也就是 -128, 在補(bǔ)碼表示中僻焚,最高位符號(hào)位為 1 代表負(fù)權(quán)重允悦,所以 1001 0101 的有符號(hào)值就是 -(128) + 16 + 4 + 1 = -107,我們用以下代碼示例佐證該結(jié)論:
你可以通過右側(cè)二進(jìn)制表示反推 char 值虑啤,加深對(duì)補(bǔ)碼表示的理解
三隙弛,浮點(diǎn)數(shù)的二進(jìn)制表示
終于到了本篇文章的主題——浮點(diǎn)數(shù),在計(jì)算機(jī)內(nèi)狞山,浮點(diǎn)數(shù)的存儲(chǔ)也不例外全闷,仍然使用二進(jìn)制位來存儲(chǔ),但將浮點(diǎn)數(shù)映射為二進(jìn)制的方式卻與整數(shù)表達(dá)大相徑庭萍启,下面的打印使用了有意為之的空格作為隔斷总珠,請(qǐng)觀察以下打印結(jié)果
乍看似乎毫無規(guī)律可循,其實(shí)你只用記住勘纯,當(dāng)今世界絕大多數(shù)計(jì)算機(jī)采用的浮點(diǎn)數(shù)編碼方式都遵守 IEEE 754 標(biāo)準(zhǔn)局服,這個(gè)標(biāo)準(zhǔn)描述了這樣一種浮點(diǎn)數(shù)的定義方式:
浮點(diǎn)數(shù)值 = (-1) ^ S * ( 2 ^ E) * M
S 是符號(hào)位,E為移碼 (階碼 + 偏置量)驳遵,M是尾數(shù)
單精度浮點(diǎn)數(shù) 符號(hào)位占 1 bit, 移碼占 8 bit淫奔,尾數(shù)占23 bit。上述打印采用了相同的格式的空格隔斷超埋。
可以用下圖來形象的記憶單精度浮點(diǎn)數(shù) ( float ) 在內(nèi)存中的結(jié)構(gòu)
因此搏讶,我們采用定義一個(gè)用位域分割的結(jié)構(gòu)體,來表示單精度浮點(diǎn)數(shù)的內(nèi)存結(jié)構(gòu)霍殴,如下代碼所示
接著定義一個(gè)聯(lián)合媒惕,讓這個(gè)結(jié)構(gòu)體和一個(gè)單精度浮點(diǎn)數(shù)共享一塊內(nèi)存空間,我們會(huì)發(fā)現(xiàn)来庭,這樣做是直觀且便于理解的妒蔚。
這里用了 yh 的前綴只是為了解決系統(tǒng)已經(jīng)有了 float_t 定義產(chǎn)生的名字沖突。
接下來就完成浮點(diǎn)數(shù)二進(jìn)制格式打印函數(shù)的定義
四月弛,iEEE 754浮點(diǎn)數(shù)的手動(dòng)轉(zhuǎn)換
下面我們執(zhí)行一些手動(dòng)的轉(zhuǎn)換肴盏,并利用工具函數(shù)驗(yàn)證結(jié)果,加深對(duì)浮點(diǎn)數(shù)的理解帽衙。
例1 :float a = -128.625
首先將十進(jìn)制128.625轉(zhuǎn)換成二進(jìn)制小數(shù)
128 -> 2^7 -> 10000000
0.625 -> 2^-1 + 2^-3 -> 0.101
128.625 -> 10000000.101
然后將二進(jìn)制小數(shù)表示為 IEEE 754標(biāo)準(zhǔn)的格式
10000000.101 -> 1.0000000101 * 2^7
-> (-1) ^ 0 * (2 ^ 7) *(0.0000000101 + 1)
階碼的轉(zhuǎn)換公式為 : E = e - 2 ^ (k - 1) (k 為階碼位數(shù))
對(duì)于單精度浮點(diǎn)數(shù)而言菜皂,階碼是 8 個(gè) bit 位
e = E + 127 = 7 + 127 = 134
將其表示為二進(jìn)制即 1000 0110
故 -128.625 的 IEEE 754標(biāo)準(zhǔn) 浮點(diǎn)數(shù)格式為
符號(hào)位 --------- 階碼 ------------------------------ 尾數(shù)
1 ------------- 1000 0110 -------------- 00000001010000000000000
用我們自己寫的工具函數(shù)來佐證這一結(jié)果:
例2 :float c = 1.1
在對(duì) 1.1 進(jìn)行 IEEE 754 標(biāo)準(zhǔn)轉(zhuǎn)換前,我們先打印出 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
對(duì)照上面的數(shù)值厉萝,接下來開始轉(zhuǎn)換 0.1
如果尾數(shù)有5位
0.0625 + 0.03125 = 0.9375 -> 0.00011
如果尾數(shù)有6位
0.0625 + 0.03125 = 0.9375 -> 0.00011 因?yàn)槿绻由系?位的1恍飘,就是 0.109375 超出了0.1
如果尾數(shù)有7位
0.625 + 0.03125 = 0.9375 -> 0.00011
如果尾數(shù)有8位
0.625 + 0.03125 + 0.00390625 = 0.09765625 -> 0.00011001
如果尾數(shù)有9位
0.625 + 0.03125 + 0.00390625 + 0.001953125 = 0.099609375 -> 0.000110011
如果尾數(shù)有10位和11位
0.6252 + 0.03125 + 0.00390625 + 0.001953125 = 0.099609375 -> 0.000110011
如果尾數(shù)是12位
0.6252 + 0.03125 + 0.00390625 + 0.001953125 + 0.000244140625 = 0.099853515625 -> 0.000110011001
如果尾數(shù)是13位
0.6252 + 0.03125 + 0.00390625 + 0.001953125 + 0.000244140625 + 0.0001220703125 = 0.0999755859375 -> 0.0001100110011
如果尾數(shù)是14位和15位
0.6252 + 0.03125 + 0.00390625 + 0.001953125 + 0.000244140625 + 0.0001220703125 = 0.0999755859375 -> 0.0001100110011
如果尾數(shù)是16位
0.6252 + 0.03125 + 0.00390625 + 0.001953125 + 0.000244140625 + 0.0001220703125 + 0.0000152587890625 = 0.0999908447265625 -> 0.0001100110011001
如果尾數(shù)是17位
0.6252 + 0.03125 + 0.00390625 + 0.001953125 + 0.000244140625 + 0.0001220703125 + 0.0000152587890625 + 0.00000762939453125 = 0.09999847412109375 -> 0.00011001100110011
如果尾數(shù)是18位和19位
0.6252 + 0.03125 + 0.00390625 + 0.001953125 + 0.000244140625 + 0.0001220703125 + 0.0000152587890625 + 0.00000762939453125 = 0.09999847412109375 -> 0.00011001100110011
如果尾數(shù)是20位
0.6252 + 0.03125 + 0.00390625 + 0.001953125 + 0.000244140625 + 0.0001220703125 + 0.0000152587890625 + 0.00000762939453125 + 0.00000095367431640625 = 0.09999942779541015625 -> 0.00011001100110011001
如果尾數(shù)是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
如果尾數(shù)是22位和23位榨崩,結(jié)果都將是
0.6252 + 0.03125 + 0.00390625 + 0.001953125 + 0.000244140625 + 0.0001220703125 + 0.0000152587890625 + 0.00000762939453125 + 0.00000095367431640625 + 0.000000476837158203125 = 0.099999904632568359375 -> 0.000110011001100110011
恭喜你!如果你仔細(xì)用紙筆執(zhí)行完上述繁瑣的計(jì)算章母,相信你對(duì)浮點(diǎn)數(shù)已經(jīng)有了一些體會(huì)母蛛,當(dāng)然,我肯定是沒手動(dòng)做這些計(jì)算乳怎,盡管我能夠?qū)μ彀l(fā)四彩郊,上述計(jì)算都是絕對(duì)精確的,它們的實(shí)現(xiàn)方式如下代碼所示
這里采用鏈?zhǔn)骄幊虨楦呔人惴ㄔ谡{(diào)用上提供了輕便的支持蚪缀,使冗余的代碼變得簡(jiǎn)潔秫逝,如果你對(duì)鏈?zhǔn)骄幊桃约癷OS內(nèi)置的高精度算法庫(kù)比較熟悉,可以自己進(jìn)行封裝椿胯。當(dāng)然筷登,對(duì)不熟悉的讀者剃根,封裝方法也會(huì)在下一篇文章中講到哩盲。
從上述的轉(zhuǎn)換過程中可以發(fā)現(xiàn),十進(jìn)制 0.1 轉(zhuǎn)成 二進(jìn)制表示的過程中似乎顯得無窮無盡狈醉,并且 0.1 的二進(jìn)制表示中不斷重復(fù)地出現(xiàn) 0011 這一形式廉油,你可能不禁想問,這個(gè)轉(zhuǎn)換過程真的是無窮無盡嗎苗傅?的確是這樣的抒线,對(duì)于單精度浮點(diǎn)數(shù)而言,因?yàn)槲矓?shù)只有23位渣慕,超出部分無法容納嘶炭,轉(zhuǎn)換似乎是停止了。但你也可以看到逊桦,我們盡力而為的二進(jìn)制表示結(jié)果 0.000110011001100110011 再轉(zhuǎn)換成 十進(jìn)制 后是 0.099999904632568359375眨猎,顯然這是一個(gè)趨近值,如果尾數(shù)部分能容納的范圍再增長(zhǎng)一些强经,這個(gè)轉(zhuǎn)換過程還將持續(xù)幾個(gè)來回睡陪,但這也僅僅只對(duì)向 0.1 的趨近中貢獻(xiàn)了微不足道的一些力量,實(shí)際上無論尾數(shù)有多長(zhǎng)匿情,都無法精確表示 0.1 (double 類型浮點(diǎn)數(shù) 的符號(hào)位占 1 bit兰迫,移碼占 11 bit,尾數(shù)占 52 bit)炬称。
整理我們剛才全部轉(zhuǎn)換過程汁果,可以得到:
1.000110011001100110011
整理成 iEEE 754 標(biāo)準(zhǔn)格式
(-1) ^ 0 * 2 ^ 0 * 0.000110011001100110011
根據(jù) 階碼 = 移碼 E + 偏置量 (2 ^ (k - 1)) k 表示階碼 bit 位數(shù),單精度是 8 bit玲躯,雙精度是 12 bit
e = E + 127 = 127 -> 01111111
得到 1.1 轉(zhuǎn)換為 iEEE 754 標(biāo)準(zhǔn)編碼的浮點(diǎn)數(shù)
符號(hào)位 --------- 階碼 ------------------------------ 尾數(shù)
1 ------------- 0111 1111 -------------- 00011001100110011001100
用我們自己寫的工具函數(shù)來佐證這一結(jié)果:
等等据德!
細(xì)心你的也許會(huì)發(fā)現(xiàn)鲸伴,這兩個(gè)結(jié)果是存在細(xì)微差別的!
用工具函數(shù)打印出來的浮點(diǎn)數(shù)尾數(shù)是
0 0011 0011 0011 0011 0011 0 "1"
最后一位是1
而經(jīng)過剛才的手工計(jì)算晋控,得到的尾數(shù)是
0 0011 0011 0011 0011 0011 0 "0"
最后一位是 0
好吧汞窗,如果你真能發(fā)現(xiàn)這一點(diǎn),那我不得不對(duì)你的細(xì)心五體投地赡译。
這里之所以產(chǎn)生如此細(xì)微的差別仲吏,原因在于操作系統(tǒng)內(nèi)部實(shí)現(xiàn)的浮點(diǎn)數(shù)編碼時(shí),默認(rèn)是向偶數(shù)舍入的蝌焚,為了說明什么是向偶數(shù)舍入裹唆,以及還有哪些舍入方式,我們來考慮下面尾數(shù)為3位的情況
五只洒,四舍六入五去偶
如果我們對(duì) 0.1001 只能提供 3 個(gè) bit 位用于表示许帐,顯然,第三位是最低有效位毕谴,我們只能忍痛“截?cái)唷钡?位往后的數(shù)據(jù)成畦,此時(shí)我們發(fā)現(xiàn),0.0001 是 0.001的一半涝开,在這種情況進(jìn)行截?cái)鄷r(shí)循帐,操作系統(tǒng)默認(rèn)采用舍入到偶數(shù)的方式,操作系統(tǒng)會(huì)認(rèn)為最低有效位為0是偶數(shù)舀武,為1就是奇數(shù)拄养,所以 操作系統(tǒng)將 0.1001 舍入為 0.100 以保證最低有效位是偶數(shù) 0,而將 0.1011 舍入為 0.110 以保證最低有效位是偶數(shù) 0银舱。
讓我們看兩個(gè)向偶數(shù)舍入的例子(保留到小數(shù)點(diǎn)后兩位)瘪匿,10.11100 采用向偶數(shù)舍入的方式變?yōu)?11.00,10.10100 采用向偶數(shù)舍入的方式變?yōu)?10.10寻馏。
需要注意的是棋弥,如果最低有效位后的小數(shù)總和大于最低有效位的一半,將采用向上舍入操软,把1進(jìn)位到最低有效位嘁锯,如果最低有效位后的小數(shù)總和小于最低有效位的一半,將會(huì)把最低有效位后的所有小數(shù)部分舍棄掉聂薪,讓我們?cè)賮砜磧蓚€(gè)向上舍入的例子(保留到小數(shù)點(diǎn)后兩位)家乘,10.01101 將會(huì)向上舍入為 10.10,0.1111 將會(huì)向上舍入為 1.00藏澳。
再看兩個(gè)向下舍入的例子(保留到小數(shù)點(diǎn)后兩位)仁锯,0.1001 將會(huì)向下舍入為 0.10,0.0101 將會(huì)向下舍入為 0.01
回到我們的剛才轉(zhuǎn)換的 1.1翔悠,轉(zhuǎn)換后結(jié)果為
0 0111 1111 00011001100110011001100 1100...
可以看到业崖,最低有效位往后的小數(shù)總和大于末尾的一半野芒,所以采用向上舍入的方式,向最低有效位進(jìn) 1双炕,最終得到
符號(hào)位 --------- 階碼 ------------------------------ 尾數(shù)
1 ------------- 0111 1111 -------------- 00011001100110011001101
到此狞悲,你應(yīng)該對(duì)很早不知何時(shí)何地聽到的
浮點(diǎn)數(shù)是無法精確表示大部分實(shí)數(shù)的
這句話有更佳深刻的體會(huì),的確妇斤,能被精確表示的只是很少的一部分摇锋,再回過頭看開頭的例子,你也許會(huì)豁然開朗站超。
并非在 [NSString stringWithFormat:...] 或者 [string doubleValue] 中發(fā)生了浮點(diǎn)數(shù)精度的丟失荸恕,而是 iEEE 754 標(biāo)準(zhǔn)定義的浮點(diǎn)數(shù)本身就無法精確表示一些實(shí)數(shù),這就好比十進(jìn)制無法精確表示 (1 / 3)這個(gè)無限不循環(huán)小數(shù)死相。
既然從一開始就是不精確的融求,又何來精度丟失之談呢。