我一直在查看一些代码,它们有一个奇怪的优化错误,在这个过程中,偶然发现了 strtod() 中的错误条件,它与 strtof() 具有不同的行为,就在非正规值的边缘。 strtof() 的行为对我来说似乎完全合理,但 strtod() 则不然!具体来说,它对于输入值“-0x1.ffffffffffffffp-1023”返回-0.0。
这是在“-0x1.ffffffffffffep-1023”表示中设置为 1 的一个额外位,它可以正确解码。更奇怪的是,添加额外的尾随数字会得到一个值 2^{-1018},我无法解释。在我看来,从分母到普通浮点数转换的特殊边缘情况处理不正确,导致值为零。
任何人都可以解释额外的额外数字引起的另一个奇怪的数字吗?
MSC 2022 和 Intel 2023 上的故障相同
用于双打和输出的示例代码 MRE(浮点数如您所期望的那样工作)
// strtod() fails to handle edge case overflow from denorms correctly
//
// Problem manifests on both MS 2022 and Intel 2023 compilers so by design? but why???
//
// using Windows 11 and Microsoft Visual Studio Community 2022 (64-bit) - Version 17.1.0
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
void Show(const char* name, int err, double arg)
{
unsigned iarg[2];
memcpy(&iarg, &arg, sizeof arg);
printf("\n\"%25s\" decoded errno=%i as 0x%08x%08x %.13a %30.22g", name, err, iarg[1], iarg[0], arg, arg);
}
void DecodeShow(const char* value)
{
char* stopstr;
double arg;
errno = 0;
arg = strtod(value, &stopstr);
Show(value, errno, arg);
}
int main(void)
{
printf("Test of doubles near denorm boundary\n");
DecodeShow("-0x1.ffffffffffffp-1023");
DecodeShow("-0x1.ffffffffffffep-1023");
DecodeShow("-0x1.ffffffffffffe8p-1023");
DecodeShow("-0x1.fffffffffffff0p-1023"); // hard fail == -0.0 !
DecodeShow("-0x1.fffffffffffff8p-1023");
DecodeShow("-0x1.ffffffffffffffp-1023");
DecodeShow("-0x1.fffffffffffffffp-1023");
printf("\n");
DecodeShow("0x1.ffffffffffffe8p-1023");
DecodeShow("0x1.fffffffffffff0p-1023"); // hard fail == +0.0
DecodeShow("0x1.fffffffffffff8p-1023");
DecodeShow("0x1.ffffffffffffffp-1023");
printf("\n");
DecodeShow("0x1.ffffffffffffep-1022");
DecodeShow("-0x1.fffffffffffffp-1022");
DecodeShow("-0x1.fffffffffffff8p-1022");
DecodeShow("-0x1.ffffffffffffffp-1022");
}
故障边界周围的选定输出:
" -0x1.ffffffffffffep-1023" decoded errno=0 as 0x800fffffffffffff -0x0.fffffffffffffp-1022 -2.225073858507200889025e-308
"-0x1.ffffffffffffe8p-1023" decoded errno=0 as 0x800fffffffffffff -0x0.fffffffffffffp-1022 -2.225073858507200889025e-308
"-0x1.fffffffffffff0p-1023" decoded errno=0 as 0x8000000000000000 -0x0.0000000000000p+0 -0
"-0x1.fffffffffffff8p-1023" decoded errno=0 as 0x8040000000000000 -0x1.0000000000000p-1019 -1.780059086805761106472e-30
我认为超出机器精度的额外尾随数字不应从根本上改变 strtod 像这样解码的浮点值。我无法通过使用十进制数字输入字符串引发同样的失败 - 跨越边界的转换似乎表现良好(尽管我不能排除一个我尚未找到的点值不起作用)。
[...] strtod() 的行为[似乎不合理]!具体来说,它对于输入值“-0x1.ffffffffffffffp-1023”返回-0.0。
我不同意这个具体结果是不合理的,但我倾向于同意你提出的结果组合似乎不合理。
"-0x1.fffffffffffffp-1023"
不应转换为其大小小于 "-0x1.ffffffffffffep-1023"
转换为的值的值。然而,鉴于前者不能完全用 IEEE-754 二进制 64 格式表示,而后者则可以(作为次正规),因此将两者转换为相同的结果是合理的。如果结果是 -0.0
,那将是合理的——而且肯定不违反语言规范。这是包含精确算术结果的两个归一化双精度之一。事实上,-0.0
正是我所期望的,当舍入模式为FE_TOWARDZERO
时,舍入到标准化数字。
然而,您的
"-0x1.fffffffffffff8p-1023"
结果更令人担忧。规范要求结果正确舍入,并且我不认为您报告的结果可以满足该要求。
任何人都可以解释额外的额外数字引起的另一个奇怪的数字吗?
从某种意义上说:
strtod()
将"-0x1.fffffffffffff8p-1023"
转换为-0x1.0000000000000p-1019
是C实现中错误的表现。可能与 -0.0
的 "-0x1.fffffffffffffp-1023"
结果相同。我对可能存在的实现缺陷有一些模糊的想法,但这并不重要,我选择不推测这个答案。