当前位置: 首页 > news >正文

php ieee754,在 Go 中探索 IEEE-754 标准

在六月份时,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 * 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 |

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

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

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

本文由 GCTT 原创翻译,Go语言中文网 首发。也想加入译者行列,为开源做一些自己的贡献么?欢迎加入 GCTT!

翻译工作和译文发表仅用于学习和交流目的,翻译工作遵照 CC-BY-NC-SA 协议规定,如果我们的工作有侵犯到您的权益,请及时联系我们。

欢迎遵照 CC-BY-NC-SA 协议规定 转载,敬请在正文中标注并保留原文/译文链接和作者/译者等信息。

文章仅代表作者的知识和看法,如有不同观点,请楼下排队吐槽

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

相关文章:

  • oracle+数据结构+字典,ORACLE 数据字典
  • liunx php libiconv-1.14 扩展库地址,编译安装php之安装libiconv-1.14.tar.gz出错解决方法...
  • Oracle标量子查询执行计划,性能为王:SQL标量子查询的优化案例分析
  • oracle insert和update,INSERTUPDATE和MERGE (转)
  • 有关oracle的论文,Oracle数据库研究论文有关Oracle数据库的论文
  • linux rocketmq 命令,Linux下安装RocketMQ(单Master)
  • linux 查看 文件夹代销,速达常见问题集
  • 斑马打印机linux驱动安装教程,热敏打印机安装驱动教程
  • linux串口设置1843200波特率,串口通信程序设计
  • linux安装sw软件,Linux 下安装python软件包(pip、nose、virtualenv、distribute )
  • linux绕过时间检测,使用linux的clockdiff命令检测两台linux主机的时间差
  • linux思源黑体乱码,deepin终端使用思源黑体做中文字体
  • linux保存python,python中import this 编程之禅的结果我想用一个变量保存,如何做
  • redhat linux yum仓库,RedHat 6.6 yum仓库的使用
  • linux spf13 vim安装,spf13-vim - Vim编辑器的终极版本
  • 0基础学习移动端适配
  • JavaScript函数式编程(一)
  • Linux Process Manage
  • Nacos系列:Nacos的Java SDK使用
  • SAP云平台里Global Account和Sub Account的关系
  • SpingCloudBus整合RabbitMQ
  • SQLServer之创建数据库快照
  • 坑!为什么View.startAnimation不起作用?
  • 什么软件可以剪辑音乐?
  • 使用Swoole加速Laravel(正式环境中)
  • 这几个编码小技巧将令你 PHP 代码更加简洁
  • Linux权限管理(week1_day5)--技术流ken
  • Mac 上flink的安装与启动
  • 选择阿里云数据库HBase版十大理由
  • ​LeetCode解法汇总1276. 不浪费原料的汉堡制作方案
  • #etcd#安装时出错
  • #vue3 实现前端下载excel文件模板功能
  • (DFS + 剪枝)【洛谷P1731】 [NOI1999] 生日蛋糕
  • (ISPRS,2023)深度语义-视觉对齐用于zero-shot遥感图像场景分类
  • (二)Eureka服务搭建,服务注册,服务发现
  • (附源码)springboot车辆管理系统 毕业设计 031034
  • (七)Knockout 创建自定义绑定
  • (四)c52学习之旅-流水LED灯
  • .gitignore文件—git忽略文件
  • .NET CF命令行调试器MDbg入门(一)
  • .net6Api后台+uniapp导出Excel
  • .net中我喜欢的两种验证码
  • :=
  • [ 转载 ] SharePoint 资料
  • []新浪博客如何插入代码(其他博客应该也可以)
  • [BZOJ2281][SDOI2011]黑白棋(K-Nim博弈)
  • [Bzoj4722]由乃(线段树好题)(倍增处理模数小快速幂)
  • [CareerCup] 14.5 Object Reflection 对象反射
  • [CISCN 2019华东南]Web11
  • [Java][Android][Process] 暴力的服务能够解决一切,暴力的方式运行命令行语句
  • [LeetCode] 197. 上升的温度
  • [Linux] Boot分区满了的处理方法 The volume boot has only 0 bytes disk space remaining
  • [Linux]进程信号(信号入门 | 信号产生的方式 | 信号捕捉初识)
  • [THUPC 2024 初赛] 二进制 (树状数组单点删除+单点查询)(双堆模拟set)
  • [word] word艺术字体如何设置? #知识分享#职场发展#媒体