前文
前言
在上文中我們學習了go語言中的自動類型推斷
我們將在本文中深入理解go語言浮點數的存儲細節
下面的一段簡單程序 0.3 + 0.6 結果是什麼?有人會天真的認爲是0.9,但實際輸出卻是0.8999999999999999(go 1.13.5)
var f1 float64 = 0.3 var f2 float64 = 0.6 fmt.Println(f1 + f2)
問題在於大多數小數表示成二進制之後是近似且無限的。以0.1爲例。它可能是你能想到的最簡單的十進制之一,但是二進制看起來卻非常複雜:0.0001100110011001100… 他是一串連續循環無限的數字(關於如何轉換爲二進制數以後介紹)。
結果的荒誕性告訴我們,必須深入理解浮點數在計算機中的存儲方式及其性質,才能正確處理數字的計算。
golang 與其他很多語言(C、C++、Python)一樣,使用了IEEE-754標準存儲浮點數。
IEEE-754 如何存儲浮點數
IEEE-754規範使用特殊的以2爲基數的科學表示法表示浮點數。
| 基本的10進制數字 | 科學計數法表示 | 指數表示 | 係數 | 底數 | 指數 | 小數 | |----------------|---------------------|----------------|-------------|------|----------|----------| | 700 | 7e+2 | 7 * 10^2 | 7 | 10 | 2 | 0 | | 4,900,000,000 | 4.9e+9 | 4.9 * 10^9 | 4.9 | 10 | 9 | .9 | | 5362.63 | 5.36263e+3 | 5.36263 * 10^3 | 5.36263 | 10 | 3 | .36263 | | -0.00345 | 3.45e-3 | 3.45 * 10^-3 | 3.45 | 10 | -3 | .45 | | 0.085 | 1.36e-4 | 1.36 * 2^-4 | 1.36 | 2 | -4 | .36 |
32位的單精度浮點數 與 64位的雙精度浮點數的差異
| 精度 | 符號位 | 指數位 | 小數位 |偏移量| |------------------|--------|------------|---------------|------| | Single (32 Bits) | 1 [31] | 8 [30-23] | 23 [22-00] | 127 | | Double (64 Bits) | 1 [63] | 11 [62-52] | 52 [51-00] | 1023 |
符號位:1 爲 負數, 0 爲正數。
指數位:存儲 指數減去偏移量,偏移量是爲了表達負數而設計的。
小數位:存儲係數的小數位的準確或者最接近的值。
以 數字 0.085 爲例。
| 符號位 | 指數位(123) | 小數位 (.36) | |------|----------------|------------------------------| | 0 | 0111 1011 | 010 1110 0001 0100 0111 1011 |
小數位的計算
以0.36 爲例: 010 1110 0001 0100 0111 1011 = 0.36 (第一位數字代表1/2,第二位數字是1/4 …)
分解後的計算步驟爲:
| Bit | Value | Fraction | Decimal | Total | |-----|---------|-----------|------------------|------------------| | 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 |
go語言顯示浮點數 - 驗證之前的理論
math.Float32bits 可以爲我們打印出數字的二進制表示。
下面的go代碼輸出0.085的二進制表達。
爲了驗證之前理論的正確性,根據二進制表示反向推導出其所表示的原始十進制0.085
package main import ( "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.085000 Bit Pattern: 0 | 0111 1011 | 010 1110 0001 0100 0111 1011 Sign: 0 Exponent: 123 (-4) Mantissa: 0.360000 Value: 0.085000
經典問題:如何判斷一個浮點數其實存儲的是整數
思考10秒鐘….
下面是一段判斷浮點數是否爲整數的go代碼實現,我們接下來逐行分析函數。它可以加深對於浮點數的理解
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)) 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") }
要保證是整數,一個重要的條件是必須要指數位大於127,如果指數位爲127,代表指數爲0. 指數位大於127,代表指數大於0, 反之小於0.下面我們以數字234523爲例子:
Starting Number: 234523.000000 Bit Pattern: 0 | 1001 0000 | 110 0101 0000 0110 1100 0000 Sign: 0 Exponent: 144 (17) Mantissa: 0.789268 Value: 234523.000000 Exponent: -6 Coefficient: 15009472 IntTest: 0 INTEGER
第一步,計算指數。由於 多減去了23,所以在第一個判斷中 判斷條件爲 exponent < -23
exponent := int(bits >> 23) - bias - 23
第二步,(bits & ((1 << 23) - 1)) 計算小數位。
coefficient := (bits & ((1 << 23) - 1)) | (1 << 23) Bits: 01001000011001010000011011000000 (1 << 23) - 1: 00000000011111111111111111111111 bits & ((1 << 23) - 1): 00000000011001010000011011000000
`| (1 << 23)`` 代表 將1加在前方。
bits & ((1 << 23) - 1): 00000000011001010000011011000000 (1 << 23): 00000000100000000000000000000000 coefficient: 00000000111001010000011011000000
1 + 小數 = 係數。
第三步,計算intTest 只有當指數的倍數可以彌補最小的小數位的時候,纔是一個整數。如下,指數是17位,其不能夠彌補最後6位的小數。即不能彌補1/2^18 的小數。由於2^18位之後爲0.所以是整數。
exponent: (144 - 127 - 23) = -6 1 << uint32(-exponent): 000000 (1 << uint32(-exponent)) - 1: 111111 coefficient: 00000000111001010000011011000000 1 << uint32(-exponent)) - 1: 00000000000000000000000000111111 intTest: 00000000000000000000000000000000
擴展閱讀:概念:Normal number and denormal (or subnormal) number
wiki的解釋是:
In computing, a normal number is a non-zero number in a floating-point representation which is within the balanced range supported by a given floating-point format: it is a floating point number that can be represented without leading zeros in its significand.
什麼意思呢?在IEEE-754中指數位有一個偏移量,偏移量是爲了表達負數而設計的。比如單精度中的0.085,實際的指數是 -3, 存儲到指數位是123。
所以表達的負數就是有上限的。這個上限就是2^-126。如果比這個負數還要小,例如2^-127,這個時候應該表達爲0.1 * 2 ^ -126. 這時係數變爲了不是1爲前導的數,這個數就叫做denormal (or subnormal) number。
正常的係數是以1爲前導的數就叫做Normal number。
擴展閱讀:概念:精度
精度是一個非常複雜的概念,在這裏筆者討論的是2進制浮點數的10進制精度。
精度爲d表示的是在一個範圍內,如果我們將d位10進制(按照科學計數法表達)轉換爲二進制。再將二進制轉換爲d位10進制。數據不損失意味着在此範圍內是有d精度的。
精度的原因在於,數據在進制之間相互轉換時,是不能夠精準匹配的,而是匹配到一個最近的數。
在這裏暫時不深入探討,而是給出結論:
float32的精度爲6-8位,
float64的精度爲15-17位
並且精度是動態變化的,不同的範圍可能有不同的精度。這裏簡單提示一下是由於 2的冪 與 10的冪之間的交錯是不同的。
總結
本文介紹了go語言使用的IEEE-754標準存儲浮點數的具體存儲方式。
本文通過實際代碼片段和一個腦筋急轉彎幫助讀者理解浮點數的存儲方式。
本文介紹了normal number 以及精度這兩個重要概念。
參考資料