理解整数到浮点数的转换

7

有人可以解释一下在32位机器上出现的这个奇怪的输出吗?

#include <stdio.h>

int main() {
  printf("16777217 as float is %.1f\n",(float)16777217);
  printf("16777219 as float is %.1f\n",(float)16777219);

  return 0;
}

输出

16777217 as float is 16777216.0
16777219 as float is 16777220.0

奇怪的是,16777217 转换为较低的值,而 16777219 转换为较高的值...


3
我假设你选择了非常具体的数字,那么你应该知道一个浮点数只用了24个二进制位来存储确切的数字,除此之外,你仅能根据指数和分数位能够存储能够精确表示为二进制的数字。如果你需要存储更大的数字,你需要接受精度损失。 - Retired Ninja
2
@WeatherVane,你的链接确实更好。我的意思是,了解float类型的机制使得这个问题不再需要;即将一个变量向上转换和向下转换也并不令人惊讶。 - Yunnosch
2
有一个非常好的答案解释了那个问题。 - Weather Vane
2
这与问题有关,但并没有直接回答 OP 的问题。 - Jean-François Fabre
@Jean-FrançoisFabre 这并不意味着那样。只是没有人提出一个可行的重复问题。 - Antti Haapala -- Слава Україні
显示剩余10条评论
4个回答

16
在IEEE-754基本的32位二进制浮点数格式中,所有从-16,777,216到+16,777,216的整数都是可表示的。从16,777,216到33,554,432,只有偶数是可表示的。接下来,从33,554,432到67,108,864,只有4的倍数是可表示的。(由于问题不需要讨论哪些数字是可表示的,因此我将省略解释并将其视为已知。)
最常见的默认舍入模式是将精确的数学结果四舍五入为最近的可表示值,并在出现“平局”的情况下,将其舍入为在其有效数字中低位为零的可表示值。
16,777,217处于两个可表示值16,777,216和16,777,218之间的等距位置。这些值分别表示为1000000000000000000000002•21和1000000000000000000000012•21。前者在其有效数字的低位为0,因此被选择作为结果。
16,777,219处于两个可表示值16,777,218和16,777,220之间的等距位置。这些值分别表示为1000000000000000000000012•21和1000000000000000000000102•21。后者在其有效数字的低位为0,因此被选择作为结果。

3
你可能听说过“精度”这个概念,比如“这个小数有3位精度”。
在定点表示中,这很容易理解。如果我有三位小数精度,那么我可以准确地表示1/2 = 0.5,我可以准确地表示1/4 = 0.25,我可以准确地表示1/8 = 0.125,但是如果我尝试表示1/16,我无法得到0.0625;我将不得不选择0.062或0.063。
但是这只适用于定点。你正在使用的计算机使用的是浮点数,它很像科学记数法。你获得一定数量的有效数字,而不仅仅是小数点右边的数字。例如,在浮点格式中具有3位小数位数的精度时,您可以表示0.123但不能表示0.1234,您可以表示0.0123和0.00123,但不能表示0.01234或0.001234。如果左边的小数点有数字,则会减少您可以在小数点右边使用的数字。您可以使用1.23但不能使用1.234,并且可以使用12.3但不能使用12.34,以及123.0但不能使用123.4或123.anythingelse。
并且 - 您现在可能已经看到模式了 - 如果您使用带有仅三个有效数字的浮点格式,则无法完全准确地表示所有大于999的数字,即使它们没有小数部分。您可以表示1230但不能表示1234,并且可以表示12300但不能表示12340。
所以这就是十进制浮点格式。另一方面,您的计算机使用二进制浮点格式,这最终变得有些棘手。我们没有精确的十进制数字位数的精度,无法准确地表示不能成为10或100的很好倍数的数字。
特别是,在大多数机器上,类型float具有24个二进制位的精度,这相当于6-7个十进制数字的精度。这显然不足以处理像16777217这样的数字。
那么数字16777216和16777220来自哪里?正如Eric Postpischil已经解释的那样,这是因为它们是2的倍数。如果我们查看附近数字的二进制表示,模式就变得清晰了。
16777208     111111111111111111111000
16777209     111111111111111111111001
16777210     111111111111111111111010
16777211     111111111111111111111011
16777212     111111111111111111111100
16777213     111111111111111111111101
16777214     111111111111111111111110
16777215     111111111111111111111111
16777216    1000000000000000000000000
16777218    1000000000000000000000010
16777220    1000000000000000000000100

16777215是24位二进制能够准确表示的最大数字。在这之后,只能表示偶数,因为低位的第25位必须为0。


1
浮点数的表示方法类似于我们在日常生活中使用的指数表示法。这是一个数字,使用我们决定足以真实地表示该值的一些数字,我们称之为尾数或有效数字,我们将其乘以一个基数或基数值升高到一个幂,我们称之为指数。简单地说:
num*base^exp

