IEEE 754——计算机中浮点数的表示方法

楔子

#include <iostream>
int main(int, char**)
{
    std::cout.precision(20);
    float   a = 123.45678901234567890;
                // warning C4305: “初始化”: 从“double”到“float”截断
                // 也即赋值号右端是double双精度类型
                // 赋值号左部是float单精度类型
    double  b = 123.45678901234567890;
    std::cout << a << std::endl;
                // 123.456787109375
                // 123.45678 7109375
    std::cout << b << std::endl;
                // 123.45678901234568
                // 123.4567890123456 8              
    float   c = 9123.45678901234567890;
                // 9123.45703125
                // 9123.45 703125
    double  d = 9123.45678901234567890;
                // 9123.4567890123453
                // 9123.456789012345 3
    return 0;
}

侯捷老师曾说“源码之下,了无秘密”,今天我说,“原理之下,水落石出”。下面即是 float与double(C语言中最重要的原生数据类型)的最本质最根上的区别。

原理

计算机中是如何存储和表达数字的?对于整数,情况比较简单,直接按照数学中的进制转换方法处理即可,即连续除以2取余(比如十进制的10转化为二进制形式,11除以2得5 余1,5除以2得2 余1,2除以2得1 余0,1除以2得0 余1 1011 即是最终的二进制形式)。这并不是难点,真正的难点在于小数(即浮点数)是如何转换为二进制码的。

当然,从数学的角度来讲,十进制的小数可以转换为二进制小数(整数部分连续除2,小数部分连续乘2),例如125.125D=1111101.001B,但问题在于计算机根本就不认识小数点“.”,更不可能认识1111101.001B。那么计算机是如何处理小数的呢?

历史上计算机科学家们曾提出过多种解决方案,最终获得广泛应用的是 IEEE 754 标准中的方案,目前最新版的标准是 IEEE std 754-2008。该标准提出数字系统中的浮点数是对数学中的实数(小数)的近似(「数学」与「数字系统」,「近似」而非「相等」,请见 Python 中的浮点数 ),同时该标准规定表达浮点数的 0、1 序列被分为三部分(三个域):


这里写图片描述

32位单精度浮点数为例(float),其具体的转换规则是:首先把二进制小数(补码)用二进制科学计数法表示,比如上面给出的例子 1111101.001=1.111101001×26 。符号位sign表示数的正负(0为正,1为负),故此处填0。exponent表示科学计数法的指数部分,请务必注意的是,这里所填的指数并不是前面算出来的实际指数,而是等于实际指数加上一个数(指数偏移),偏移量为2^(e-1)-1,其中e是exponent的宽度(位数)。对于32位单精度浮点数,exponent宽度为8,因此偏移量为127,所以exponent的值为133,即10000101。之后的fraction表示尾数,即科学计数法中的小数部分11110100100000000000000(共23位)。因此32位浮点数125.125D在计算机中就被表示为01000010111110100100000000000000。

对于32位单精度浮点数(float),sign是1位,exponent是8位(指数偏移量是127),fraction是23位。对于64位双精度浮点数(double),sign是1为,exponent是11位(指数偏移量是1023),fraction是52位。

需要指出的是125.125D的转换结果实际上是规约形式的浮点数,即exponent的数值大于0且小于2^e-1,默认科学计数法中整数部分为1,因此尾数只保留了小数部分。但当数值非常接近于0时,可能出现exponent的数值等于0,且科学计数法中整数部分为0的情况,这就称为非规约形式的浮点数。对此IEEE std 754-2008规定:非规约形式浮点数的exponent值等于同种情况下规约形式浮点数的exponent再加1。比如exponent=1,显然这是规约形式浮点数,其实际指数应该是-126(1-127);而exponent=0,这是非规约形式浮点数,(若按照规约形式浮点数计算,其实际指数应为-127(0-127))那么根据前面提到的标准可知这个非规约形式浮点数的实际指数也是-126。所有的非规约浮点数比规约浮点数更接近0。

对于二进制小数,长度为 k 时的最大值为 12k,好比对于二进制整数,长度为 k 时的最大值为 2k1,如:

  • 一位时: 0.1B=121=.5
  • 二位时: 0.11B=122=0.75
  • 四位时: 0.1111B=124=0.9357

