49

在 Go 中探索 IEEE-754 标准

 5 years ago
source link: https://studygolang.com/articles/14407?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在六月份时,Gustavo Niemeyer 在他的博客 Labix.org 上提出了下面这个问题:

假设 uf 是一个无符号的 64 位整型,但包含的内容却是符合 IEEE-754 标准的二进制浮点数,你怎么去区分 uf 是表示整型还是浮点型呢?

由于我对这方面的了解并不是很多,所以无法快速得出这个问题的答案,而且我也无法用语言来向你解释,只能通过写一个相关的程序来进行示范。幸运的是 Gustavo 发布了答案的思路,我认为非常有意思,然后我将思路进行了分解,这样能更好地理解最初的那个问题。为了更通俗易懂,后面的示例我将使用 32 位的数字。

IEEE-754 标准是如何将一个浮点数字储存为二进制格式的呢?最开始我找到了下面两篇论文:

http://steve.hollasch.net/cgindex/coding/ieeefloat.html http://class.ece.iastate.edu/arun/CprE281_F05/ieee754/ie5.html

IEEE-754 规范用来表示一个特定二进制格式的浮点数,它是一种以 2 为底的科学计数法,如果你对我所说的 “二进制格式” 感到疑惑的话,那么你应该去读一下我以前发布的这篇《理解 Go 语言的类型》。

科学计数法能非常高效地表达极大或极小的数字,它使用小数和乘法来进行表示,下面是几个示例:

| 十进制 | 科学计数法 | 计算式 | 系数 | 底数 | 指数 | 尾数 | | :------------ | :--------- | :------------ | :------ | :--: | :--: | :----: | | 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 |

正常情况下科学计数法要求小数点左边要有一个数字,十进制时这个数字在 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 -4 | 1.36 | 2 | -4 | .36 |

我们需要用 2 的若干次方除以 10 进制的数字(0.085)来获得一个以 1 开头的分数,“以1开头的分数”是什么意思呢?在示例中我们需要一个看上去像是系数的值 1 + .36。IEEE-754 标准中需要系数以 "1." 开头,这让我们必须储存尾数部分并且需要额外的比特位来表示精度。

下面我们将采用一种暴力的方法,你会看到最终会得到代表着 0.085 并且以 1 开头的分数:

0.085 / 2 -1 = 0.17

0.085 / 2 -2 = 0.34

0.085 / 2 -3 = 0.68

0.085 / 2 -4 = 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 -4 ,因此指数为 -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:

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

如果你将输出中的位模式和之前的示例对比一下,那么你会发现它们是一致的,所以我们之前所学习的 IEEE-754 都是正确的。

现在我们可以回答 Gustavo 的问题了,我们如何区分一个整型中的内容呢?下面是一个能检测整型是否被储存为 IEEE-754 格式的函数,感谢 Gustavo 的代码。

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")
}

那么这个函数是如何进行检测的呢?让我们先将指数小于 -23 作为首要条件进行测试,如果用 1 作为我们的测试值,那么指数和偏移值都是 127,这意味着我们用指数减去偏移值会得到 0。

Starting Number: 1.000000

Bit Pattern: 0 | 0111 1111 | 000 0000 0000 0000 0000 0000

Sign: 0 Exponent: 127 (0) Mantissa: 0.000000 Value: 1.000000


Exponent: -23 Coefficient: 8388608 IntTest: 0
INTEGER

在测试函数中减去了一个额外的 23,它表示 IEEE-754 格式中指数的比特位开始的位置,这也就是为什么你会看到测试函数中会出现 -23 这个指数值。

| 精度 | 标志位 | 指数 | 分数位 | 偏移值 | | :--------------: | :----: | :-----------: | :--------: | :----: | | Single (32 Bits) | 1 [31] | 8 [30- 23 ] | 23 [22-00] | 127 |

在第二个测试中必须要用到减法,所以任何小于 -23 的值必定小于 1,因此不是一个整型。

让我们用一个整型值来理解第二个测试是如何工作的,这次我们将要在代码中把这个值设置为 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

第二个测试通过两个条件来识别这个数字是否为整型,这需要用到按位运算,下面提供了一个演示函数,让我们看一下其中的算法:

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:          00000000011111111111111111111111
bits & ((1 << 23) - 1): 00000000011001010000011011000000

第一部分的系数计算中从 IEEE-754 位模式中移除了符号位和指数位。

第二部分的计算中会把 “1 +” 加入到位模式中。(注:看图就会明白,就是分数位最高位的前一位进行了或操作变为1)

coefficient := (bits & ((1 << 23) - 1)) | (1 << 23)

bits & ((1 << 23) - 1): 00000000011001010000011011000000
(1 << 23):              00000000100000000000000000000000
coefficient:            00000000111001010000011011000000

到了这时系数就已经确定了,我们用这个 intTest 函数来计算一下:

exponent := int(bits >> 23) - bias - 23
intTest := (coefficient & ((1 << uint32(-exponent)) - 1))

exponent:                     (144 - 127 - 23) = -6
1 << uint32(-exponent):       000000
(1 << uint32(-exponent)) - 1: 111111

coefficient:                 00000000111001010000011011000000
1 << uint32(-exponent)) - 1: 00000000000000000000000000111111
intTest:                     00000000000000000000000000000000

我们通过测试函数计算出的指数值通常用来 确定 下一步中用来比较系数值。在这个例子中指数值为 -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

如果没有 Gustavo 的代码,我可能一辈子都找不到解决方法,下面是他解决方法的代码链接:

http://bazaar.launchpad.net/~niemeyer/strepr/trunk/view/6/strepr.go#L160

你也可以复制粘贴下面的副本代码:

package main

import (
    "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 提出这个问题并且让我彻底理解了其中一些知识。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK