作者:jicanmeng
时间:2014年06月19日
今天看了阮一峰老师写的浮点数的二进制表示,感觉写的非常好。摘抄要点如下:
根据国际标准IEEE 754,任意一个二进制浮点数V可以表示成下面的形式:
- (-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。
- M表示有效数字,大于等于1,小于2。
- 2^E表示指数位。
举例来说,十进制的5.0,写成二进制是101.0,相当于1.01×2^2。那么,按照上面V的格式,可以得出s=0,M=1.01,E=2。
十进制的-5.0,写成二进制是-101.0,相当于-1.01×2^2。那么,s=1,M=1.01,E=2。
IEEE 754规定,对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。
对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
IEEE 754对有效数字M和指数E,还有一些特别规定。
前面说过,1≤M<2,也就是说,M可以写成1.xxxxxx的形式,其中xxxxxx表示小数部分。IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。
至于指数E,情况就比较复杂。
首先,E为一个无符号整数(unsigned int)。这意味着,如果E为8位,它的取值范围为0~255;如果E为11位,它的取值范围为0~2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,E的真实值必须再减去一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。
比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。
然后,指数E还可以再分成三种情况:
- E不全为0或不全为1。这时,浮点数就采用上面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1。
- E全为0。这时,浮点数的指数E等于1-127(或者1-1023),有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。
- E全为1。这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s);如果有效数字M不全为0,表示这个数不是一个数(NaN)。
只看还不行,我又实践了一下,以下是实践的情况。
程序如下:
#include <stdio.h> int main(void){ int num = 9; float* pFloat=(float *)# printf("sizeof(int) is %d, sizeof(float) is %d\n", sizeof(int), sizeof(float)); printf("value of num is %d\n",num); printf("value of *pFloat is %f\n",*pFloat); *pFloat = 9.0f; printf("value of num is %d\n",num); printf("value of *pFloat is %f\n",*pFloat); return 0; }
使用gdb调试结果如下:
[jicanmeng@andy tmp]$ gcc -g float.c -o float
float.c: In function ‘main’:
float.c:4: warning: initialization from incompatible pointer type
[jicanmeng@andy tmp]$ ./float
sizeof(int) is 4, sizeof(float) is 4
value of num is 9
value of *pFloat is 0.000000
value of num is 1091567616
value of *pFloat is 9.000000
[jicanmeng@andy tmp]$ gdb float
GNU gdb (GDB) Red Hat Enterprise Linux (7.2-60.el6_4.1)
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
...
Reading symbols from /home/jicanmeng/Desktop/tmp/float...done.
(gdb) break float.c:10
Breakpoint 1 at 0x40052e: file float.c, line 10.
(gdb) run
Starting program: /home/jicanmeng/Desktop/tmp/float
sizeof(int) is 4, sizeof(float) is 4
value of num is 9
value of *pFloat is 0.000000
Breakpoint 1, main () at float.c:10
10 *pFloat=9.0;
(gdb) print num
$1 = 9
(gdb) print &num
$2 = (int *) 0x7fffffffe0e4
(gdb) x/4tb 0x7fffffffe0e4
0x7fffffffe0e4: 00001001 00000000 00000000 00000000
(gdb) step
11 printf("value of num is %d\n",num);
(gdb) x/4tb 0x7fffffffe0e4
0x7fffffffe0e4: 00000000 00000000 00010000 01000001
(gdb) continue
Continuing.
value of num is 1091567616
value of *pFloat is 9.000000
Program exited normally.
(gdb) q
[jicanmeng@andy tmp]$
从gdb的调试结果中可以看到,和阮一峰老师说的完全相同。当赋值9.0时,四个字节从高到底分别为01000001,00010000,00000000,00000000。红色的一个bit表示符号位,是0;绿色的8个bit表示指数,为3+127=130;蓝色的23个bit表示有效数字,为1.001。
其中x/4tb 0x7fffffffe0e4
表示查看从0x7fffffffe0e4地址开始的4个字节的内容。我们可以通过help x
命令来查看x命令的用法。
知道了浮点数的表示方法后,我们来看几个特殊的浮点数:使用浮点数表示的最小的正数, FLT_MIN, FLT_MAX, FLT_EPSILON.
使用浮点数表示的最小正数
: 它是一个非规格化的数,四个字节从高到低分别为00000000,00000000,00000000,00000001. 其实正好对应于int类型的数据0x01.FLT_MIN
: 表示最小的符号为正的规格化数,四个字节从高到低分别为00000000,10000000,00000000,00000000. 正好对应于int类型的0x00800000. float.h文件中对FLT_MIN的描述是"min positive value", 其实准确的描述应该是"minimum normalized positive floating-point number".FLT_MAX
: 表示最大的规格化数,四个字节从高到低分别为01111111,01111111,11111111,11111111. 正好对应于int类型的0x7F7FFFFF.FLT_EPSILON
: 浮点数可以表示1.0f, 可以表示紧邻着1.0f的比1.0f大的一个数,这个数和1.0f的差值就是FLT_EPSILON. 可以先看一看float.h中对这三个数的定义:
#define FLT_EPSILON 1.192092896e-07F /* smallest such that 1.0+FLT_EPSILON != 1.0 */ #define FLT_MAX 3.402823466e+38F /* max value */ #define FLT_MIN 1.175494351e-38F /* min positive value */
使用下面的这个程序来验证一下刚才所说的:
#include <stdio.h> #include <math.h> #include <float.h> int main(void){ int a = 0x1; float *pF = (float *)&a; printf("%.60f\n", *pF); printf("%.60f\n", FLT_MIN); a = 0x00800000; printf("%.60f\n", *pF); printf("%.10f\n", FLT_MAX); a = 0x7f7fffff; printf("%.10f\n", *pF); printf("%.50f\n", FLT_EPSILON); return 0; }
如果使用windows平台上面的mingw编译,程序输出如下:
D:\softwares\Qt\Qt5.8.0\Tools\mingw530_32\bin>a.exe
0.000000000000000000000000000000000000000000001401298464324817
0.000000000000000000000000000000000000011754943508222875000000
0.000000000000000000000000000000000000011754943508222875000000
340282346638528860000000000000000000000.0000000000
340282346638528860000000000000000000000.0000000000
0.00000011920928955078125000000000000000000000000000
D:\softwares\Qt\Qt5.8.0\Tools\mingw530_32\bin>
如果使用linux平台上面的gcc编译,程序输出如下:
[jicanmeng@andy tmp]$ ./a.out
0.000000000000000000000000000000000000000000001401298464324817
0.000000000000000000000000000000000000011754943508222875079687
0.000000000000000000000000000000000000011754943508222875079687
340282346638528859811704183484516925440.0000000000
340282346638528859811704183484516925440.0000000000
0.00000011920928955078125000000000000000000000000000
[jicanmeng@andy tmp]$
我们再来使用linux上面自带的bc计算器来手动计算一下这几个特殊的浮点数。
最小正数值为0.0000 0000 0000 0000 0000 0012 * 21-127 = 2-23 * 2-126 = 2-149 = 0.000000000000000000000000000000000000000000001401298464324817. 正好等于程序输出的值。
最小的符号为正的规格化数为1.0000 0000 0000 0000 0000 0002 * 21-127 = 1.0 * 2-126 = 2-126 = 0.000000000000000000000000000000000000011754943508222875079687. 正好等于linux平台下gcc编译程序后输出的值。
最大的规格化数为1.1111 1111 1111 1111 1111 1112 * 2254-127 = (1+(1-2-23)) * 2127 = 340282346638528859811704183484516925440.0. 正好等于linux平台下gcc编译程序后输出的值。
1.0f对应的数据为00111111,10000000,00000000,00000000. 和1.0f紧邻着的大于1.0f的这个数为00111111,10000000,00000000,00000001. 差值为2-23 = 0.00000011920928955078125. 正好等于程序输出的值。
截图如下:
[jicanmeng@andy tmp]$ bc
bc 1.06.95
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'.
scale=60
2^(-149)
.000000000000000000000000000000000000000000001401298464324817
2^(-126)
.000000000000000000000000000000000000011754943508222875079687
(1 + 1 - 2^(-23)) * 2^127
340282346638528859811704183484516925440.0000000000000000000000000000\
00000000000000000000000000000000
scale=50
2^(-23)
.00000011920928955078125000000000000000000000000000
[jicanmeng@andy tmp]$
可以看出,bc和linux平台下的gcc在计算FLT_MIN和FLT_MAX的时候,精度要高于windows平台下的mingw。所以我们平时做验证的时候,如果要求精度高一些,还是使用linux平台下的工具才靠谱。
常常在一些书或文章中看到"浮点数的精度是6~7位",这句话是什么意思呢?
首先看下表:
-------------------------------------------------------------------- 0 01111111 0000000 00000000 00000000 1.0f 0 01111111 0000000 00000000 00000001 1.00000011920928955078125 0 01111111 0000000 00000000 00000010 1.0000002384185791015625 0 01111111 0000000 00000000 00000011 1.00000035762786865234375 0 01111111 0000000 00000000 00000100 1.000000476837158203125 0 01111111 0000000 00000000 00000101 1.00000059604644775390625 0 01111111 0000000 00000000 00000110 1.0000007152557373046875 0 01111111 0000000 00000000 00000111 1.00000083446502685546875 0 01111111 0000000 00000000 00001000 1.00000095367431640625 0 01111111 0000000 00000000 00001001 1.00000107288360595703125 ... 0 01111111 1111111 11111111 11111010 1.9999992847442626953125 0 01111111 1111111 11111111 11111011 1.99999940395355224609375 0 01111111 1111111 11111111 11111100 1.999999523162841796875 0 01111111 1111111 11111111 11111101 1.99999964237213134765625 0 01111111 1111111 11111111 11111110 1.9999997615814208984375 0 01111111 1111111 11111111 11111111 1.99999988079071044921875 -------------------------------------------------------------------- 0 10000000 0000000 00000000 00000000 2.0f
从1.0f到2.0f,中间有223个数。每个数之间的间隔都是(2-1)/(223) = 1/8388608 = 0.00000011920928955078125。这样,小数点后第6位数中的0,1,2,3,4,5,6,7,8,9都可以表示出来。再加上小数点之前的1,这样,浮点数表示的精度是7位。
那么,浮点数表示的精度是6位是什么情况呢?
我们知道,浮点数每223个数之间的间隔是相同的。下一个223个数之间的间隔是前面的2倍。例如,从1到2之间的223个数之间的间隔是1/(223) = 0.00000011920928955078125; 从2到4之间的223个数之间的间隔是2/(223) = 0.0000002384185791015625,是前面的间隔的2倍;然后,从4到8之间的223个数之间的间隔是4/(223) = 0.000000476837158203125,又是前面的间隔的2倍。考虑1.0f, 0.5f, 0.25f, 0.125f, 0.0625f, 0.03125f, 0.015625f, 0.0078125, 0.00390625, 0.001953125, 0.0009765625这些数,正是这些数决定了紧跟着的223个数之间的间隔。而0.0009765625这个数又比较特殊了,紧跟着它的223个数的间隔是0.0009765625/223 = 0.000000000116415321826934814453, 也就是说紧挨着0.0009765625的一个数是0.000976562616415321826934814453。这样看来,从第一个有效数字开始,第7个有效数字不能将0,1,2,3,4,5,6,7,8,9都表示出来,但第6个有效数字都能将0,1,2,3,4,5,6,7,8,9表示出来。所以,这些浮点数表示的精度是6位。
所以说,浮点数表示的精度为6~7位,大多数的浮点数表示的精度为7位。
我们继续往下看,浮点数表示的精度为6~7位,是不是就是说,使用浮点数表示的有效数字的前6位一定是精确的?在"百度知道"里面有一个网友就问过这样的问题:单精度浮点数有效位数一定是7位吗?他还给出了一个示例程序:
#include <stdio.h> #include <math.h> #include <float.h> int main(void){ float a = 999999.9999f; float b = 8388608.6f; printf("%f %f\n", a, b); return 0; }
运行结果如下:
[jicanmeng@andy tmp]$ ./a.out
1000000.000000 8388609.000000
[jicanmeng@andy tmp]$
有一个网友的回答是:至少6位是精确地;第七位,可能精确,可能不精确。他的回答对吗?
按照我的理解,他的回答是错误的。浮点数的精度是6位表示的意思并不是说前6位一定是精确的,而是说在浮点数所有能表示的数字中,第6位都能将0,1,2,3,4,5,6,7,8,9表示出来。但是如果你表示一个数,第6个有效数字不一定是准确的。例如如下例子:
------------------------------------------------------------------------- 0 01110101 0000000 00000000 00000000 0.0009765625f 0 01110101 0000000 00000000 00000001 0.000976562616415321826934814453125f 0 01110101 0000000 00000000 00000010 0.00097656273283064365386962890625f 0 01110101 0000000 00000000 00000011 0.000976562849245965480804443359375f 0 01110101 0000000 00000000 00000100 0.0009765629656612873077392578125f 0 01110101 0000000 00000000 00000101 0.000976563082076609134674072265625f 0 01110101 0000000 00000000 00000110 0.000976563198491930961608886718750f 0 01110101 0000000 00000000 00000111 0.0009765633149072527885437011718750f ..... 0 01110101 0000000 01110011 01010111 0.0009799998952075839f 0 01110101 0000000 01110011 01011000 0.00098000001162290573f ..... 0 01110101 0000001 10100001 01010101 0.00098899996373802423f 0 01110101 0000001 10100001 01010110 0.00098900008015334606f ..... 0 01110101 1111111 11111111 11111111 0.001953124883584678173065185546875f -------------------------------------------------------------------- 0 10000000 0000000 00000000 00000000 2.0f
在上面这个例子中,第6个有效数字中,从2到3到4到5都能表示出来,但是如果你将0.000976563赋值给一个浮点数变量再打印一下,那么它会打印出来0.0009765629656612873077392578125f。此时表示的完全准确的有效数字就只有5位了。
另外要特别注意,由于浮点数的间隔特性,如果你将一个数赋值给一个浮点型变量,编译器首先检查这个这个数是否在两个浮点数之间,离哪个浮点数接近,离哪个近,这个变量中就保存哪个值。例如将0.000989f赋值给一个浮点型变量,那么再打印出来,打印结果就是0.00098899996373802423f,只有两个有效数字是准确的。所以浮点数的四舍五入也是你需要考虑的问题。
现在回过头来再看看网友提到的这个例子:
999999.9999这个数位于524288和1048576之间,之间的223个数的间隔是524288/8388608=0.0625。所以和1000000.000000紧邻的一个数是1000000.000000 - 0.0625 = 999999.9375。999999.9999离1000000.000000只有0.00001,离999999.9375有0.0624,所以就四舍五入保存为1000000.000000。
类似地,对于8388608.6这个数呢,它位于8388608和16777216之间,之间的223个数的间隔为1,所以位于8388608和8388609之间。8388608.6离8388609比较近,所以就打印为8388609.000000了。其实就是这么简单。