从 IEEE 754 浮点数表示法解释浮点数运算损失小数部分的问题


在做 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 So float can represent integers up to 2**24 - 1 == 16777215 which is close to 14385471.

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. 符号位:1(因为是负数)。
  2. 6.5 转换为二进制:110.1
  3. 正规化二进制表示:1.101 x 2^2
  4. 指数部分:2 + 127(偏移量)= 129,二进制为 10000001
  5. 尾数部分:10100000000000000000000(去掉隐含的 1)。

最终表示为:

1 10000001 10100000000000000000000

IEEE 754 浮点数表示法为什么指数要加上偏移量

在浮点数表示中使用偏移量(bias)来表示指数是为了处理指数的正负值。这种方法使得浮点数表示中的指数部分始终为非负数,从而简化了比较和排序操作。

也即,在单精度的情况下,所支持的 2 进制指数范围是 [ − 2 7 , 2 7 − 1 ] [-2^7, 2^7-1] [27,271] ,因为虽然存储浮点数的时候,这 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 就很正常。

为什么使用偏移量

  1. 简化硬件实现

    • 使用非负数可以简化硬件电路的设计,特别是在比较和排序操作中。比较两个带符号位的二进制数(包括负数)的硬件电路要复杂得多。
    • 非负数的比较可以直接使用无符号整数的比较电路,这样更高效。
  2. 统一表示范围

    • 通过使用偏移量,可以统一表示正指数和负指数。例如,在单精度浮点数中,偏移量为 127,这意味着实际指数范围从 -126 到 +127。
    • 指数存储的范围是 0 到 255(8 位),减去偏移量 127 后,实际指数范围是 -127 到 128,其中 -127 和 128 分别用于表示特殊值(如非规格化数、无穷大和 NaN)。

浮点数的精度

以单精度浮点数为例,二进制存储中使用 23 位代表尾数,那么二进制下的小数部分能去到 23 位,也就是 b 1.000 … 001 b1.000\dots 001 b1.000001 ,那么最小的变化量就是 b 0.000 … 001 b0.000\dots001 b0.000001 ,这个二进制小数大概是十进制的多少呢?大约是 2.220446049250313 × 1 0 − 16 2.220446049250313\times 10^{-16} 2.220446049250313×1016 这么多,这就是单精度浮点数表示的十进制精度。

浮点数的尾数部分表示一个二进制小数,其位数决定了精度。例如,在 IEEE 754 单精度浮点数(32 位)中,尾数部分有 23 位;在双精度浮点数(64 位)中,尾数部分有 52 位。

尾数部分的二进制表示

尾数部分可以表示的最小增量是其最后一位,即第 23 位(对于单精度浮点数),或第 52 位(对于双精度浮点数)。

单精度浮点数

单精度浮点数的尾数部分有 23 位。这些位表示的是一个二进制小数,因此其最小增量是 2 − 23 2^{-23} 223 。但是,这并不表示尾数部分的精度,而是表示尾数部分的最小增量。

双精度浮点数

双精度浮点数的尾数部分有 52 位。这些位表示的是一个二进制小数,因此其最小增量是 2 − 52 2^{-52} 252 。同样,这并不表示尾数部分的精度,而是表示尾数部分的最小增量。

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. 十进制数值: 1.0e+7 即 10,000,000
  2. 二进制表示:
    • 10,000,000 的二进制表示为 100110001001011010000000 (23 位)
  3. 正规化:
    • 把它写成二进制科学计数法:1.00110001001011010000000 × 2^23
  4. IEEE 754 表示:
    • 符号位: 0(正数)
    • 指数位: 23 + 127 = 150,二进制为 10010110
    • 尾数位: 00110001001011010000000(省略隐含的前导 1)

所以,1.0e+7f 在 IEEE 754 单精度浮点数中的表示是:

0 10010110 00110001001011010000000
2. 表示 0.1f
  1. 十进制数值: 0.1
  2. 二进制表示:
    • 0.1 的二进制表示是一个无限循环小数 0.0001100110011001100110011001100110011001100110011...
  3. 正规化:
    • 把它写成二进制科学计数法:1.10011001100110011001101 × 2^-4(精确到23位)
  4. 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. 对齐指数

    • 1.0e+7f 的指数为 230.1f 的指数为 -4。为了相加,需要将 0.1f 的尾数右移 27 位(因为 23 - (-4) = 27)。
  2. 对齐后的 0.1f

    • 对齐后的 0.1f 尾数变为:0.00000000000000000000000(所有位右移到最右边,影响被完全忽略)
  3. 相加

    • 对齐后实际相加的尾数变为:1.00110001001011010000000 + 0.00000000000000000000000,结果仍然是 1.00110001001011010000000

结论

由于 0.1f 相对于 1.0e+7f 来说太小,在对齐指数位后,它的尾数几乎完全被移出了有效数字的范围,导致它的影响被完全忽略。因此,1.0e+7f + 0.1f 的结果仍然是 1.0e+7f

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值