在 Go 中探索 IEEE-754 标准

maxwell365 · 2018-08-24 16:14:59 · 2631 次点击 · 预计阅读时间 14 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2018-08-24 16:14:59 的文章,其中的信息可能已经有所发展或是发生改变。

在六月份时,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 * 102 7 10 2 0
4,900,000,000 4.9e+9 4.9 * 109 4.9 10 9 .9
5362.63 5.36263e+3 5.36263 * 103 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

我们需要设置正确的分数位来累加得到尾数,或者是足够接近尾数的值,这也是为什么有时我们会丢失一些精度。

010 1110 0001 0100 0111 1011 = (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 提出这个问题并且让我彻底理解了其中一些知识。


via: https://www.ardanlabs.com/blog/2013/08/gustavos-ieee-754-brain-teaser.html

作者:William Kennedy  译者:maxwell365  校对:polaris1119

本文由 GCTT 原创编译,Go语言中文网 荣誉推出


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2631 次点击  
加入收藏 微博
被以下专栏收入,发现更多相似内容
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传