在 Go 中探索 IEEE-754 标准

maxwell365 · · 2400 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

在六月份时,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语言中文网 荣誉推出


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

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

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