golang 快速入門 [8.3]-深入理解浮點數

weishixianglian發表於2020-03-30

golang 快速入門 [8.3]-深入理解浮點數

前文

前言

  • 在上文中我們學習了 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 與區塊鏈技術)

歡迎加微信:ywj2271840211

更多原創文章乾貨分享,請關注公眾號
  • golang 快速入門 [8.3]-深入理解浮點數
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章