深入淺出iOS浮點數精度問題 (上)

目錄

一,浮點數精度丟失?

二,整數的二進制表示

三,浮點數的二進制表示

四,iEEE 754浮點數的手動轉換

五,四舍六入五去偶


一,浮點數精度丟失?

在iOS開發中,我們時常會使用 NSString 的 +方法,用格式化字符串將一個浮點數包裹為字符串,如下面代碼所示:

將浮點數包裝為字符串

接著在需要使用基本數據類型的地方,再將字符串轉為基本數據類型,使用 NSString 的 - 方法 doubleValue 或 floatValue 可以輕松幫我們做到這一點,如下面代碼所示:

取出字符串的浮點值

一切都看起來很美好,不是嗎?16542.7 變為了字符串 @"16542.70",當我們需要使用基本數據類型來參與運算時,比如要計算總和,計算利率,再將 @"16542.70" 轉換為 浮點數的 16542.70,完美!perfect!輕松加愉快!好,先別高興太早,讓我們看看將字符串轉為基本數據類型后的結果:

字符串轉為浮點值

的確如我們所期,打印出了我們一開始定義的兩個浮點數值,但這個直白且 “毋庸置疑” 的打印結果,其實僅僅是個煙霧彈,我們換一種方式,對格式化限定符稍加改動,再來看打印結果:

保留到小數點后10位

結果似乎仍然正確顯示。
別慌,我們再稍加改動,再看打印結果:

保留到小數點后12位

怎么會這樣??
浮點數似乎鬧了點小脾氣,他在我們預料的 “正確結果” 上發生了細小的偏差,至此你可能會恍然大悟,因為 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 的 負值 的二進制表示
展示有符號 char 的 正值 的二進制表示

你可以通過右側二進制表示反推 char 值,加深對補碼表示的理解

三,浮點數的二進制表示

終于到了本篇文章的主題——浮點數,在計算機內,浮點數的存儲也不例外,仍然使用二進制位來存儲,但將浮點數映射為二進制的方式卻與整數表達大相徑庭,下面的打印使用了有意為之的空格作為隔斷,請觀察以下打印結果



單精度浮點數的二進制表示

乍看似乎毫無規律可循,其實你只用記住,當今世界絕大多數計算機采用的浮點數編碼方式都遵守 IEEE 754 標準,這個標準描述了這樣一種浮點數的定義方式:

浮點數值 = (-1) ^ S * ( 2 ^ E) * M

S 是符號位,E為移碼 (階碼 + 偏置量),M是尾數

單精度浮點數 符號位占 1 bit, 移碼占 8 bit,尾數占23 bit。上述打印采用了相同的格式的空格隔斷。

可以用下圖來形象的記憶單精度浮點數 ( float ) 在內存中的結構

iEEE 754編碼的單精度浮點數的內存示意圖

因此,我們采用定義一個用位域分割的結構體,來表示單精度浮點數的內存結構,如下代碼所示


單精度浮點數位域結構體

接著定義一個聯合,讓這個結構體和一個單精度浮點數共享一塊內存空間,我們會發現,這樣做是直觀且便于理解的。

單精度浮點數聯合

這里用了 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

用我們自己寫的工具函數來佐證這一結果:

屏幕快照 2017-09-06 12.34.37.png

例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高精度庫封裝分類

這里采用鏈式編程為高精度算法在調用上提供了輕便的支持,使冗余的代碼變得簡潔,如果你對鏈式編程以及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)這個無限不循環小數。

既然從一開始就是不精確的,又何來精度丟失之談呢。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容