浮点数

引言

在 c 以及一些语言中,为什么 0.1 + 0.2 = 0.30000000000000004?

IEEE784

在1985年,由Intel公司赞助,由William kahan制定的表示浮点数及其运算的标准,目前,所有计算机都支持该标准。

  1. IEEE浮点表示
    v = (-1)s * M * 2E
    • s是符号位,共一位,1为负数,0为正数,而数值0的符号位另外说明。
    • E是比例因子的指数,称为浮点数的指数,是一个整数,也称为阶码。单精度(32位)有8位,双精度(64位)有11位。
    • M称为浮点数的尾数,是一个纯小数,单精度(32位)有23位,双精度(64位)有52位。
32位浮点数表示
  1. 浮点数的存储(以32位为例)


    浮点数存储
    • S:首位,符号位 。

    • E :阶码,用移码表示(将负数和正数都移动到正数区间,在浮点数比较的时候就可以免除符号位的比较,简化了操作),且移码为127(单精度),1023(双精度)。计算机中存储的都是阶码的移码,比如e为3时,实际存储E=3+127=130的二进制表示。

      为什么移码为127(1023)?
      1、8位移码的取值范围为0-255(00000000-11111111),但在浮点数的阶码中,全0(表示0与接近0的数)与全1(表示无穷与NAN)被保留用作特殊情况,所以阶码可用范围只有1~254,总共有254个值。
      2、8位有符号数取值范围为(-128)-(+127)(10000000~01111111),这里的二进制用补码表示,其中特别规定补码10000000没有原码,为-128的补码,总共有256个值。
      3、按照移码的一般习惯,如果采用偏置2(k-1) (27=128),在表达+127时会产生上溢(移码11111111被保留),所以在阶码中偏置为(128-1),与此同时,在表达-127时会产生下溢(移码00000000被保留),所以阶码中去掉-127与-128,取值范围为(-126~127)->(1-254),总共254个值。

    • M:尾数,IEEE规定,当尾数的值不为0时,尾数域的最高有效位应为1,这称为浮点数的规格化表示。否则以修改阶码同时左右移动小数点位置的办法,使其变成规格化数的形式,。

      如果是一个规格化(阶码不是全0或全1)的浮点数,其中尾数域所表示的值必然是1.M,为了提高精度,又由于规格化的浮点数的尾数域最左位(最高有效位)总是1,故这一位经常不予存储,而认为隐藏在小数点的左边。于是用23位字段可以存储24位有效数。 所以float类型的十进制的精度为log10(224)≈7位,同理double类型精度为log10(253)≈15位

  2. 浮点数的取值范围
    根据阶码的值,将浮点数的编码值分成三种,分别是规格化值、非规格化值、无穷大(这种情况包含NaN)。

    • 规格化值:阶码不为0或255(1-254)。

    当阶码和尾数都取最小时(E=1,M=0),表示的数值最小,阶码部分为 1 − 127 = − 126 ,尾数部分为 隐含的1加上其余的23位0,最小正数值为1*2-126。当阶码和尾数都取最大时(E=254,M全1),表示的数值最大,阶码部分为 254 − 127 = 127,尾数部分为 隐含的1加上其余的23位1,最大正数为(2−2 −23 )×2 127

    格式 正数 负数
    单精度 2-126到(2−2 −23 )×2 127 -2-126到-(2−2 −23 )×2 127
    双精度 2-1022到(2−2 −52 )×2 1023 -2-1022到-(2−2 −52 )×2 1023
    • 非规格化值,IEEE规定,当阶码全为0时,为非规格化值,此时的尾数为本身值,不必像规格化值去缺省加上开头的1,这样做是为了能表示0值,所以我们可以看出,当尾数全为0时,阶码全为0是,这就是0值(包括+0和-0)。

    为了提供一种规格化值到非规格化值的平滑过渡,IEEE规定非规格化值的阶码为1-移码(127或1023),所以当尾数全为1时最大值为(1-2-23)2-126,仅仅比规格化数的值小了一个能够表示的最小值(2-23),这就是开头提到的平滑过渡。故而非规格化值范围是
    -(1-2-23)
    2-126到(1-2-23)*2-126

    • 当阶码全为1时,表示特殊值。

    如果尾数全为0,表示无穷大,如果位数不为0,表示NaN。

    • 非负浮点数值范围如下所示:


      非负浮点数示例
  3. 浮点数的舍入。
    因为表示方式限制了浮点数的范围和精度,所以在精度溢出时,需要做舍入操作。IEEE设计了四种舍入方式:

    • 向零舍入,也就是向数轴0方式舍入,比如1.5舍入为1,-1.5舍入为-1。
    • 向下舍入,往值更小的方向舍入。
    • 向上舍入,往值更大的方向舍入。
    • 向偶舍入(round-to-even),也叫向最接近的值舍入,IEEE默认采用这种舍入方式。

    舍入规则是向最接近的数舍入,当数处于中间值是,向尾数是偶数的方式舍入,比如我们要舍入到两位有效数的二进制
    10.001102 => 10.012 (2位有效数字后为110,超过了一半(一半为100),故向最近(上)舍入),类似1.6舍入为2。
    10.000112 => 10.002 (2位有效数字后为011,没有超过一半(一半为100),故向最近(下)舍入),类似1.4舍入为1。
    10.111002 => 11.002 (2位有效数字后为100(中间值),,故向偶舍入(使得最后一位为偶数0),这里选择向上舍入,类似1.5舍入为2。
    10.101002 => 10.102 (2位有效数字后为100(中间值),,故向偶舍入(使得最后一位为偶数0),这里选择向下舍入,类似2.5舍入为2。

    这种向最近值舍入的方式减小了舍入数据的误差,例如,当我们舍入一组数据时,采用向上舍入的方式会使得这组数据偏大,向下舍入则会使得数据偏小,而向偶舍入可以减小误差。

  4. 十进制数的浮点表示示例(100.25)。

    • 进制转换:100.2510 = 1100100.012
    • 规格化处理:1100100.01 = 1.10010001×26
    • 计算阶码:E = 6 +127 = 133 (10000101)
    • 符号位0,尾数10010001000000000000000(缺省首位1)
    • 二进制表示:0 10000101 10010001000000000000000
  5. 浮点数的计算
    浮点数的加减运算一般由以下五个步骤完成:对阶、尾数运算、规格化、舍入处理、溢出判断

    • 对阶:是指将两个进行运算的浮点数的阶码对齐的操作,对阶的目的是为使两个浮点数的尾数能够进行加减运算,对阶的原则是小阶对大阶。即小阶加上差值,并尾数右移相应的尾数保存数值不变。
    • 尾数计算:对阶后的尾数相加减。
    • 规格化:计算完成的数转换成规格化浮点数,左规操作:将尾数左移,同时阶码减值,直至尾数成为1.M的形式,右规操作:将尾数右移1位,同时阶码增1,便成为规格化的形式了。
    • 舍入处理:在对阶或右规时,尾数需要右移,被右移出去的位会被丢掉,从而造成运算结果精度的损失。为了减少这种精度损失,可以将一定位数的移出位先保留起来,称为保护位,在规格化后用于舍入处理(采用向偶舍入)。
    • 溢出判断:若阶码的值超过了阶码所能表示的最大正数,则为上溢,进一步,若此时浮点数为正数,则为正上溢,记为+∞,若浮点数为负数,则为负上溢,记为-∞;若阶码的值超过了阶码所能表示的最小负数,则为下溢,进一步,若此时浮点数为正数,则为正下溢,若浮点数为负数,则为负下溢。正下溢和负下溢都作为0处理。
  6. 0.1+0.2=0.30000000000000004

    • 将二进制的0.1和0.2转换成指数形式
      十进制:0.1
      二进制形式:0.00011001100110011001100(1100循环)
      指数形式:1.10011001100110011......2-4
      二进制表示:0-01111111011-1001100110011001100110011001100110011001100110011010
      十进制:0.2
      二进制形式:0.001100110011001100110011......
      指数形式:1.100110011001100110011......
      2-3
      二进制表示:0-01111111100-1001100110011001100110011001100110011001100110011010

双精度数尾数保留52位,所以无限循环小数需要做舍入,只保留52位小数(就是第53位以后全部舍弃,第53位“0舍1入”,而当第52位为1时,还得再进一位)

对阶相加得到:10.0100100100100100100100100100100......100111,但是此时得到的数指数依然是指数形式,要将它转换成为二进制形式,就是将小数点向前移三位变成0.01001001001001001001001.....00111

然后将这个数转换为十进制数就是0.30000000000000004