【问题标题】:How is infinity represented in a C double?C double 中的无穷大如何表示?
【发布时间】:2014-12-28 14:20:21
【问题描述】:

我从Computer Systems: A Programmer's Perspective一书中了解到,IEEE标准要求双精度浮点数使用以下64位二进制格式表示:

  • s: 1 位符号
  • exp:11 位的指数
  • frac:分数为 52 位

+infinity 表示为具有以下模式的特殊值:

  • s = 0
  • 所有 exp 位都是 1
  • 所有小数位均为 0

我认为 double 的完整 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 编译器。)

添加 1

我修改了如下代码,并成功验证了 Infinity 和 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 函数以double 格式解释unsigned long long 变量。

添加 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是怎么得到的?

【问题讨论】:

  • 因为您设置的是 ,而不是表示。
  • 什么是“无限误差”?

标签: c floating-point


【解决方案1】:

首先,0x7ff0000000000000 确实是双无穷大的位表示。但是强制转换不设置位表示,它将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;

实际上没有办法绕过这个实现定义的行为,因为不能保证机器会以与整数值相同的顺序存储浮点值。甚至有些机器使用这样的字节顺序: 我什至不想知道是谁提出了这个绝妙的主意,但它确实存在,我们必须接受它。


最后一个问题:浮点运算本质上不同于整数运算。这些位具有特殊含义,浮点单元将其考虑在内。特别是像无穷大、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 ntohd() 可以解决问题,但是,afaik,它不是 POSIX 标准的一部分。 glibc 似乎只实现了ntohs()ntohl(),它们都只适用于整数。而且由于整数可能使用与浮点数不同的字节顺序,这甚至不足以设置float 的位。
  • 鉴于 David Hammen quoting chapter and verse 以下的相反意见,“C 标准禁止 [...]” 可能需要引用。跨度>
  • "即使这不再是未定义的行为,它仍然是实现定义的行为" 不,它仍然是 UB。 &amp;bits 不是double*,因此不允许从char* 转换。在现实生活中,这意味着当对齐不匹配或编译器优化决定做一些有趣的事情时,代码会爆炸。请改用unionmemcpy
【解决方案2】:

初始化

double x1=(double)0x7ff0000000000000;

正在整数文字转换为double。您可能想要共享按位表示。这是特定于实现的(可能是unspecified bahavior),但您可以使用联合:

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 处理器上可能无法正常工作,例如在您的平板电脑中。

【讨论】:

  • 这合法吗?我认为 C 规范说,从除最后写入的成员之外的任何联合成员中读取是未定义的行为......但我可能只是在想象事情
  • 即使在联合中也违反了严格的别名规则。允许编译器在写入之前执行读取。然后他们被允许优化写入,因为数据永远不会被读取,等等。绝对是 UB。
  • @cmaster - 虽然这是 C++ 中未定义的行为,但此问题未标记为 C++。它被标记为 C。通过联合的类型双关语在 C 中是合法的,并且不违反 C 的严格别名规则。你应该把你的愤怒指向使用强制转换的答案。那是 C 和 C++ 中的 UB。
  • @cmaster - 存储一种类型的联合成员并检索不同类型的成员是 C90 中的 UB。编译器供应商收到了如此多的投诉,以至于许多人恢复了这种广泛使用的 pre-ANSI 行为。 C99 标准删除了这个措辞。 C11标准保留了C99的措辞并增加了一个脚注,如果用于读取联合对象内容的成员与上次用于在对象中存储值的成员不同,则对象的适当部分值的表示被重新解释为新类型中的对象表示,如 6.2.6 中所述
【解决方案3】:

您将值转换为双精度值,但不会按预期工作。

double x1=(double)0x7ff0000000000000; // Not setting the value directly

为避免此问题,您可以将该值解释为双指针并取消引用它(尽管非常不推荐这样做,并且仅适用于 unsigned long long == 双大小约束):

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

Example on ideone

【讨论】:

  • 如果它不依赖未定义的行为,这个答案会更好。 (例如,double x1 = *((double*)&amp;x1n);
  • 有一个免责声明用于说明目的,但不要在家里这样做
【解决方案4】:

您已常量0x7ff00... 转换为double。这与获取该值的位表示并将其解释为double 完全不同。

这也解释了为什么x1==x2。当您转换为双精度时,您会失去精度;所以有时对于大整数,你最终得到的double 在这两种情况下是相同的。这会给您带来一些奇怪的效果,对于较大的浮点值,加 1 会使其保持不变。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-17
    • 1970-01-01
    • 1970-01-01
    • 2016-04-30
    • 2012-01-28
    • 1970-01-01
    相关资源
    最近更新 更多