我们通常使用10作为基数,因为我们手上有10个手指,所以我们习惯于像1e2这样的数字,它表示100=1*10^2
当然,对于这么小的数字,我们很遗憾地使用指数表示法,但是在处理非常大的数字时,或者更好的是,当我们的数字具有足够代表我们要评估的实体的位数时,我们更喜欢使用它。
正确的位数可能是我们可以用头脑处理的位数,或者是工程应用所需的位数。当我们决定需要多少位数时,我们将不再关心我们即将处理的数字表示与真实值的粘着度。也就是说,对于像123456.789e5这样的数字,如果添加99个单位,我们就能容忍舍入表示,并且仍然认为它是可接受的,否则我们应该改变表示并使用适当位数的不同表示,例如 12345678900
在计算机中,当你需要处理非常大的数字,无法适应标准整数时,或者当你需要表示一个带有小数部分的实数时,正确的选择是使用浮点或双精度浮点表示。它使用与上述讨论相同的布局,但基数为2而不是10。这是因为计算机只有两个手指状态0或1。所以我们之前使用的表示100的公式变成了:
100100*2^0

那仍然不是真正的浮点表示,但可以给出一个想法。现在考虑在计算机中,浮点格式是标准化的,对于标准浮点,根据IEE-754,它使用以下内存布局(我们将在后面看到为什么假定尾数多1位):23位尾数,1位符号和8位指数偏差为-127(这意味着它将在-126+127之间变化而无需符号位,并且值0x000xff保留用于特殊含义)。
现在考虑使用0作为指数,这意味着值2^exponent=2^0=1乘以尾数会给出与23位整数相同的行为。这意味着像这样递增计数:
float f = 0;
while(1)
{
    f +=1;
    printf ("%f\n", f);
}

你会发现打印的值线性增加,直到饱和23位,指数将开始增长。
如果我们浮点数的基数或基数为10,那么在前100个(10^2)值中,我们将看到每10个循环增加一次,然后在接下来的1000个(10^3)值中增加100。您可以看到这对应于我们必须进行的截断,因为可用数字数量有限。
使用二进制基数时也会观察到同样的现象,只是更改发生在2的幂间隔上。
到目前为止我们讨论的被称为浮点数的非规范化形式,通常使用的是其对应的规范化形式。后者简单地意味着有一个未存储的第24位始终为1。换句话说,我们不会对小于2^24的数字使用指数为0,但我们会将其移位(乘以2)达到MSbit == 1的24位,然后调整指数到足以强制转换将数字向后移回其原始值的负值。

记得我们之前提到的指数的保留值吗?当 exponent==0x00 时,意味着我们有一个非规格化数。当 exponent==0xff 时,表示一个 nan(非数字)或者如果 mantissa==0 则为 +/-infinity。

现在应该清楚了,当我们表达的数字超出了24位有效数字(尾数),我们应该期望根据我们距离 2^24 的距离来近似实际值。

现在你正在使用的数字正好处于 2^24=16,277,216 的边缘:

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0|1|0|0|1|0|1|1|0|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1| = 16,277,215
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 s\______ _______/\_____________________ _______________________/
 i       v                              v
 g   exponent                        mantissa
 n

Now increasing by 1 we have:

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0|1|0|0|1|0|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0| = 16,277,216
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 s\__ exponent __/\_________________ mantissa __________________/

请注意,我们已经将第24位触发为1,但从现在开始,我们已经超过了24位表示,每个可能的进一步表示都是以2^1=2的步长进行的。只需每次前进2或者可以表示仅为偶数(2^1=2的倍数)。也就是说,将最低有效位设置为1,我们有:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0|1|0|0|1|0|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1| = 16,277,218
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 s\__ exponent __/\_________________ mantissa __________________/

再次增加:

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0|1|0|0|1|0|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0| = 16,277,220
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 s\__ exponent __/\_________________ mantissa __________________/

正如您所看到的,我们无法准确地表示16,277,219。在您的代码中:
// This will print 16777216, because 1 increment isn't enough to
// increase the significant that can express only intervals
// that are > 2^1
printf("16777217 as float is %.1f\n",(float)16777217);
// This will print 16777220, because an increment of 3 on
// the base 16777216=2^24 will trigger an exponent increase rounded
// to the closer exact representation
printf("16777219 as float is %.1f\n",(float)16777219);

如上所述,数字格式的选择必须适合使用情况,浮点数仅是实数的近似表示,并且我们有责任仔细使用正确的类型。
如果需要更高的精度,可以使用双精度或长整型。
为了完整起见,我想补充一下对于不可约分数的近似表示。这些数字不能被2的分数整除,因此在浮点格式中的表示将始终不精确,并且需要在转换为十进制表示时四舍五入到正确的值。
有关更多详细信息,请参见:

在线演示应用程序:


1

类型float不能容纳太多的有效数字。尾数只能容纳24位。其中23位被存储,第24位为1且未存储,因为尾数已被归一化。

阅读此文档,其中提到"在[-16777216,16777216]范围内的整数可以被精确表示",但您的数值超出了该范围。


2
这并没有解释为什么结果是16,777,216和16,777,220,而不是16,777,218和16,777,220或其他任何数字。 - Eric Postpischil

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接