在六月份時,Gustavo Niemeyer 在他的部落格 [Labix.org](http://blog.labix.org/) 上提出了下面這個問題:*假設 uf 是一個無符號的 64 位元整型,但包含的內容卻是符合 IEEE-754 標準的二進位浮點數,你怎麼去區分 uf 是表示整型還是浮點型呢?*由於我對這方面的瞭解並不是很多,所以無法快速得出這個問題的答案,而且我也無法用語言來向你解釋,只能通過寫一個相關的程式來進行示範。幸運的是 Gustavo 發布了答案的思路,我認為非常有意思,然後我將思路進行了分解,這樣能更好地理解最初的那個問題。為了更通俗易懂,後面的樣本我將使用 32 位的數字。IEEE-754 標準是如何將一個浮點數字儲存為二進位格式的呢?最開始我找到了下面兩篇論文:http://steve.hollasch.net/cgindex/coding/ieeefloat.htmlhttp://class.ece.iastate.edu/arun/CprE281_F05/ieee754/ie5.htmlIEEE-754 規範用來表示一個特定二進位格式的浮點數,它是一種以 2 為底的科學計數法,如果你對我所說的 “二進位格式” 感到疑惑的話,那麼你應該去讀一下我以前發布的這篇 [《理解 Go 語言的類型》](https://studygolang.com/articles/13976)。科學計數法能非常高效地表達極大或極小的數字,它使用小數和乘法來進行表示,下面是幾個樣本:| 十進位 | 科學計數法 | 計算式 | 係數 | 底數 | 指數 | 尾數 || :------------ | :--------- | :------------ | :------ | :--: | :--: | :----: || 700 | 7e+2 | 7 * 10<sup>2</sup> | 7 | 10 | 2 | 0 || 4,900,000,000 | 4.9e+9 | 4.9 * 10<sup>9</sup> | 4.9 | 10 | 9 | .9 || 5362.63 | 5.36263e+3 | 5.36263 * 10<sup>3</sup> | 5.36263 | 10 | 3 | .36263 || -0.00345 | 3.45e-3 | 3.45 * 10<sup>-3</sup> | 3.45 | 10 | -3 | .45 || 0.085 | 1.36e-4 | 1.36 * 2<sup>-4</sup> | 1.36 | 2 | -4 | .36 |正常情況下科學計數法要求小數點左邊要有一個數字,十進位時這個數字在 1 到 9 之間,二進位時只能為 1。小數點右邊的數字稱為尾數,在科學計數法中所有的這些數字被稱為係數,這些術語都很重要,所以請花時間去學習一下並且好好理解上面的樣本。當我們把小數點移動到首位時,指數的值會進行怎樣的變化呢?如果我們將小數點移動到左側,那麼指數會是一個正數,反之會是一個負數,請觀察一下上面圖表中每個樣本的指數值。底數和指數需要組合起來使用,指數決定了對底數進行多少次冪的計算。在上面的第一個樣本中,數字 7 與 10(底數)的 2 次方(指數)相乘得到了原始的十進位數字 700。我們將小數點向左邊移動兩位把 700 轉換為 7.00 ,這時就會指數 +2 並且形成了科學計數法形式 7e+2 。IEEE-754 標準不是以 10 進位的方式,而是以 2 進位來儲存科學計數法。上面圖表中的最後一個樣本是 10 進位的數字 0.085 用 2 進位的科學計數法來表示,讓我們學習一下這是如何計算出來的。| 十進位 | 科學計數法 | 計算式 | 係數 | 底數 | 指數 | 尾數 || :----- | :--------- | :--------- | :--- | :--: | :--: | :--: || 0.085 | 1.36e-4 | 1.36 * 2<sup>-4</sup> | 1.36 | 2 | -4 | .36 |我們需要用 2 的若干次方除以 10 進位的數字(0.085)來獲得一個以 1 開頭的分數,“以1開頭的分數”是什麼意思呢?在樣本中我們需要一個看上去像是係數的值 1 + .36。IEEE-754 標準中需要係數以 "1." 開頭,這讓我們必須儲存尾數部分並且需要額外的位元位來表示精度。下面我們將採用一種暴力的方法,你會看到最終會得到代表著 0.085 並且以 1 開頭的分數:> 0.085 / 2<sup>-1</sup> = 0.17>> 0.085 / 2<sup>-2</sup> = 0.34>> 0.085 / 2<sup>-3</sup> = 0.68>> 0.085 / 2<sup>-4</sup> = 1.36 ** 我們找到了以1開頭的分數-4 這個指數讓我們獲得了所需的以 1 開頭的分數,現在我們已經具備了將 10 進位數字 0.085 儲存為 IEEE-754 格式的所有條件。讓我們來看看在 IEEE-754 格式中的位元位是如何排列的。| 精度 | 符號位 | 指數 | 分數位 | 位移 || :--------------: | :----: | :--------: | :--------: | :--: || Single (32 Bits) | 1 [31] | 8 [30-23] | 23 [22-00] | 127 || Double (64 Bits) | 1 [63] | 11 [62-52] | 52 [51-00] | 1023 |這些位元位可以分為三個部分,先是一個用來標記符號的位元位,然後是表示指數和分數部分的位元位。我們會將尾數作為二進位分數形式儲存在分數位元位中。當我們把 0.085 儲存為單精確度(32位元字)時,IEEE-754的位元模式看上去就像這樣:| 符號位 | 指數位 (123) | 分數位(.36) || :----: | :----------: | :--------------------------: || 0 | 0111 1011 | 010 1110 0001 0100 0111 1011 |最左邊的符號位決定了這個數的正負,如果把符號位設定為1,那麼這個數就是負數。接下來的 8 位代表著指數。在我們的樣本中,十進位數字 0.085 被轉換成以2為底的科學計數法格式 1.36 * 2<sup>-4</sup>,因此指數為 -4。為了能夠表示負數,指數位會有一個位移值,當數字是 32 位時這個位移值為 127。我們需要找到一個數,這個數減去位移值能得到 -4,在我們的樣本中這個數為 123。如果你注意一下指數的位元模式就會發現這個二進位表示的是數字 123。剩下的 23 位為分數位,為了得到出分數位的位元模式,我們需要對二進位的分數位進行計算和求和,直到求出尾數或者最為接近尾數的值,因為我們假定整數部分一直為 “1.”,所以只要儲存尾數部分。查看下面的圖表你就會明白二進位分數位是如何被計算出來的,從左至右的每一位都表示不同的分數值。| 二進位 | 分數 | 小數 | 冪 || :----: | :--: | :---: | :--: || 0.1 | 1⁄2 | 0.5 | 2-1 || 0.01 | 1⁄4 | 0.25 | 2-2 || 0.001 | 1⁄8 | 0.125 | 2-3 |我們需要設定正確的分數位來累加得到尾數,或者是足夠接近尾數的值,這也是為什麼有時我們會丟失一些精度。0**1**0 **111**0 000**1** 0**1**00 0**111** **1**0**11** = (0.36)| 位 | 值 | 分數 | 小數 | 總計 || :--: | :-----: | :-------: | :--------------: | :--------------: || 2 | 4 | 1⁄4 | 0.25 | 0.25 || 4 | 16 | 1⁄16 | 0.0625 | 0.3125 || 5 | 32 | 1⁄32 | 0.03125 | 0.34375 || 6 | 64 | 1⁄64 | 0.015625 | 0.359375 || 11 | 2048 | 1⁄2048 | 0.00048828125 | 0.35986328125 || 13 | 8192 | 1⁄8192 | 0.0001220703125 | 0.3599853515625 || 17 | 131072 | 1⁄131072 | 0.00000762939453 | 0.35999298095703 || 18 | 262144 | 1⁄262144 | 0.00000381469727 | 0.3599967956543 || 19 | 524288 | 1⁄524288 | 0.00000190734863 | 0.35999870300293 || 20 | 1048576 | 1⁄1048576 | 0.00000095367432 | 0.35999965667725 || 22 | 4194304 | 1⁄4194304 | 0.00000023841858 | 0.35999989509583 || 23 | 8388608 | 1⁄8388608 | 0.00000011920929 | 0.36000001430512 |你會看到當這12個位元位排列好之後,我們就得到了0.36這個值,以及後面還帶有一些額外的分數。讓我們總結一下現在所知道的IEEE-754格式:1. 任何10進位的數字都會被儲存為基於科學計數法的格式。2. 基於2進位的科學計數法必須遵循以1開頭的分數格式。3. 整個格式被分為截然不同的三部分。4. 符號位決定了數位正負。5. 指數位表示一個減去位移量的值。6. 分數位表示使用二進位分數累加得到的尾數。讓我們來驗證一下對於 IEEE-754 格式的分析是否正確。我們應該可以把 0.85 這個數字儲存為位元模式,並且它每一部分的值和我們之前看到的應該一致。接下來的代碼儲存了以二進位 IEEE-754 表示的數字 0.085:```gopackage mainimport ( "fmt" "math")func main() { var number float32 = 0.085 fmt.Printf("Starting Number: %f\n\n", number) // Float32bits returns the IEEE 754 binary representation bits := math.Float32bits(number) binary := fmt.Sprintf("%.32b", bits) fmt.Printf("Bit Pattern: %s | %s %s | %s %s %s %s %s %s\n\n", binary[0:1], binary[1:5], binary[5:9], binary[9:12], binary[12:16], binary[16:20], binary[20:24], binary[24:28], binary[28:32]) bias := 127 sign := bits & (1 << 31) exponentRaw := int(bits >> 23) exponent := exponentRaw - bias var mantissa float64 for index, bit := range binary[9:32] { if bit == 49 { position := index + 1 bitValue := math.Pow(2, float64(position)) fractional := 1 / bitValue mantissa = mantissa + fractional } } value := (1 + mantissa) * math.Pow(2, float64(exponent)) fmt.Printf("Sign: %d Exponent: %d (%d) Mantissa: %f Value: %f\n\n", sign, exponentRaw, exponent, mantissa, value)}```當我們運行程式時會顯示下面的輸出:```Starting Number: 0.085000Bit Pattern: 0 | 0111 1011 | 010 1110 0001 0100 0111 1011Sign: 0 Exponent: 123 (-4) Mantissa: 0.360000 Value: 0.085000```如果你將輸出中的位元模式和之前的樣本對比一下,那麼你會發現它們是一致的,所以我們之前所學習的 IEEE-754 都是正確的。現在我們可以回答 Gustavo 的問題了,我們如何區分一個整型中的內容呢?下面是一個能檢測整型是否被儲存為 IEEE-754 格式的函數,感謝 Gustavo 的代碼。```gofunc IsInt(bits uint32, bias int) { exponent := int(bits >> 23) - bias - 23 coefficient := (bits & ((1 << 23) - 1)) | (1 << 23) intTest := (coefficient & (1 << uint32(-exponent) - 1)) fmt.Printf("\nExponent: %d Coefficient: %d IntTest: %d\n", exponent, coefficient, intTest) if exponent < -23 { fmt.Printf("NOT INTEGER\n") return } if exponent < 0 && intTest != 0 { fmt.Printf("NOT INTEGER\n") return } fmt.Printf("INTEGER\n")}```那麼這個函數是如何進行檢測的呢?讓我們先將指數小於 -23 作為首要條件進行測試,如果用 1 作為我們的測試值,那麼指數和位移值都是 127,這意味著我們用指數減去位移值會得到 0。```Starting Number: 1.000000Bit Pattern: 0 | 0111 1111 | 000 0000 0000 0000 0000 0000Sign: 0 Exponent: 127 (0) Mantissa: 0.000000 Value: 1.000000Exponent: -23 Coefficient: 8388608 IntTest: 0INTEGER```在測試函數中減去了一個額外的 23,它表示 IEEE-754 格式中指數的位元位開始的位置,這也就是為什麼你會看到測試函數中會出現 -23 這個指數值。| 精度 | 標誌位 | 指數 | 分數位 | 位移值 || :--------------: | :----: | :-----------: | :--------: | :----: || Single (32 Bits) | 1 [31] | 8 [30-**23**] | 23 [22-00] | 127 |在第二個測試中必須要用到減法,所以任何小於 -23 的值必定小於 1,因此不是一個整型。讓我們用一個整型值來理解第二個測試是如何工作的,這次我們將要在代碼中把這個值設定為 234523,然後再一次運行程式。```Starting Number: 234523.000000Bit Pattern: 0 | 1001 0000 | 110 0101 0000 0110 1100 0000Sign: 0 Exponent: 144 (17) Mantissa: 0.789268 Value: 234523.000000Exponent: -6 Coefficient: 15009472 IntTest: 0INTEGER```第二個測試通過兩個條件來識別這個數字是否為整型,這需要用到按位元運算,下面提供了一個示範函數,讓我們看一下其中的演算法:```go exponent := int(bits >> 23) - bias - 23 coefficient := (bits & ((1 << 23) - 1)) | (1 << 23) intTest := coefficient & ((1 << uint32(-exponent)) - 1)```係數的計算需要向尾數部分增加1, 因此我們有了基於2進位的係數值。當我們查看係數計算的第一部分時,會看到下面的位元模式:```coefficient := (bits & ((1 << 23) - 1)) | (1 << 23)Bits: 01001000011001010000011011000000(1 << 23) - 1: 00000000011111111111111111111111bits & ((1 << 23) - 1): 00000000011001010000011011000000```第一部分的係數計算中從 IEEE-754 位元模式中移除了符號位和指數位。第二部分的計算中會把 “1 +” 加入到位元模式中。(註:看圖就會明白,就是分數位最高位的前一位進行了或操作變為1)```coefficient := (bits & ((1 << 23) - 1)) | (1 << 23)bits & ((1 << 23) - 1): 00000000011001010000011011000000(1 << 23): 00000000100000000000000000000000coefficient: 00000000111001010000011011000000```到了這時係數就已經確定了,我們用這個 intTest 函數來計算一下:```exponent := int(bits >> 23) - bias - 23intTest := (coefficient & ((1 << uint32(-exponent)) - 1))exponent: (144 - 127 - 23) = -61 << uint32(-exponent): 000000(1 << uint32(-exponent)) - 1: 111111coefficient: 000000001110010100000110110000001 << uint32(-exponent)) - 1: 00000000000000000000000000111111intTest: 00000000000000000000000000000000```我們通過測試Function Compute出的指數值通常用來 確定 下一步中用來比較係數值。在這個例子中指數值為 -6,這個值是使用所儲存的指數值(144)減去位移值(127),再減去指數的開始位置(23)所計算出來的。-6 表示的位元模式是 6 個 1(1‘s),最後的操作是將這個 6 個位元位對係數的最右邊 6 位進行位與計算,最終就得到了 intTest 的值。第二個測試函數是在 initTest 值不為 0 的情況下尋找小於零 (0) 的指數值,表明這個數字中儲存的不是整型。在值為 234523 的這個樣本中,指數小數零 (0) 但是 intTest 的值大於零 (0),說明這是一個整型。我將樣本程式放在了官網的 Go Playground 上面,你可以方便地運行它。[http://play.golang.org/p/3xraud43pi](https://www.ardanlabs.com/blog/broken-link.html)如果沒有 Gustavo 的代碼,我可能一輩子都找不到解決方案,下面是他解決方案的代碼連結:http://bazaar.launchpad.net/~niemeyer/strepr/trunk/view/6/strepr.go#L160你也可以複製粘貼下面的副本代碼:```gopackage mainimport ( "fmt" "math")func main() { var number float32 = 234523 fmt.Printf("Starting Number: %f\n\n", number) // Float32bits returns the IEEE 754 binary representation bits := math.Float32bits(number) binary := fmt.Sprintf("%.32b", bits) fmt.Printf("Bit Pattern: %s | %s %s | %s %s %s %s %s %s\n\n", binary[0:1], binary[1:5], binary[5:9], binary[9:12], binary[12:16], binary[16:20], binary[20:24], binary[24:28], binary[28:32]) bias := 127 sign := bits & (1 << 31) exponentRaw := int(bits >> 23) exponent := exponentRaw - bias var mantissa float64 for index, bit := range binary[9:32] { if bit == 49 { position := index + 1 bitValue := math.Pow(2, float64(position)) fractional := 1 / bitValue mantissa = mantissa + fractional } } value := (1 + mantissa) * math.Pow(2, float64(exponent)) fmt.Printf("Sign: %d Exponent: %d (%d) Mantissa: %f Value: %f\n\n", sign, exponentRaw, exponent, mantissa, value) IsInt(bits, bias)}func IsInt(bits uint32, bias int) { exponent := int(bits>>23) - bias - 23 coefficient := (bits & ((1 << 23) - 1)) | (1 << 23) intTest := coefficient & ((1 << uint32(-exponent)) - 1) ShowBits(bits, bias, exponent) fmt.Printf("\nExp: %d Frac: %d IntTest: %d\n", exponent, coefficient, intTest) if exponent < -23 { fmt.Printf("NOT INTEGER\n") return } if exponent < 0 && intTest != 0 { fmt.Printf("NOT INTEGER\n") return } fmt.Printf("INTEGER\n")}func ShowBits(bits uint32, bias int, exponent int) { value := (1 << 23) - 1 value2 := (bits & ((1 << 23) - 1)) value3 := (1 << 23) coefficient := (bits & ((1 << 23) - 1)) | (1 << 23) fmt.Printf("Bits:\t\t\t%.32b\n", bits) fmt.Printf("(1 << 23) - 1:\t\t%.32b\n", value) fmt.Printf("bits & ((1 << 23) - 1):\t\t%.32b\n\n", value2) fmt.Printf("bits & ((1 << 23) - 1):\t\t%.32b\n", value2) fmt.Printf("(1 << 23):\t\t\t%.32b\n", value3) fmt.Printf("coefficient:\t\t\t%.32b\n\n", coefficient) value5 := 1 << uint32(-exponent) value6 := (1 << uint32(-exponent)) - 1 inTest := (coefficient & ((1 << uint32(-exponent)) - 1)) fmt.Printf("1 << uint32(-exponent):\t\t%.32b\n", value5) fmt.Printf("(1 << uint32(-exponent)) - 1:\t%.32b\n\n", value6) fmt.Printf("coefficient:\t\t\t%.32b\n", coefficient) fmt.Printf("(1 << uint32(-exponent)) - 1:\t%.32b\n", value6) fmt.Printf("intTest:\t\t\t%.32b\n", inTest)}```我想要感謝 Gustavo 提出這個問題並且讓我徹底理解了其中一些知識。
via: https://www.ardanlabs.com/blog/2013/08/gustavos-ieee-754-brain-teaser.html
作者:William Kennedy 譯者:maxwell365 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
144 次點擊