• IEEE-754标准float类型在内存中的存储原理


    golang 与其他很多语言(C、C++、Python…)一样,使用了IEEE-754标准存储浮点数。

    1. IEEE-754 如何存储浮点数

    1.1 存储原理

    32位的浮点数 与 64位浮点数的差异

    精度符号位指数位小数位偏移量
    32 bit1[31]8[30-23]23[22-0]127
    64 bit1[63]11[62-52]52[51-0]1023

    1.1.1 十进制浮点数的2进制表示

    首先我们来看十进制浮点数是如何转换为二进制的

    十进制浮点数二进制浮点数计算方式二进制科学计数法
    3.511.1 3.5 = 2 1 + 2 0 + 2 − 1 3.5 = 2^{1} + 2^{0} + 2^{-1} 3.5=21+20+21 1.11 × 2 1 1.11 \times 2^{1} 1.11×21
    0.50.1 0.5 = 2 − 1 0.5 = 2^{-1} 0.5=21 1.0 × 2 − 1 1.0 \times 2^{-1} 1.0×21
    0.250.01 0.25 = 2 − 2 0.25 = 2^{-2} 0.25=22 1.0 × 2 − 2 1.0 \times 2^{-2} 1.0×22
    0.1250.001$ 0.125 = 2^{-3} $ 1.0 × 2 − 3 1.0 \times 2^{-3} 1.0×23
    0.06250.0001 0.0625 = 2 − 4 0.0625 = 2^{-4} 0.0625=24 1.0 × 2 − 4 1.0 \times 2^{-4} 1.0×24
    0.80.11001100… 0.8 = 2 − 1 + 2 − 2 + 2 − 5 + 2 − 6 + . . . 0.8 = 2^{-1} + 2^{-2} + 2^{-5} + 2^{-6} + ... 0.8=21+22+25+26+... 1.1001100... × 2 − 1 1.1001100... \times 2^{-1} 1.1001100...×21

    从上面我们可以观察到,对于任何数来说,表示成二进制科学计数法后,都成以转换成 1.xxx(尾数) * 2 的 n 次方(指数)。

    另外如上图中的十进制小数0.8,表示成二进制后变成了以1100循环的无限循环小数。这便是浮点数有精度问题的根源之一,在代码中声明的小数0.8,计算机底层其实是无法精确存储那个无限循环的二进制数的。只能存储一个0舍1入后的近似值

    同样的对于负数来说可以表示为 -1.xxx(尾数) * 2 的 n 次方(指数), 所以内存中要存储这个小数可以拆成三个部分存储:

    • 正负号
    • 尾数(因为使用二进制科学计数法表示后首位必然是1所以可以省略掉只存尾数)
    • 指数

    1.1.2 十进制浮点数到二进制的转换过程

    我们如何快速将一个十进制浮点数转成二进制浮点数呢? 下面我们以3.66为例进行演示

    // 先将数拆成整数位和小数位
    // 整数位转换方法同整数
    3                 --> 11
    .66               --> .
    // 用剩余的小数乘以2, 然后提取结果的整数位, 直到结果为1或超出存储上限
    0.66 * 2 = 1.32   --> 1
    0.32 * 2 = 0.64   --> 0
    0.64 * 2 = 1.28   --> 1
    0.28 * 2 = 0.56   --> 0
    0.56 * 2 = 1.12   --> 1
    0.12 * 2 = 0.24   --> 0
    0.24 * 2 = 0.48   --> 0
    0.48 * 2 = 0.96   --> 0
    0.96 * 2 = 1.92   --> 1
    0.92 * 2 = 1.84   --> 1
         .
         .
         .
    // 所以3.66转换为二进制浮点数为
    11.1010100011...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    正如上述过程所示, 浮点数的整数部分按照整数的方式直接转成二进制, 小数部分乘以2后提取整数部分剩余部分继续乘2并提取整数部分, 重复执行此步骤知道结果为1或达到我们能表示的最大精度

    1.2 32位浮点数在内存中的具体存储方式

    1.2.1 规约形式的浮点数

    以0.8为例 1.1001100... × 2 − 1 1.1001100... \times 2^{-1} 1.1001100...×21, 其具体存储方式如下

    // golang 打印浮点数的二进制表示方法
    package main
    
    import (
    	"fmt"
    	"math"
    )
    
    func main() {
    	fmt.Println(fmt.Sprintf("%032b", math.Float32bits(0.8)))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    符号位指数位小数位(尾数)
    00111 1110100 1100 1100 1100 1100 1101
    • 符号位: 0表示正数, 1表示负数

    • 指数位: 01111110 对应的值是126, 那么为什么本应该是-1的值却是126呢?

      • 这就是所谓的指数偏差: 指数偏差(表示法中的指数为实际指数减掉某个值)为 ,其中的e为存储指数的比特的长度。减掉一个值因为指数必须是有号数才能表达很大或很小的数值,但是有号数通常的表示法——补码(two’s complement),将会使比较变得困难。为了解决这个问题,指数在存储之前需要做偏差修正,将它的值调整到一个无符号数的范围内以便进行比较。此外,指数采用这种方法表示的优点还在于使得浮点数的正规形式和非正规形式之间有了一个平滑的转变。
      • 指数偏移值(exponent bias),是指浮点数表示法浮点数表示法中的指数域的编码值为指数的实际值加上某个固定的值,IEEE 754标准规定该固定值为 2 e − 1 − 1 2^{e-1} - 1 2e11,其中的 e e e为存储指数的比特的长度。
      • 对于32位浮点数指数域是8bit, 固定偏移值是 2 8 − 1 − 1 = 127 2^{8-1} - 1 = 127 2811=127, 所以-1在指数域的表示就是 − 1 + 127 = 126 -1 + 127 = 126 1+127=126
    • 尾数: 也就是二进制科学计数法小数点右侧的部分, 32位浮点数尾数最长23bit

    1.2.2 非规约形式的浮点数

    我们思考一个问题, 以上述我们所说的规约形式的浮点数能表示的大于0的最小浮点数是多少呢? 是 2 − 127 2^{-127} 2127吗?

    那我们尝试以上节讲的内容将 2 − 127 2^{-127} 2127表示出来试一下

    符号位指数位小数位
    0 − 127 + 127 = 0 -127 + 127 = 0 127+127=0 1.0 ∗ 2 − 127 1.0 * 2^{-127} 1.02127
    00000 0000000 0000 0000 0000 0000 0000

    我们发现所有位都成为了0, 而在浮点数中0的表示方法就是所有位都是0

    实际在IEEE 754标准规定中最小的规约形式的单精度浮点数的指数部分编码值为1, 对于32位浮点数而言指数的实际值为-126, 也就是说规约形式的浮点数能表示的大于0的最小浮点数是 2 − 126 2^{-126} 2126, 将指数部分编码值为0的部分用于表示非规约形式的浮点数

    ​ 以下摘录自百度百科对非规约形式的浮点数的介绍


    如果浮点数的指数部分的编码值是0,分数部分非零,那么这个浮点数将被称为非规约形式的浮点数。一般是某个数字相当接近零时才会使用非规约型式来表示。 IEEE 754标准规定:非规约形式的浮点数的指数偏移值比规约形式的浮点数的指数偏移值小1。例如,最小的规约形式的单精度浮点数的指数部分编码值为1,指数的实际值为-126;而非规约的单精度浮点数的指数域编码值为0,对应的指数实际值也是-126而不是-127。实际上非规约形式的浮点数仍然是有效可以使用的,只是它们的绝对值已经小于所有的规约浮点数的绝对值;即所有的非规约浮点数比规约浮点数更接近0。规约浮点数的尾数大于等于1且小于2,而非规约浮点数的尾数小于1且大于0。


    除了规约浮点数,IEEE754-1985标准采用非规约浮点数,用来解决填补绝对值意义下最小规格数与零的距离。(举例说,正数下,最大的非规格数等于最小的规格数。而一个浮点数编码中,如果exponent=0,且尾数部分不为零,那么就按照非规约浮点数来解析)非规约浮点数源于70年代末IEEE浮点数标准化专业技术委员会酝酿浮点数二进制标准时,Intel公司对渐进式下溢出(gradual underflow)的力荐。当时十分流行的DECVAX机的浮点数表示采用了突然式下溢出(abrupt underflow)。如果没有渐进式下溢出,那么0与绝对值最小的浮点数之间的距离(gap)将大于相邻的小浮点数之间的距离。例如单精度浮点数的绝对值最小的规约浮点数是$1.0 \times 2^{-126} $,它与绝对值次小的规约浮点数之间的距离为 2 − 126 × 2 − 23 = 2 − 149 2^{-126} \times 2^{-23} = 2^{-149} 2126×223=2149。如果不采用渐进式下溢出,那么绝对值最小的规约浮点数与0的距离是相邻的小浮点数之间距离的 2 23 2^{23} 223倍!可以说是非常突然的下溢出到0。这种情况的一种糟糕后果是:两个不等的小浮点数X与Y相减,结果将是0.训练有素的数值分析人员可能会适应这种限制情况,但对于普通的程序员就很容易陷入错误了。采用了渐进式下溢出后将不会出现这种情况。例如对于单精度浮点数,指数部分实际最小值是(-126),对应的尾数部分从 1.1111...11 1.1111...11 1.1111...11 , 1.1111...10 1.1111...10 1.1111...10一直到 0.0000...10 , 0.0000...01 , 0.0000...00 0.0000...10, 0.0000...01, 0.0000...00 0.0000...10,0.0000...01,0.0000...00相邻两小浮点数之间的距离(gap)都是 2 − 126 × 2 − 23 = 2 − 149 2^{-126} \times 2^{-23} = 2^{-149} 2126×223=2149;而与0最近的浮点数(即最小的非规约数)也是 2 − 126 × 2 − 23 = 2 − 149 2^{-126} \times 2^{-23} = 2^{-149} 2126×223=2149

    这里有三个特殊值需要指出

    符号位指数位小数位实际值
    *100 ± 0 \pm 0 ±0
    *1 2 e − 1 2^{e} - 1 2e10 ± ∞ \pm \infty ±
    *1 2 e − 1 2^{e} - 1 2e1非0NaN

    正如我们本小节开头的例子 2 − 127 2^{-127} 2127不再以 1.0 ∗ 2 − 127 1.0 * 2^{-127} 1.02127表示而是表示为 0.1 ∗ 2 − 126 0.1 * 2^{-126} 0.12126, 同理 2 − 130 2^{-130} 2130表示为 0.0001 ∗ 2 − 126 0.0001 * 2^{-126} 0.00012126, 因为首位必然是0所以存储的时候依然只存储尾数部分, 比如 2 − 130 2^{-130} 2130将存储为

    符号位指数位小数位(尾数)
    00000 0000000 1000 0000 0000 0000 0000

    1.2.3 32位浮点数各种极值情况

    类别正负号实际指数有偏移指数指数域尾数域数值
    00-12700000 0000000 0000 0000 0000 0000 00000.0
    -01-12700000 0000000 0000 0000 0000 0000 0000−0.0
    1001270111 1111000 0000 0000 0000 0000 00001.0
    -1101270111 1111000 0000 0000 0000 0000 0000−1.0
    最小的非规约数*1-12700000 0000000 0000 0000 0000 0000 0001 ± 2 − 126 × 2 − 23 ≈ ± 1.4 × e − 45 \pm 2^{-126} \times 2^{-23} \approx \pm 1.4 \times e^{-45} ±2126×223±1.4×e45
    中间大小的非规约数*1-12700000 0000100 0000 0000 0000 0000 0000 ± 2 − 127 ≈ ± 5.88 × e − 39 \pm 2^{-127} \approx \pm 5.88 \times e^{-39} ±2127±5.88×e39
    最大的非规约数*1-12700000 0000111 1111 1111 1111 1111 1111 ± ( 2 − 126 − 2 − 149 ) ≈ ± 1.18 × e − 38 \pm(2^{-126} - 2^{-149}) \approx \pm 1.18 \times e^{-38} ±(21262149)±1.18×e38
    最小的规约数*1-12610000 0001000 0000 0000 0000 0000 0000 ± 2 − 126 ≈ ± 1.18 × e − 38 \pm 2^{-126} \approx \pm 1.18 \times e^{-38} ±2126±1.18×e38
    最大的规约数*11272541111 1110111 1111 1111 1111 1111 1111 ± ( 2 128 − 2 105 ) ≈ ± 3.4 × e 38 \pm(2^{128} - 2^{105}) \approx \pm 3.4 \times e^{38} ±(21282105)±3.4×e38
    正无穷01282551111 1111000 0000 0000 0000 0000 0000 + ∞ + \infty +
    负无穷11282551111 1111000 0000 0000 0000 0000 0000 − ∞ - \infty
    NaN*11282551111 1111non zeroNaN

    1. * 表示可以为0或1 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  • 相关阅读:
    RFID拓展的相关问答
    Asp-Net-Core学习笔记:3.使用SignalR实时通信框架开发聊天室
    Spring Security使用总结八,Security的第二个功能授权,不同的角色访问不同的资源
    Linux进程概念
    【单链表增删查改接口的实现】
    2023去水印小程序源码修复版-前端后端内置接口+第三方接口
    【PyCharm】设置(风格 | 字体 | 模板)
    RF和SVM的特点
    webpack5基础--13_生产模式介绍
    Linxu学习——上
  • 原文地址:https://blog.csdn.net/p1049990866/article/details/126159874