前文
- golang快速入門[1]-go語言導論
- golang快速入門[2.1]-go語言開發環境配置-windows
- golang快速入門[2.2]-go語言開發環境配置-macOS
- golang快速入門[2.3]-go語言開發環境配置-linux
- golang快速入門[3]-go語言helloworld
- golang快速入門[4]-go語言如何編譯為機器碼
- golang快速入門[5.1]-go語言是如何運作的-連結器
- golang快速入門[5.2]-go語言是如何運作的-記憶體概述
- golang快速入門[5.3]-go語言是如何運作的-記憶體配置設定
- golang快速入門[6.1]-內建開發環境-goland詳解
- golang快速入門[6.2]-內建開發環境-emacs詳解
- golang快速入門[7.1]-項目與依賴管理-gopath
- golang快速入門[7.2]-北冥神功—go module絕技
- golang快速入門[8.1]-變量類型、聲明指派、作用域聲明周期與變量記憶體配置設定
- 唯識相鍊:golang快速入門[8.2]-自動類型推斷的秘密
前言
- 在上文中我們學習了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: %fnn", 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 %snn",
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: %fnn",
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: %dn",
exponent,
coefficient,
intTest)
if exponent < -23 {
fmt.Printf("NOT INTEGERn")
return
}
if exponent < 0 && intTest != 0 {
fmt.Printf("NOT INTEGERn")
return
}
fmt.Printf("INTEGERn")
}
- 要保證是整數,一個重要的條件是必須要指數位大于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,這個時候應該表達為
. 這時系數變為了不是1為前導的數,這個數就叫做denormal (or subnormal) number。0.1 * 2 ^ -126
- 正常的系數是以1為前導的數就叫做Normal number。
擴充閱讀:概念:精度
- 精度是一個非常複雜的概念,在這裡筆者讨論的是2進制浮點數的10進制精度。
- 精度為d表示的是在一個範圍内,如果我們将d位10進制(按照科學計數法表達)轉換為二進制。再将二進制轉換為d位10進制。資料不損失意味着在此範圍内是有d精度的。
- 精度的原因在于,資料在進制之間互相轉換時,是不能夠精準比對的,而是比對到一個最近的數。
- 在這裡暫時不深入探讨,而是給出結論:
- float32的精度為6-8位,
- float64的精度為15-17位
- 并且精度是動态變化的,不同的範圍可能有不同的精度。這裡簡單提示一下是由于 2的幂 與 10的幂之間的交錯是不同的。
總結
- 本文介紹了go語言使用的IEEE-754标準存儲浮點數的具體存儲方式。
- 本文通過實際代碼片段和一個腦筋急轉彎幫助讀者了解浮點數的存儲方式。
- 本文介紹了normal number 以及精度這兩個重要概念。
參考資料
- 項目連結
- 作者知乎
- blog
- Why 0.1 Does Not Exist In Floating-Point
- Normal number
- 7-bits-are-not-enough-for-2-digit-accuracy
- Decimal Precision of Binary Floating-Point Numbers
喜歡本文的朋友歡迎點贊分享~
唯識相鍊啟用微信交流群(Go與區塊鍊技術)
歡迎加微信:ywj2271840211