文章目录
在做 MPI 大数据排序实验的时候,因为要求自己实现生成随机排序的浮点数数据,所以写了一个简单的 C 生成随机数。没怎么多想就按照整型随机数数据集的生成方式来了,就是先生成一个 [ 0.0 , 1.0 ] [0.0, 1.0] [0.0,1.0] 之间的小数,然后乘以
float
类型所能表示的最大值来生成可能的浮点数范围内的任意值。但是当我检查生成的二进制数据文件读取浮点数时,发现浮点数的整型部分都非常大,并且小数部分都为零,百思不得其解。
float gen_random_flt()
{
// 生成一个在 [0, 1] 范围内的随机浮点数
float scale = (float)rand() / (float)RAND_MAX;
// 随机决定是正数还是负数
float sign = (rand() % 2 ? 1.0f : -1.0f); // 生成 -1 或 1
// 将随机浮点数映射到 [-FLT_MAX, FLT_MAX] 范围
return sign * scale * FLT_MAX + scale;
// return sign * scale;
}
我还特意在返回浮点数的时候,加一个 [ 0.0 , 1.0 ] [0.0, 1.0] [0.0,1.0] 范围之间的小数,这样总能保证小数部分不是全为零了吧。结果输出一看,还是全部都是零:
312074220398877433439531912707967025152.000000
float
类型的浮点数,十进制小数点输出是 6 位,然后 double
类型的话可以去到 16 位十进制小数。但是为什么小数部分没有显示呢?如果单生成
[
0.0
,
1.0
]
[0.0, 1.0]
[0.0,1.0] 部分的小数的话,打印二进制文件里面的浮点数数据又是可以显示的:
-0.157101 0.983587 -0.751909 -0.583410 0.082617 0.445290 -0.836798 -0.527569 -0.666163 0.714496 0.434055 0.626577 0.449175 -0.802974 -0.672282 -0.494159 0.969527 -0.614576 0.772751 0.874313 -0.062900 0.690523 -0.498628 -0.344090 -0.262711 0.267150 ...
所以我猜测应该是和浮点数所能表示的最大大小有关系。那么 FLT_MAX
宏定义定义的数值是多少呢?
#define FLT_MAX __FLT_MAX__
扩展到:
3.40282346638528859811704183484516925e+38F
使用了十进制的 38 次方,这个数确实有点大了,所以我去谷歌了一下看看别人有没有遇到这个情况,基本上是说浮点数正确的比较方法,以及为什么会导致精度丢失的问题,都不是我这个不显示小数部分的问题。
直到这篇 Stackoverflow 帖子 大概是说明了原因:
Well,
float
(Single
) uses 23 bits for mantissa Sofloat
can represent integers up to2**24 - 1 == 16777215
which is close to14385471
.
IEEE 754 浮点数表示法
-
符号位(Sign bit):
- 这是浮点数的最高位,用于表示浮点数的正负。
- 0 表示正数,1 表示负数。
-
指数部分(Exponent):
- 指数部分表示浮点数的指数,用来确定浮点数的规模或范围。
- 在 IEEE 754 标准中,指数部分通常使用偏移量(Bias)表示法。对于单精度浮点数(32 位),偏移量是 127;对于双精度浮点数(64 位),偏移量是 1023。
- 实际的指数值等于存储的指数值减去偏移量。
-
尾数部分(Mantissa / Significand):
- 尾数部分表示浮点数的精度,用来表示数值的有效位。
- 尾数部分通常假设一个隐含的 1 在最左边,即实际表示的尾数是 1.xxxxx…,其中 xxxxx 是尾数部分的位。
- 在单精度浮点数(32 位)中,尾数部分有 23 位;在双精度浮点数(64 位)中,尾数部分有 52 位。
示例:IEEE 754 单精度浮点数(32 位)表示法
- 1 位符号位
- 8 位指数部分
- 23 位尾数部分
例如,一个单精度浮点数的二进制表示如下:
S EEEEEEEE MMMMMMMMMMMMMMMMMMMMMMM
其中:
S
是符号位。EEEEEEEE
是 8 位的指数部分。MMMMMMMMMMMMMMMMMMMMMMM
是 23 位的尾数部分。
示例:IEEE 754 双精度浮点数(64 位)表示法
- 1 位符号位
- 11 位指数部分
- 52 位尾数部分
例如,一个双精度浮点数的二进制表示如下:
S EEEEEEEEEEE MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
其中:
S
是符号位。EEEEEEEEEEE
是 11 位的指数部分。MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
是 52 位的尾数部分。
浮点数表示例子
假设有一个十进制数 -6.5
。将其表示为 IEEE 754 单精度浮点数:
- 符号位:1(因为是负数)。
- 将
6.5
转换为二进制:110.1
。 - 正规化二进制表示:
1.101 x 2^2
。 - 指数部分:2 + 127(偏移量)= 129,二进制为
10000001
。 - 尾数部分:
10100000000000000000000
(去掉隐含的 1)。
最终表示为:
1 10000001 10100000000000000000000
IEEE 754 浮点数表示法为什么指数要加上偏移量
在浮点数表示中使用偏移量(bias)来表示指数是为了处理指数的正负值。这种方法使得浮点数表示中的指数部分始终为非负数,从而简化了比较和排序操作。
也即,在单精度的情况下,所支持的 2 进制指数范围是 [ − 2 7 , 2 7 − 1 ] [-2^7, 2^7-1] [−27,27−1] ,因为虽然存储浮点数的时候,这 8 位当作无符号整型来存储,比较的时候也是按照无符号整型来比较,因此对于无符号整型比较来说, 1 > 0 1 > 0 1>0 ,也就是第一位不能解释为符号位 − - − ,所以正指数 x x x 的无符号整型实际表示是 1000000 + x b 1000000 + x_b 1000000+xb ,这样无符号整形比较的时候,正数指数一定大于负数指数,因为负数指数的开头位一定为 0 0 0 ,那么 0 x x x x x x < 1 x x x x x x 0xxxxxx < 1xxxxxx 0xxxxxx<1xxxxxx 就很正常。
为什么使用偏移量
-
简化硬件实现:
- 使用非负数可以简化硬件电路的设计,特别是在比较和排序操作中。比较两个带符号位的二进制数(包括负数)的硬件电路要复杂得多。
- 非负数的比较可以直接使用无符号整数的比较电路,这样更高效。
-
统一表示范围:
- 通过使用偏移量,可以统一表示正指数和负指数。例如,在单精度浮点数中,偏移量为 127,这意味着实际指数范围从 -126 到 +127。
- 指数存储的范围是 0 到 255(8 位),减去偏移量 127 后,实际指数范围是 -127 到 128,其中 -127 和 128 分别用于表示特殊值(如非规格化数、无穷大和 NaN)。
浮点数的精度
以单精度浮点数为例,二进制存储中使用 23 位代表尾数,那么二进制下的小数部分能去到 23 位,也就是 b 1.000 … 001 b1.000\dots 001 b1.000…001 ,那么最小的变化量就是 b 0.000 … 001 b0.000\dots001 b0.000…001 ,这个二进制小数大概是十进制的多少呢?大约是 2.220446049250313 × 1 0 − 16 2.220446049250313\times 10^{-16} 2.220446049250313×10−16 这么多,这就是单精度浮点数表示的十进制精度。
浮点数的尾数部分表示一个二进制小数,其位数决定了精度。例如,在 IEEE 754 单精度浮点数(32 位)中,尾数部分有 23 位;在双精度浮点数(64 位)中,尾数部分有 52 位。
尾数部分的二进制表示
尾数部分可以表示的最小增量是其最后一位,即第 23 位(对于单精度浮点数),或第 52 位(对于双精度浮点数)。
单精度浮点数
单精度浮点数的尾数部分有 23 位。这些位表示的是一个二进制小数,因此其最小增量是 2 − 23 2^{-23} 2−23 。但是,这并不表示尾数部分的精度,而是表示尾数部分的最小增量。
双精度浮点数
双精度浮点数的尾数部分有 52 位。这些位表示的是一个二进制小数,因此其最小增量是 2 − 52 2^{-52} 2−52 。同样,这并不表示尾数部分的精度,而是表示尾数部分的最小增量。
float 所能带小数部分的最大数
使用一段简单的代码测试了一下这个问题:
int main(int argc, char** argv)
{
printf("%f", 10000000.49f);
return 0;
}
发现当需要显示的浮点数到达了
1.0
e
+
7
1.0e+7
1.0e+7 级别的时候,小数部分开始就无法精确表示了,比如上述的输出结果是 10000000.000000
,但是如果显示的浮点数是 10000000.51f
那么结果就是 10000001.000000
,怪不得之前使用超大浮点数表示的时候,实际上表示出来都是整数,也就是说浮点数是有十进制位的限制的,最多表示 6~7 位十进制数,超过了这个范围小数部分的精度就没法保证了。但这是为什么呢?问 GPT 从 IEEE 754 浮点数表示法的角度来解答这个问题。
具体示例:1.0e+7f + 0.1f
1. 表示 1.0e+7f
- 十进制数值:
1.0e+7
即 10,000,000 - 二进制表示:
10,000,000
的二进制表示为100110001001011010000000
(23 位)
- 正规化:
- 把它写成二进制科学计数法:
1.00110001001011010000000 × 2^23
- 把它写成二进制科学计数法:
- IEEE 754 表示:
- 符号位:
0
(正数) - 指数位:
23 + 127 = 150
,二进制为10010110
- 尾数位:
00110001001011010000000
(省略隐含的前导 1)
- 符号位:
所以,1.0e+7f
在 IEEE 754 单精度浮点数中的表示是:
0 10010110 00110001001011010000000
2. 表示 0.1f
- 十进制数值:
0.1
- 二进制表示:
0.1
的二进制表示是一个无限循环小数0.0001100110011001100110011001100110011001100110011...
- 正规化:
- 把它写成二进制科学计数法:
1.10011001100110011001101 × 2^-4
(精确到23位)
- 把它写成二进制科学计数法:
- IEEE 754 表示:
- 符号位:
0
(正数) - 指数位:
-4 + 127 = 123
,二进制为01111011
- 尾数位:
10011001100110011001101
(省略隐含的前导 1)
- 符号位:
所以,0.1f
在 IEEE 754 单精度浮点数中的表示是:
0 01111011 10011001100110011001101
3. 计算 1.0e+7f + 0.1f
当你计算 1.0e+7f + 0.1f
时,需要对齐它们的指数部分:
-
对齐指数:
1.0e+7f
的指数为23
,0.1f
的指数为-4
。为了相加,需要将0.1f
的尾数右移 27 位(因为23 - (-4) = 27
)。
-
对齐后的
0.1f
:- 对齐后的
0.1f
尾数变为:0.00000000000000000000000
(所有位右移到最右边,影响被完全忽略)
- 对齐后的
-
相加:
- 对齐后实际相加的尾数变为:
1.00110001001011010000000 + 0.00000000000000000000000
,结果仍然是1.00110001001011010000000
。
- 对齐后实际相加的尾数变为:
结论
由于 0.1f
相对于 1.0e+7f
来说太小,在对齐指数位后,它的尾数几乎完全被移出了有效数字的范围,导致它的影响被完全忽略。因此,1.0e+7f + 0.1f
的结果仍然是 1.0e+7f
。