从44.556677想到的

如下代码:

float a = 44.556677f;
printf("%f\n", a);

得到的输出是什么?并不是44.556677,而是44.556679。浮点数这么坑吗?为什么不一样呢?

浮点数的二进制表示,一种是手算,另一种是直接格式转换然后输出。

手算的,我看的这里的 https://blog.csdn.net/youmeichifan/article/details/80775360

发现44.556677算出来是42323a09,44.556679是42323a0a,两个不一样。计算过程如下:

强转格式输出的,我看的这里的:http://www.cnblogs.com/tlz888/p/9211600.html

44.556677和44.556679的输出都是42323a0a

对应的代码:

//show_float.c 
#include <stdio.h>

unsigned int float2hexRepr(float* a){
    unsigned int c;
    c = ((unsigned int*)a)[0];
    return c;
}

int main(){
    float a = 44.556677f;
    printf("print out 44.556677 but get: %f\n", a);

    printf("44.556677's hex: %x\n", float2hexRepr(&a));

    float b = 44.556679f;
    printf("44.556679's hex: %x\n", float2hexRepr(&b));
   
    return 0;
}

编译运行结果:

print out 44.556677 but get: 44.556679
44.556677's hex: 42323a0a
44.556679's hex: 42323a0a

也就是:44.556677f这个float32类型的数据,在内存中的16进制,应该是0x42320a0a而不是0x42320a09。为什么呢?

因为10进制的0.556677转换为2进制的小数时,多次乘以2仍然得不到1.0,但是IEEE 754标准规定的位数又有限,这就产生了真实值和存储值的误差。为了尽量减小误差就需要考虑舍入(rounding)。

具体怎么rounding呢?实际上IEEE 754规定了4种rounding方式,但只有一个默认方式:rounding to neareast,也就是舍入到偶数,那么转换得到的尾数的2进制表示的最后4位,也就是1001,它的最后一位是奇数,要变成1010。(但是为什么不是1000呢)


上图来自《CSAPP》。然而感觉还是没有说清楚。

又看了wikipedia[https://en.wikipedia.org/wiki/IEEE_754#Rounding_rules],总算明白了。IEEE 754其实规定了5个rounding rule,默认是:Round to nearest, ties to even – rounds to the nearest value; if the number falls midway, it is rounded to the nearest value with an even least significant digit; this is the default for binary floating point and the recommended default for decimal.

完整计算:

对于44.556677,整数部分44的2进制表示是101100;小数部分的2进制表示,先假设为F,那么整个44.556677的2进制科学记数法表示为1.01100F * 2^5。

然后考虑这个F的计算:尾数部分总共23位,其中作为整数部分的44的产生了.01100这5位,作为尾数的前5位;

剩余18位则由0.556677转换为2进制小数而得到:每次乘以2,然后记录下整数部分,拿小数部分再进入下一次计算,而因为0.556677本身的原因,多次乘以2还是无法得到1.0,也就是“乘2”的次数超过了18次,但是还没有停止的意思。。

我们来看它产生的19位都是什么:1000111010000010011 (为什么要19位,因为第19位不为0,我们当前需要18位,这就产生了截断:去掉了第19位和更后面的位,影响了精度。为了让精度下降的更少些,就需要舍入,也就是rounding)。

  • 我们看到第19位是1,那么第18位从1变成0同时第17位从0变成1,这样精度的损失最多是第20位级别的;
  • 而如果第18位变成0,则精度损失是第18位级别的;
  • 而如果第18位不变,则精度损失是第19位级别的

显然,只有第17、18位从01变成10,才能由最小的精度损失。这就是rounding to nearest的第一种意思。(另一种意思是:如果被考虑的数字的二进制表示存在精度损失,并且恰好由两个跟它的值最接近的2进制表示,那么取偶数的那个表示)。

参考:

http://www.styb.cn/cms/ieee_754.php

这个网址计算规则是有问题的,不是遵循IEEE 754标准的。 比如44.556677,IEEE 754标准下应该是0x42323a0a,而不是那个网址算出来的0x42323a09。原因是尾数存在精度损失,需要舍入(rounding),但是这个网址里没做rounding。rounding默认规则是rounding to neareast,也就是尾数需要改为“让精度损失最小的表示”。

rounding规则在这里:

https://en.wikipedia.org/wiki/IEEE_754#Rounding_rules

https://www.h-schmidt.net/FloatConverter/IEEE754.html 这个网址的转换才是靠谱的。

posted @ 2018-12-18 22:28  ChrisZZ  阅读(519)  评论(0编辑  收藏  举报