golang 快速入門 [8.3]-深入理解浮點數
golang 快速入門 [8.3]-深入理解浮點數
前文
- 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: %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 以及精度這兩個重要概念。 ## 參考資料
- 專案連結
- 作者知乎
- 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
更多原創文章乾貨分享,請關注公眾號
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- 深入理解浮點數的表示
- 深入理解浮點數的運算
- 浮點數的理解
- golang快速入門(四)Golang
- JS中如何理解浮點數?JS
- C#快速入門教程(9)——浮點數、Decimal型別和數值型別轉換C#Decimal型別
- 【廖雪峰python入門筆記】整數和浮點數Python筆記
- 深入理解CSS浮動CSS
- 新手入門,如何快速理解JavaScriptJavaScript
- golang 快速入門 [3]-go 語言 helloworldGolang
- Golang快速入門:從菜鳥變大佬Golang
- golang快速入門(六)特有程式結構Golang
- Golang語言之管道channel快速入門篇Golang
- 浮點數
- golang 快速入門 [1]-go 語言導論Golang
- Golang語言檔案操作快速入門篇Golang
- 深入理解 Golang 指標Golang指標
- 深入理解golang 的棧Golang
- 深入理解golang:ContextGolangContext
- Golang Context深入理解GolangContext
- Golang interface介面深入理解Golang
- CSS 深入理解之 float 浮動CSS
- Canvas快速入門知識點Canvas
- 深入理解Java SPI之入門篇Java
- Java入門學習-深入理解集合Java
- Go 快速入門指南 - 變數Go變數
- golang 入門Golang
- 深入理解 Python 虛擬機器:浮點數(float)的實現原理及原始碼剖析Python虛擬機原始碼
- 深入理解 Golang 之 contextGolangContext
- 深入理解golang:sync.mapGolang
- Golang 物件導向深入理解Golang物件
- javascript快速入門15--節點JavaScript
- Golang入門-Golang包管理Golang
- 【GoLang 那點事】實踐 gRPC 之 GoLang 入門 HelloWord(三)GolangRPC
- golang 快速入門 [7.1]-專案與依賴管理-gopathGolang
- 浮點數小知識點
- [視訊版]-Golang深入理解GMPGolang
- 深入理解Golang之interface和reflectGolang