是否可以保真地将浮点双精度数往返于两个十进制整数?

Posted

技术标签:

【中文标题】是否可以保真地将浮点双精度数往返于两个十进制整数?【英文标题】:Is it possible to round-trip a floating point double to two decimal integers with fidelity? 【发布时间】:2013-05-14 12:57:43 【问题描述】:

我正在尝试辨别是否可以将双精度 IEEE 浮点值分解为两个整数,然后以 full 保真度重新组合它们。想象一下这样的事情:

double foo = <inputValue>;
double ipart = 0;
double fpart = modf(foo, &ipart);

int64_t intIPart = ipart;
int64_t intFPart = fpart * <someConstant>;

double bar = ((double)ipart) + ((double)intFPart) / <someConstant>;

assert(foo == bar);

逻辑上很明显,任何 64 位量都可以存储在 128 位中(即只存储文字位。)这里的目标是将双精度的整数部分和小数部分分解为整数表示(接口和我无法控制其存储格式的 API)并在重新组合两个 64 位整数时返回一个位精确的双精度数。

我对 IEEE 浮点有一个概念性的理解,并且我知道双精度数是以 2 为基数存储的。根据经验,我观察到,使用上述方法,有时foo != bar 甚至是非常大的&lt;someConstant&gt; 值。我已经离开学校有一段时间了,我无法完全理解在不同的基础(或其他一些因素)下这是否可能。

编辑:

我猜这是在我的大脑中暗示/理解的,但在这里没有捕捉到:在这种情况下,我保证问题中双精度的总体幅度将始终在 +/- 2^63 以内(并且 > 2^ -64)。有了这种理解,整数部分可以保证适合 64 位 int 类型,那么我的期望是,在大约 16 位的十进制精度下,小数部分也应该很容易用 64 位 int 类型表示。

【问题讨论】:

&lt;someConstant&gt; 无法做到这一点,你需要巨大的整数。但是,您可以使用有效和指数。检查frexp 为什么不uint32_t i, f; memcpy(&amp;i, &amp;ipart, 4); memcpy(&amp;f, &amp;fpart, 4); 你想让这些整数有什么特定的含义吗?否则只需将二进制表示复制到uint64_tuint32_t 您需要更清楚地指定您的最终要求。您的问题是“我如何使这种有缺陷的方法发挥作用?”而不是“这是我需要解决的问题,我该如何正确解决?” @ipmcc “十进制整数”到底是什么意思?整数就是整数,除非你说的是二进制编码的十进制,这是另一回事。 【参考方案1】:

如果你知道数字在 [–263, +263) 并且 ULP(数字中最低位的值)至少是2-63,那么你可以使用这个:

double ipart;
double fpart = modf(foo, &ipart);

int64_t intIPart = ipart;
int64_t intFPart = fpart * 0x1p63;

double bar = intIPart + intFPart * 0x1p-63;

如果您只需要几个整数来重构值,而不关心这些整数的含义(例如,其中一个不必是整数部分),那么您可以使用 @ 987654322@将数字反汇编成有效数(带符号)和指数,您可以使用ldexp重新组合:

int exp;
int64_t I = frexp(foo, &exp) * 0x1p53;
int64_t E = exp;

double bar = ldexp(I, E-53);

此代码适用于 IEEE-754 64 位二进制浮点对象的任何有限值。它不支持无穷大或 NaN。

如果您想解决问题,甚至可以将 IE 打包到一个 int64_t 中。

【讨论】:

使用图书馆解决方案很有意义,+1 如果foo 允许负数,则fpart 可以为负数。它的绝对值可以存储在uint64_t 中,因为它总是与ipart 相同的符号,但代码应该是intFPart = fabs(fpart) * 0x1.0p64bar的计算应该涉及到copysign(intFPart * 0x1.0p-64, (double)intIPart) @PascalCuoq:如果ipart 为零,则可能会丢失符号。为简单起见,我将 intFPart 更改为有符号并将支持的范围减少了一位。如果 OP 表示需要更多,我们可以更新它。 谢谢。这是我正在寻找的最接近的答案。【参考方案2】:

这里的目标是分解整数部分和小数部分 将双精度数转换为整数表示

您甚至无法可靠地获得整数部分或小数部分。问题是您似乎误解了浮点数的存储方式。它们没有整数部分和小数部分。它们有一个有效数字部分,称为尾数和一个指数。指数本质上是放大或缩小尾数,类似于科学记数法的工作原理。

双精度浮点数有 11 位的指数,给出的值范围类似于 2-1022...21023。如果要存储整数和小数部分,则需要两个整数,每个整数大约有 210 位。不过,那将是一种愚蠢的做事方式——这些位中的大多数都不会被使用,因为只有尾数中的位是重要的。使用两个非常长的整数可以让您以相同的精度表示整个 double 范围内的所有值,这是您无法使用 double 完成的。例如,您可以有一个非常大的整数部分和一个非常小的小数部分,但这是一个双精度数无法准确表示的数字。

更新

如果您在评论中指出,您知道所讨论的值在 ±263 范围内,您可以使用Extract fractional part of double *efficiently* in C 的答案,如下所示:

double whole = // your original value
long iPart = (long)whole;
double fraction = whole - iPart;
long fPart = fraction * (2 << 63);

我还没有测试过,但它应该可以得到你想要的。

【讨论】:

我非常熟悉它们是如何存储为以 2 为底的分数、指数和符号位的。我想缺少的信息是我正在处理一种特殊的实际情况,在这种情况下,我知道双精度值的大小将始终在 +-2^63 的范围内。 @ipmcc 如果您在问题中包含该信息,那将是体贴的。 @ipmcc 除非丢失的信息还包括知道双精度高于 2^-64,否则只能解决一半问题。【参考方案3】:

查看 wikipedia 了解双精度的格式:

http://en.wikipedia.org/wiki/Double-precision_floating-point_format

IEEE 双精度格式编码三个整数:有效数、指数和符号位。 下面是提取 IEEE 双精度格式的三个组成整数的代码:

double d = 2.0;  

// sign bit
bool s = (*reinterpret_cast<int64_t*>(&d)) >> 63;

// significand
int64_t m = *reinterpret_cast<int64_t*>(&d) & 0x000FFFFFFFFFFFFFULL;

// exponent
int64_t e = ((*reinterpret_cast<int64_t*>(&d) >> 52) & 0x00000000000007FFULL) - 1023;

// now the double d is exactly equal to s * (1 + (m / 2^52)) * 2^e
// print out the exact arithmatic expression for d:

std::cout << "d = " << std::dec << (s ? "-(1 + " : "(1 + (") << m << "/" << (1ULL << 52) << ")) x 2^" << e;

【讨论】:

@PascalCuoq 果然,我没有注意到这个问题被标记为 C。这个答案是 C++,但重要的位在 C++ 的 C 子集中。 当有一个标准库函数用于提取有效数和指数时,为什么要使用不支持的强制转换来访问编码?这就是frexp 的用途。 @EricPostpischil ipmcc 想要双精度的整数表达式。 frexp 并没有做到这一点。 @JamesBrock:但它使您能够以一种简单的受支持方式进行操作。只需乘以0x1p53 并转换为整数,如我的答案所示。

以上是关于是否可以保真地将浮点双精度数往返于两个十进制整数?的主要内容,如果未能解决你的问题,请参考以下文章

如何在没有不必要的十进制 0 的情况下很好地将浮点数格式化为字符串

单精度和双精度浮点值有啥不同? [复制]

《Lua程序设计》之 数值

BigInteger大精度整数

解决“浮点上下文中的整数除法”警告

浮点型是啥意思