如下代码:
float a = 44.556677f;printf("%f\n", a);
得到的输出是什么?并不是44.556677,而是44.556679。浮点数这么坑吗?为什么不一样呢?
浮点数的二进制表示,一种是手算,另一种是直接格式转换然后输出。
手算的,我看的这里的
发现44.556677算出来是42323a09,44.556679是42323a0a,两个不一样。计算过程如下:
强转格式输出的,我看的这里的:
44.556677和44.556679的输出都是42323a0a
对应的代码:
//show_float.c #includeunsigned 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.55667944.556677's hex: 42323a0a44.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[],总算明白了。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进制表示,那么取偶数的那个表示)。
参考:
这个网址计算规则是有问题的,不是遵循IEEE 754标准的。 比如44.556677,IEEE 754标准下应该是0x42323a0a,而不是那个网址算出来的0x42323a09。原因是尾数存在精度损失,需要舍入(rounding),但是这个网址里没做rounding。rounding默认规则是rounding to neareast,也就是尾数需要改为“让精度损失最小的表示”。
rounding规则在这里:
这个网址的转换才是靠谱的。