为什么 printf 和 isnan 不同意 long double 值是否为 NaN?

Why do printf and isnan disagree whether a long double value is a NaN?

考虑以下代码:

#include <math.h>
#include <stdio.h>
#include <string.h>

int main() {
  __uint128_t n = (__uint128_t(0x00007ffff7dd6e65ULL) << 64) |
                               0x63696c400a2d2d21ULL;
  long double d = 0;
  memcpy(&d, &n, sizeof(long double));
  printf("%d\n", isnan(d));
  printf("%Le\n", d);
}

在 macOS 上使用 clang 版本 12.0.0 (clang-1200.0.32.29) 和 运行 编译时,它会产生以下输出:

1
3.345927e+3575

为什么 isnan 将此 long double 报告为 NaN 而 printf 将其打印为 3.345927e+3575

iostreams 和 clang++ 也是如此:

std::cout << d; // prints 3.34593e+3575

具体来说,为什么在处理这个数字(看起来是 unnormal extended precision number)时,不同的 C 和 C++ API 在行为上存在差异?

由128位整数初始化形成的对象无效,因为其显式有效位与其他位不匹配。

Apple Clang 使用英特尔的 80 位浮点格式。根据 Intel 64 and IA-32 Architectures Software Developer's Manual(2017 年 12 月)4.2.2,对于无穷大、正规数和 NaN,“整数”位明确设置为 1,并且0 表示次正规和零。 Integer1位是有效数的前导位,编码中的第63位。

有效数的64位在128位整数的低位,题中代码设置为63696c400A2D2D2116。其中,第 63 位为 0(高位 6 为 01102)。由于指数字段是 6E6516(位 79 到 64),这应该是一个正常的数字,所以位 63 应该是 1.

我没有看到整数位设置不正确时的行为规范,因此我们可能认为它没有定义。 (有人可能想知道这是否是 isnan 的故意行为,因为它“正确地”报告无效编码不是数字。)

0x6369… 更正为 0xE369… 时,程序正确报告该值不是 NaN。

脚注

1 之所以这样称呼是因为有效数通常表示为b.bbbbbb,其中前导位是小数点仅剩的一位,因此是唯一表示整数值的位。有效数的其余位是小数位。