对于32位单精度浮点数而言,最大的非规约数是 (1223)21261.181038 ,最小的非规约数是 2232126=21491.401045 。对于 64 为双精度浮点数而言,最大的非规约数是 (1252)210222.2210308 ,最大的非规约数是 252210224.9410324

由上面的内容可以知道,浮点数能表示的范围其实是有限的,它只能表示整条数轴中的三部分

  • 某个很大的负数到某个很接近于0的负数、
  • 0、
  • 某个很接近于0的整数到某个很大的正数。

此外,由数学分析的知识可知实数是“稠密”(dense)的,可以证明在任意两个不相等的实数之间总有无穷多个两两不等的实数;但浮点数不是这样,浮点数是“稀疏”的,两个浮点数之间只有有限个浮点数,并且两个“相邻”的浮点数之间的距离可能是巨大的,这就会带来精度方面的一系列问题。

譬如两个“相邻”的32位单精度浮点数,它们的符号位和指数位都相同,尾数位的前22位都相同,只有最后一位相差1,那么这两个浮点数之间的差值可能是非常惊人的。例如01111110100000000000000000000001和01111110100000000000000000000000(指数部分,253-127=126),在32位单精度情况下,它们是“相邻”的,但它们之间的差值竟高达1.014*10^31。换句话说,在32位单精度浮点数中,处于这段差值以内的数都无法表示。如果以相对误差来讨论的话,32位单精度浮点数的尾数只有23位,第24位及其后的值会被舍入,可以近似认为其相对误差为 2231.20107 。这对于某些需要上亿甚至百亿次迭代的程序而言是无法接受的。而64位双精度浮点数的相对误差可以近似认为是 2522.221016 ,比32位单精度浮点数的精度高出不少。可见,64位双精度浮点数不仅表示数的范围扩大了,而且它所刻画的浮点数分布更加“细密”,相对误差更小。并且,对于64位线宽度的计算机而言,处理64位双精度浮点数与处理32位双精度浮点数所需的开销相同,并不需要额外的循环移位,因此还是建议使用64位双精度浮点数

当然,浮点数位数越多,其相对误差也就越小,只要它的精度满足程序运行需要就可放心使用。但无论如何,浮点数终究只是实数的粗糙近似,浮点数不可能完全刻画实数,因为浮点数的位数终究是有限的,换句话说它所能表示的总是有限个有理数,而根据数学分析的知识,在实数轴中虽然无理数和有理数都是无限多的,但无理数集是不可数的,而有理数集却是可数的

除了上面的内容以外,在编程中需要特别注意的有两点:
一、浮点数都是带符号的,不存在unsigned double和unsigned float;
二、两个浮点数之间不能用==来判断是否相等,因为浮点数是对实数的近似,所以计算机中两个浮点数不可能完全相等,最多也只能保证其差值小于用户规定的误差限度。(详细论述及解决方案,请见请见 Python 中的浮点数

仿真

最后提供一段c++代码(vs2013),用来进行单精度浮点数和双精度浮点数与其对应的IEEE 754二进制位的转换

std::bitset<32> float2bits(float n)
{
    _ULonglong nMem = *(_ULonglong* )&n;
                    // typedef unsigned long long _ULonglong;
    return std::bitset<32>(nMem);
}
            // float2bits(125.125)
            // 01000010111110100100000000000000
std::bitset<64> double2bits(double n)
{
    _ULonglong nMem = *(_ULonglong* )&n;
    return std::bitset<64>(nMem);
}
            // double2bits(125.125)
            // 0100000001011111010010000000000000000000000000000000000000000000

float bits2float(std::bitset<64> bs)
{
    return *(float* )&bs;
}
            // bits2float(float2bits(125.125))
            // 125.125
double bits2double(std::bitset<64> bs)
{
    return *(double* )&bs;
}
            // bits2double(double2bits(125.125))
            // 125.125

References

[1] 浅谈计算机中浮点数的表达方法(IEEE 754)

  • 7
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

五道口纳什

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值