C double中如何表示无穷大?

15

我从书籍计算机系统:程序员的视角中学到,IEEE标准要求使用以下64位二进制格式来表示双精度浮点数:

  • s:1位符号位
  • exp:11位指数位
  • frac:52位小数位

+无穷大被表示为以下特殊值:

  • s = 0
  • 所有的exp位都是1
  • 所有的frac位都是0

我认为完整的64位双精度应该按照以下顺序:

(s)(exp)(frac)

因此我编写了以下C代码进行验证:

//Check the infinity
double x1 = (double)0x7ff0000000000000;  // This should be the +infinity
double x2 = (double)0x7ff0000000000001; //  Note the extra ending 1, x2 should be NaN
printf("\nx1 = %f, x2 = %f sizeof(double) = %d", x1,x2, sizeof(x2));
if (x1 == x2)
    printf("\nx1 == x2");
else
    printf("\nx1 != x2");

但结果是:

x1 = 9218868437227405300.000000, x2 = 9218868437227405300.000000 sizeof(double) = 8
x1 == x2

为什么这个数字是有效的数字,而不是无穷大错误?

为什么x1等于x2?

(我正在使用MinGW GCC编译器。)

ADD 1

我修改了以下代码并成功验证了无穷大和NaN。

//Check the infinity and NaN
unsigned long long x1 = 0x7ff0000000000000ULL; // +infinity as double
unsigned long long x2 = 0xfff0000000000000ULL; // -infinity as double
unsigned long long x3 = 0x7ff0000000000001ULL; // NaN as double
double y1 =* ((double *)(&x1));
double y2 =* ((double *)(&x2));
double y3 =* ((double *)(&x3));

printf("\nsizeof(long long) = %d", sizeof(x1));
printf("\nx1 = %f, x2 = %f, x3 = %f", x1, x2, x3); // %f is good enough for output
printf("\ny1 = %f, y2 = %f, y3 = %f", y1, y2, y3);

结果是:

sizeof(long long) = 8
x1 = 1.#INF00, x2 = -1.#INF00, x3 = 1.#SNAN0
y1 = 1.#INF00, y2 = -1.#INF00, y3 = 1.#QNAN0

详细的输出看起来有点奇怪,但我认为重点很清楚。

PS.:似乎指针转换并不是必要的。 只需使用%f告诉printf函数将unsigned long long变量解释为double格式。

ADD 2

出于好奇,我使用以下代码检查了变量的位表示。

typedef unsigned char *byte_pointer;

void show_bytes(byte_pointer start, int len)
{
    int i;
    for (i = len-1; i>=0; i--)
    {
        printf("%.2x", start[i]);
    }
    printf("\n");
}

我尝试了以下代码:

//check the infinity and NaN
unsigned long long x1 = 0x7ff0000000000000ULL; // +infinity as double
unsigned long long x2 = 0xfff0000000000000ULL; // -infinity as double
unsigned long long x3 = 0x7ff0000000000001ULL; // NaN as double
double y1 =* ((double *)(&x1));
double y2 =* ((double *)(&x2));
double y3 = *((double *)(&x3));

unsigned long long x4 = x1 + x2;  // I want to check (+infinity)+(-infinity)
double y4 = y1 + y2; // I want to check (+infinity)+(-infinity)

printf("\nx1: ");
show_bytes((byte_pointer)&x1, sizeof(x1));
printf("\nx2: ");
show_bytes((byte_pointer)&x2, sizeof(x2));
printf("\nx3: ");
show_bytes((byte_pointer)&x3, sizeof(x3));
printf("\nx4: ");
show_bytes((byte_pointer)&x4, sizeof(x4));

printf("\ny1: ");
show_bytes((byte_pointer)&y1, sizeof(y1));
printf("\ny2: ");
show_bytes((byte_pointer)&y2, sizeof(y2));
printf("\ny3: ");
show_bytes((byte_pointer)&y3, sizeof(y3));
printf("\ny4: ");
show_bytes((byte_pointer)&y4, sizeof(y4));

输出结果为:

x1: 7ff0000000000000

x2: fff0000000000000

x3: 7ff0000000000001

x4: 7fe0000000000000

y1: 7ff0000000000000

