[system] 浮点数存储结构说明
写在前面
这篇文章其实是些在2019年1月18的,当时还没看CS:APP的浮点数部分,现在看到了,便又想起之前曾研究过浮点数的事情;所以,就在这里一块发出来啦。
这篇文章中使用的一些字母表示,可能和CS:APP中有不同含义,查看时应留意。
2019 Dec. 8
十进制小数转化为二进制小数的方法
可以将十进制小数视为:小数点前的整数部分,和小数点后的小数部分。整数部分十进制转化二进制不再详述。小数部分的转化方法与整数部分不同:小数部分累计乘以2,并取整数部分为结果,直到小数部分不存在(即,乘以2后,积为0)。
例如:
8.5(D)转化为二进制:
-> 拆分:整数部分8(D),小于1的小数部分0.5(D)
-> 整数部分转为二进制得到1000(B)
-> 小数部分0.5(D)的转化:0.5x2=1.0 取1,0x2=0 取0,结束。得到0.10(B),即为0.5(D)的二进制表示。
-> 合并整体结果便是:1000.1(B)为8.5(D)的二进制表示。
十进制小数转化为二进制小数的特点
如上面的示例,十进制小数转化为二进制小数本质是,将十进制小数表示为1/(2^N),N对应为二进制小数点后的位数。例如:0.5(D)<->0.1(B)小数点后第1位为1,代表了1/(2^1)。
但是,很显然不是所有十进制小数,都能够用有限的1/(2^N)累加的方式来表示。这意味着存在相当数量的十进制小数,转化为二进制的小数是无限小数。
例如:
0.4(D)转化为二进制:
0.4x2=0.8 取0,
0.8x2=1.6 取1,
0.6x2=1.2 取1,
0.2x2=0.4 取0,
0.4x2=0.8 取0,
……
上面的计算始终不能得到一个积为0,整个计算是无限循环的。也就是说0.4(D)转化为二进制,得到的是无限循环二进制小数0.011001100110……(0110无限循环)。
而且这种十进制小数是普遍存在的。换句话说:存在相当数量的十进制小数(即使是看起来并不复杂的小数),在二进制中无法使用“有限位数”的小数表示。
另外,也随便一提,同整数转化一样,十进制小数的某一位与二进制小数的某一位,没有什么“对应关系”,即便是二进制小数第一位,也是十进制小数全部数据都参与转换运算的结果。毕竟不是同一种进位制,观察位数的“对应关系”没有意义。
浮点数在计算机中的表示
计算机中使用科学计数法表示小数。由上面的例子观察发现:任何一个二进制小数都可以表示为:1.XXX * 2^N的形式。
例如:
8.5(D)-> 1000.1(B)-> 1.0001 * 2^3(B)
0.4(D)-> 0.01100110…(B)-> 1.100110… * 2^-2(B)
可以发现所有二进制小数的科学计数法表示,都是由三部分决定:小数的正负、小数点后的数据、2的次幂。
在计算机中表示小数,正是开辟一段内存空间记录上面的三个部分。
浮点数(float、double等)在计算机中使用类似的结构在内存中表示浮点数:分为三段——符号位、指数位、尾数位。只是在不同的浮点数类型中,指数位和尾数位的长度有所不同。
符号位 | 指数位E | 尾数位X | |
---|---|---|---|
float类型 32bit | 1 bit | 8 bit | 23 bit |
double类型 64 bit | 1 bit | 11 bit | 52 bit |
对于二进制小数:1.XXX * 2^N(B)
符号位:0表示正数;1表示负数
指数位:用于表示上文中的N。指数N可正可负。
float类型中指数位长度8位,值的范围:0~255(D);其值E与N的关系:E=127+N
double类型中指数位长度11位,值的范围:0~2047(D);其值E与N的关系:E=1023+N
尾数位:即表示上文的XXX。
(注:实际浮点数标准,要比上文描述的更复杂。例如,指数位全0、全1,是有其他作用的。指数位全0,用来表示0.0, -0.0 和 denormalized number(非规约数)。指数位全1,用来表示正负无穷、NaN(非数)。具体详细细节参见IEEE 754 二进制浮点数算数标准。)
例如:
8.5(D)-> 1000.1(B)-> 1.0001 * 2^3(B)
E=127+3=130 -> 10000010(B)
X=00010…(其后全为0)
内存中为:
0|100 0001 0|000 10…(其后全为0)(开头的0表示 “正数” )
即0x41080000(H)
-8.5(D)-> -1000.1(B)-> -1.0001 * 2^3(B)
E=127+3=130 -> 10000010(B)
X=00010…(其后全为0)
内存中为:
1|100 0001 0|000 10…(其后全为0)(开头的1表示 “负数” )
即0xC1080000(H)
0.4(D)-> 0.01100110…(B)-> 1.100110… * 2^-2(B)
E=127-2=125 -> 01111101(B)
X=100110…(1001无限循环)但是float尾数位长度有限(23位),所以:
X=100 1100 1100 1100 1100 1101(最后一个1是后面1001的进位,“零舍一入”)
内存中为:
0|011 1110 1|100 1100 1100 1100 1100 1101
即0x3ECCCCCD(H)
计算机中浮点数范围与误差
浮点数1.XXX * 2^N(B)的范围,取决于指数位的值E的范围;其精度,取决于尾数位,因为尾数X以2的次幂方式记录小数,尾数位长度越长,可以应用的2的次幂数越大,也就越精确。而造成其不精确的原因就是,计算机中尾数的最后一位可能是二进制无限小数“零舍一入”后的结果。这便是浮点数误差的来源。所以,对于衡量精度,也可以理解为,转化为十进制小数后,原二进制小数尾数的最后一位,是十进制小数点后的第几位。同时,这也说明:由计算机记录的,部分浮点数是完全准确的,没有误差;而另外部分浮点数是存在误差的,且不可避免,但误差值是确定的,而非随机误差值。
对于float类型:8位指数位,23位尾数位,E=127+N
E的范围0~255(D),8位全0到全1,即N的范围是 -127~128(D)。N最小时为非零float的最小绝对值,N最大时为float的最大绝对值。2^-127 ~ 2^128,再加上正负号,也即-3.40E+38 ~ +3.40E+38 。
X的范围0~8,388,607(D)0~0x007F FFFF(H),23位全0到全1。2^23 = 8,388,608 = 10^6.92,所以对于十进制,精确位数是6~7位。
对于double类型:11位指数位,52位尾数位,E=1023+N
E的范围0~2047(D),11位全0到全1,即N的范围是-1023~1024(D)。N最小时为非零double的最小绝对值,N最大时为double的最大绝对值。2^-1023~2^1024,再加上正负号,也即-1.79E+308 ~ +1.79E+308 。
X的范围0~4,503,599,627,370,495(D)0~0x000F FFFF FFFF FFFF(H),52位全0到全1。2^52=4503599627370496=10^15.65,所以对于十进制,精确位数是15~16位。
简而言之,浮点类型的范围,无论是float的10^38数量级,还是double的10^308数量级,这些往往是足够使用的。而浮点类型的精确度,则是在使用过程中能够明显感受到的,因为浮点数据类型记录数据的“尾数位”中的最后一位,很可能是对二进制无限小数的近似。这个近似反应在十进制小数上的误差的大小,同时也受到“指数位”的影响。所以,给人的感受是:有的浮点数误差较大、有的较小、还有的没有误差。这些都是“尾数位”最后一位和“指数位”共同的效果。
所以在选择使用float类型、double类型、甚至long double类型,判断的依据往往在于:我们数据的有效数字是几位。
我觉得,当数据所需的有效数字的位数小于浮点类型的精确位数时,也即浮点类型“尾数位”最后一位影响不到有效数字,那就可以完全抛开浮点类型的误差问题,并认为数据的有效数字是完全准确的。
测试代码
下面的代码测试float数据存储,并使用GDB观察变量在内存中的存储。可以与上文的说明相对照。
代码变量名开头为“n”指的是“负数”。变量max_num并不是一个最大float值,我只是将尾数位全部设置为1,N设置为0(E=127)。
#include <stdio.h>
#include <string.h>
#include <math.h>
int main(int argc, char *argv[])
{
int i = 1;
int j = 0;
int ni = -1;
float f1 = 0.5;
float f2 = 8.5;
float nf1 = -0.5;
float nf2 = -8.5;
float f3 = 0.4;
/**
* In the 24 chars, the 1st is individual digit (1),
* others (23 chars) are "XXX" in "1.XXX * 2^N".
* All are "1".
*/
char *max = "111111111111111111111111";
float max_num = 0;
/**
* Almost 0.4 (D) -> 1.XXX * 2^N (B):
* In the 25 chars, the 1st is individual digit (0),
* the 2nd is the "1" in "1.XXX * 2^N",
* others (23 chars) are "XXX" in "1.XXX * 2^N".
*/
char *str = "0110011001100110011001100";
float number = 0;
for (j = 0; j < strlen(str); j++) {
number = number + (str[j] - '0') * pow(2, -(j + 1));
}
/**
* In order to make the Exponent to 127(D) 0x7F(H),
* I need a decimal such like 1.xxx
* so I use "-j" instead of "-(j + 1)".
* And the string have 24 chars instead of 25 chars.
* Then I can make out the float as "0x3FFFFFFF"
* in memory.
* (Add one "0" at the begin of 0x7F, it is 0x3F.)
*/
for (j = 0; j < strlen(max); j++) {
max_num = max_num + (max[j] - '0') * pow(2, -j);
}
printf("i = %d\n", i);
printf("ni = %d\n", ni);
printf("f1 = %.20f\n", f1);
printf("nf1 = %.20f\n", nf1);
printf("f2 = %.20f\n", f2);
printf("nf2 = %.20f\n", nf2);
printf("f3 = %.20f\n", f3);
printf("number = %.20f\n", number);
printf("number = %.8f\n", number);
printf("number = %.6f\n", number);
printf("number = %f\n", number);
printf("max_num = %.20f\n", max_num);
if (number == f3)
printf("number == f3\n");
else
printf("number != f3\n");
if (number == 0.4)
printf("number == 0.4\n");
else
printf("number != 0.4\n");
return 0;
}
下面是使用GDB调试查看变量的结果。
➜ ~ gdb a.out
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.3) 7.7.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
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-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from a.out...done.
(gdb) b 75
Breakpoint 1 at 0x4009cd: file test.c, line 75.
(gdb) r
Starting program: /home/aningsk/a.out
i = 1
ni = -1
f1 = 0.50000000000000000000
nf1 = -0.50000000000000000000
f2 = 8.50000000000000000000
nf2 = -8.50000000000000000000
f3 = 0.40000000596046447754
number = 0.39999997615814208984
number = 0.39999998
number = 0.400000
number = 0.400000
max_num = 1.99999988079071044922
number != f3
number != 0.4
Breakpoint 1, main (argc=1, argv=0x7fffffffdde8) at test.c:75
75 return 0;
(gdb) x &i
0x7fffffffdcc4: 0x00000001
(gdb) x &ni
0x7fffffffdcc8: 0xffffffff
(gdb) x &j
0x7fffffffdcb8: 0x00000018
(gdb) x &f1
0x7fffffffdccc: 0x3f000000
(gdb) x &nf1
0x7fffffffdcd4: 0xbf000000
(gdb) x &f2
0x7fffffffdcd0: 0x41080000
(gdb) x &nf2
0x7fffffffdcd8: 0xc1080000
(gdb) x &f3
0x7fffffffdcdc: 0x3ecccccd
(gdb) x &number
0x7fffffffdcc0: 0x3ecccccc
(gdb) x &max_num
0x7fffffffdcbc: 0x3fffffff
(gdb) q
A debugging session is active.
Inferior 1 [process 31865] will be killed.
Quit anyway? (y or n) y
➜ ~