以 GNU C
为例,它遵循 IEEE 754-2008
标准中制定的浮点表示规范。在该规范中定义了 5
种不同大小的基础二进制浮点格式,包括:16
位,32
位,64
位,128
位,256
位。其中,32
位的格式被用作标准 C
类型的 float
,64
位的格式被用作标准 C
类型的 double
,128
位的格式被用作标准 C
类型的 long double
。
每种浮点格式都由三部分组成,包括:1
个比特的符号位、若干比特的指数位、若干比特的小数位,如下图所示:
以 C
语言中 float
类型(32
位浮点)为例,它的符号位占 1
位,指数位占 8
位,小数位占 23
位,如下图所示:
值得注意的是,
1)指数位编码的指数以 2
为底数,若要表示很小的数,例如 1/(2^n) == 2^(-n)
,则指数位会出现负数 (-n)
,那么还需要专门的符号位来表示,因此指数位是以 127
为偏移的,也就是说,一个数在用浮点数来表示时,其指数位的值是实际指数值加上 127
。对于 64
位浮点数,它的指数位是 11
个比特,以 1023
为偏移。
2)浮点数的小数部分是将该浮点数表示为 2^n * 1.m
后,0.m
那部分的数值,也就是说,整数部分始终是 1
,且被忽略了(不用保存),这样就能省出一位来表示更高的精度。
举例来说,
1)0.5 = 2^(-1) * 1.0
,其指数位为 (-1 + 127)
,小数位为 0.0
,其二进制是:0 01111110 00000000000000000000000
,也就是 0x3F000000
。
2)5 = 2^(2) * 1.25
,其指数位为 (2 + 127)
,小数位为 0.25
,其二进制是:0 10000001 01000000000000000000000
, 也就是 0x40A00000
。
测试代码如下:
#include <stdio.h>
int main(void)
{
float a = 0.5;
unsigned int *b = (unsigned int*)&a;
printf("b = 0x%X\n", *b);
a = 5;
printf("b = 0x%X\n", *b);
}
程序运行结果如下:
$ gcc -o main main.c
$ ./main
b = 0x3F000000
b = 0x40A00000
从 32
位浮点的指数宽度来看,理论上,它可以表示很大的数,比如:2^(128) * 1.9999999
,这比我们 32
位整型能表示的范围(2^(32) - 1)
大的多,但受限于浮点小数位的宽度,有些 32
位整型数是无法用浮点数来表示的,也就是会出现空洞,原因是浮点数的小数部分精度有限,当小数部分比 1/(2^23)
还小时,就无法准确表示该整型数,例如:2^(24) + 1
这个数用 32
位整型来表示是没有问题的,但用 32
位浮点来表示,则会被舍入为:2^24
。
测试代码如下:
#include <stdio.h>
int main(void)
{
float a = 1 << 24;
unsigned b = 1 << 24;
printf("a = %f, b = %u\n", a, b);
a += 1;
b += 1;
printf("a = %f, b = %u\n", a, b);
return 0;
}
程序运行结果如下:
$ gcc -o main main.c
$ ./main
a = 16777216.000000, b = 16777216
a = 16777216.000000, b = 16777217
参考资料:
- https://www.gnu.org/software/c-intro-and-ref/manual/html_node/Floating-Representations.html
- https://docs.nvidia.com/cuda/floating-point/index.html