y2: fff0000000000000

y3: 7ff8000000000001

y4: fff8000000000000  // <== Different with x4

奇怪的是,尽管x1和x2与y1和y2具有相同的比特模式,但总和x4与y4不同。

printf("\ny4=%f", y4);

给出这个:

y4=-1.#IND00  // What does it mean???

它们为什么不同?y4是如何获得的?


10
因为你设置的是“值”,而不是“表示”。 - Oliver Charlesworth
什么是“无限错误”? - Lightness Races in Orbit
4个回答

20

首先,0x7ff0000000000000 确实是双精度无穷大的比特表示。但强制类型转换并不设置比特表示,它将被解释为一个64位整数的逻辑值进行转换。因此,您需要使用其他方式来设置比特模式。

设置比特模式的直接方法是:

uint64_t bits = 0x7ff0000000000000;
double infinity = *(double*)&bits;

然而,这是未定义的行为。C标准禁止将已存储为一种基本类型( uint64_t )的值读取为另一种基本类型( double )。这被称为严格别名规则,允许编译器生成更好的代码,因为它可以假设对一种类型的读取和对另一种类型的写入的顺序无关紧要。
唯一的例外是 char 类型:您可以明确地将任何指针转换为 char * 并返回。因此,您可以尝试使用此代码:
char bits[] = {0x7f, 0xf0, 0, 0, 0, 0, 0, 0};
double infinity = *(double*)bits;

尽管这不再是未定义行为,但它仍然是"实现定义的行为": double类型中字节的顺序取决于您的计算机。给定的代码适用于像ARM和Power家族这样的大端机器,但不适用于X86。对于X86,您需要使用以下版本:
char bits[] = {0, 0, 0, 0, 0, 0, 0xf0, 0x7f};
double infinity = *(double*)bits;

由于机器不保证以相同顺序存储浮点数和整数值,所以真的没有绕过此实现定义行为的方法。甚至有一些机器使用像这样的字节顺序: <1, 0, 3, 2> 我甚至不想知道是谁想出了这个绝妙的主意,但它确实存在,我们必须接受它。


回答你的最后一个问题:浮点算术与整数算术本质上是不同的。位具有特殊含义,而浮点单元将其考虑在内。特别是像无穷大、NAN 和非规范化数这样的特殊值会被特殊处理。由于 +inf + -inf 的结果定义为产生 NAN,因此您的浮点单元会发射 NAN 的比特模式。整数单元不知道无穷大或 NAN,因此它只是将比特模式解释为巨大的整数,并愉快地执行整数加法(在这种情况下会溢出)。得到的比特模式不是 NAN 的那种。它碰巧是一个真正巨大的正浮点数的比特模式(2^1023,确切地说),但这毫无意义。


实际上,有一种方法可以以可移植的方式设置除 NAN 之外的所有值的位模式:给定三个变量,包含符号、指数和尾数的比特,您可以这样做:

uint64_t sign = ..., exponent = ..., mantissa = ...;
double result;
assert(!(exponent == 0x7ff && mantissa));    //Can't set the bits of a NAN in this way.
if(exponent) {
    //This code does not work for denormalized numbers. And it won't honor the value of mantissa when the exponent signals NAN or infinity.
    result = mantissa + (1ull << 52);    //Add the implicit bit.
    result /= (1ull << 52);    //This makes sure that the exponent is logically zero (equals the bias), so that the next operation will work as expected.
    result *= pow(2, (double)((signed)exponent - 0x3ff));    //This sets the exponent.
} else {
    //This code works for denormalized numbers.
    result = mantissa;    //No implicit bit.
    result /= (1ull << 51);    //This ensures that the next operation works as expected.
    result *= pow(2, -0x3ff);    //Scale down to the denormalized range.
}
result *= (sign ? -1.0 : 1.0);    //This sets the sign.

这个方法使用浮点数单元本身来将位移动到正确的位置。由于无法使用浮点算术与NAN的尾数位进行交互,因此不可能在此代码中包含生成NAN的过程。你可以生成一个NAN,但是无法控制其尾数位模式。


