在六月份时,Gustavo Niemeyer 在他的博客 [Labix.org](http://blog.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 语言的类型》](https://studygolang.com/articles/13976)。
科学计数法能非常高效地表达极大或极小的数字,它使用小数和乘法来进行表示,下面是几个示例:
| 十进制 | 科学计数法 | 计算式 | 系数 | 底数 | 指数 | 尾数 |
| :------------ | :--------- | :------------ | :------ | :--: | :--: | :----: |
| 700 | 7e+2 | 7 * 10<sup>2</sup> | 7 | 10 | 2 | 0 |
| 4,900,000,000 | 4.9e+9 | 4.9 * 10<sup>9</sup> | 4.9 | 10 | 9 | .9 |
| 5362.63 | 5.36263e+3 | 5.36263 * 10<sup>3</sup> | 5.36263 | 10 | 3 | .36263 |
| -0.00345 | 3.45e-3 | 3.45 * 10<sup>-3</sup> | 3.45 | 10 | -3 | .45 |
| 0.085 | 1.36e-4 | 1.36 * 2<sup>-4</sup> | 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<sup>-4</sup> | 1.36 | 2 | -4 | .36 |
我们需要用 2 的若干次方除以 10 进制的数字(0.085)来获得一个以 1 开头的分数,“以1开头的分数”是什么意思呢?在示例中我们需要一个看上去像是系数的值 1 + .36。IEEE-754 标准中需要系数以 "1." 开头,这让我们必须储存尾数部分并且需要额外的比特位来表示精度。
下面我们将采用一种暴力的方法,你会看到最终会得到代表着 0.085 并且以 1 开头的分数:
> 0.085 / 2<sup>-1</sup> = 0.17
>
> 0.085 / 2<sup>-2</sup> = 0.34
>
> 0.085 / 2<sup>-3</sup> = 0.68
>
> 0.085 / 2<sup>-4</sup> = 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<sup>-4</sup>,因此指数为 -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:
```go
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 的代码。
```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")
}
```
那么这个函数是如何进行检测的呢?让我们先将指数小于 -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
```
第二个测试通过两个条件来识别这个数字是否为整型,这需要用到按位运算,下面提供了一个演示函数,让我们看一下其中的算法:
```go
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](https://www.ardanlabs.com/blog/broken-link.html)
如果没有 Gustavo 的代码,我可能一辈子都找不到解决方法,下面是他解决方法的代码链接:
http://bazaar.launchpad.net/~niemeyer/strepr/trunk/view/6/strepr.go#L160
你也可以复制粘贴下面的副本代码:
```go
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语言中文网 首发。也想加入译者行列,为开源做一些自己的贡献么?欢迎加入 GCTT!
翻译工作和译文发表仅用于学习和交流目的,翻译工作遵照 CC-BY-NC-SA 协议规定,如果我们的工作有侵犯到您的权益,请及时联系我们。
欢迎遵照 CC-BY-NC-SA 协议规定 转载,敬请在正文中标注并保留原文/译文链接和作者/译者等信息。
文章仅代表作者的知识和看法,如有不同观点,请楼下排队吐槽
有疑问加站长微信联系(非本文作者))