@cmaster 无法使用 htonntoh 系列函数来确定字节顺序(或至少使其保持一致)。 - clcto
1
@clcto ntohd()可以解决问题,但据我所知,它不是POSIX标准的一部分。 glibc似乎只实现了ntohs()ntohl(),两者仅适用于整数。由于整数可能使用与浮点数不同的字节顺序,因此这甚至不足以设置float的位。 - cmaster - reinstate monica
鉴于David Hammen在下面引用了相关章节和诗句,可能需要提供引文来支持“C标准禁止……”的观点。 - dmckee --- ex-moderator kitten
尽管这不再是未定义行为,但它仍然是实现定义的行为。不,它仍然是未定义行为。&bits不是 double*,因此不能从 char* 进行转换。在现实生活中,这意味着当对齐不匹配或编译器优化决定做有趣的事情时,代码会崩溃。请改用 unionmemcpy - user694733

8

初始化

double x1=(double)0x7ff0000000000000;

将整数字面量转换为double。您可能希望共享位表示。这是实现特定的(可能是未指定的行为),但您可以使用联合:

union { double x; long long n; } u;
u.n = 0x7ff0000000000000LL;

然后使用u.x; 我假设在您的机器上,long longdouble都是64位。同时endianessfloating point表示也很重要。
另请参阅http://floating-point-gui.de/ 请注意,并非所有处理器都是x86,也不是所有浮点实现都是IEEE754(即使在2014年,大多数都是)。例如,在您的平板电脑上可能无法正确运行您的代码,因为它使用的是ARM处理器。

4
这合法吗?我记得C规范说从联合体成员读取数据时,只能读取最后一个被写入的成员,其他成员都是未定义行为......但可能我在自作多情。 - dreamlax
2
即使在联合体中,它也违反了严格别名规则。编译器允许在写入之前执行读取操作。然后,它们可以优化写入操作,因为数据从未被读取等。这绝对是未定义行为。 - cmaster - reinstate monica
3
虽然在C++中这是未定义行为,但是*此问题的标签不是C ++*,而是C。通过联合体实现类型装换在C中是合法的,不违反C的严格别名规则。你应该针对使用强制转换的答案表达不满,因为这在C和C ++中都是未定义行为。 - David Hammen
2
@cmaster - 在C90中,存储一个共用体(union)成员的一种类型并检索另一种类型的成员是未定义的行为。编译器供应商收到了如此多的投诉,以至于许多人恢复了这种广泛使用的ANSI之前的行为。C99标准删除了这个措辞。C11标准保留了C99的措辞,并添加了一个注释:如果用于读取共用体对象内容的成员与最后用于在对象中存储值的成员不相同,则将该值的对象表示的适当部分重新解释为新类型中的对象表示,如6.2.6所述。 - David Hammen
2
还可以参考https://dev59.com/P2gu5IYBdhLWcg3wRlL0和https://dev59.com/-2gu5IYBdhLWcg3wBym1(以及其他)。 - David Hammen
显示剩余9条评论

8
您将该值转换为double类型,这不会按照您的期望工作。
double x1=(double)0x7ff0000000000000; // Not setting the value directly

为了避免这个问题,您可以将该值解释为双指针并取消引用它(尽管这是极不推荐的,并且仅适用于unsigned long long == double size约束):
unsigned long long x1n = 0x7ff0000000000000ULL; // Inf
double x1 = *((double*)&x1n);
unsigned long long x2n = 0x7ff0000000000001ULL; // Signaling NaN
double x2 = *((double*)&x2n);

printf("\nx1=%f, x2=%f sizeof(double) = %d", x1, x2, sizeof(x2));
if (x1 == x2)
    printf("\nx1==x2");
else
    printf("\nx1!=x2"); // x1 != x2

在ideone上的示例


1
如果不依赖未定义行为,这个答案将更好。 (例如, double x1 = *((double*)&x1n);) - David Hammen
有一个免责声明仅供说明目的,但是不要在家里尝试 - Marco A.

6
你已经将常量0x7ff00...转换为double类型。但这并不等同于把该值的位表示解释为double类型。这也解释了为什么 x1==x2。当你将其转为double类型时,会失去精度;因此对于一些大整数,你最终得到的double值在两种情况下是相同的。这会导致一些奇怪的效果,例如对于一个大的浮点数,加上1后它仍然不变。

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