“long double”和“std::uintmax_t”之间的转换会失去整数的精度

Conversion between `long double` and `std::uintmax_t` loses precision on whole numbers

提问人:Piotr Siupa 提问时间:2/18/2023 最后编辑:Piotr Siupa 更新时间:4/6/2023 访问量:151

问:

我创建了一个类,允许拆分为整数值和二进制指数(用于一些精确的计算)。long double

我的问题很难重现,因为该类通常运行良好,但在我测试过的一台特定机器上,它在每次转换时都会丢失一些最低有效位。(稍后会详细介绍。

这是代码。(它需要保留在单独的文件中才能发生此错误。

SplitLD.hh:

#include <cstdint>

// Splits `long double` into an integer and an exponent.
class SplitLD
{
public: // Everything is public to make the example easier to test.
    std::uintmax_t integer;
    int exponent;

    SplitLD(const long double number);
    operator long double() const;
};

SplitLD.cc:

#include <cfloat>
#include <cmath>
#include <limits>
#include <climits>
#include "SplitLD.hh"

SplitLD::SplitLD(long double number) // For the sake of simplicity, we ignore negative numbers and various corner cases.
{
    static_assert(FLT_RADIX == 2);
    static_assert(sizeof(std::uintmax_t) * CHAR_BIT >= std::numeric_limits<long double>::digits);
    // The following two operations change the exponent to make the represented value a whole number.
    number = std::frexp(number, &exponent);
    number = std::ldexp(number, std::numeric_limits<long double>::digits);
    exponent -= std::numeric_limits<long double>::digits;
    integer = number; // cast from `long double` to `std::uintmax_t`
}

SplitLD::operator long double() const
{
    long double number = integer; // cast from `std::uintmax_t` to `long double`
    number = std::ldexp(number, exponent);
    return number;
}

main.cc:

#include "SplitLD.hh"

int main()
{
    const long double x = 12345.67890123456789l; // arbitrarily chosen number for the test
    const SplitLD y = x;
    const long double z = y;
    return z == x ? 0 : 1;
}

如果您尝试运行此代码,它可能会正常工作。 但是,我有一台机器,可以始终如一地重现问题。

(可能)触发错误的条件如下:

  • 浮点类型必须为 。我试过了,它们似乎工作正常。long doublefloatdouble
  • GCC 和 Clang 的行为相似,我可以在两者上重现该问题。
  • 如果我将所有代码放入一个文件中,它就会开始工作,这可能是因为在编译过程中内联或计算了函数。
  • 我在 Ubuntu 的 WSL(适用于 Linux 的 Windows 子系统)上遇到了错误。
  • 这可能与硬件配置有关。

我尝试打印数字的二进制表示形式(格式化以提高可读性)。 (我很确定第二组是符号,第三组是指数,第四组是尾数。我不确定第一组是什么,但它可能只是填充。

通常二进制值如下(因为我只打印):yinteger

x 000000000000000000000000000000000000000000000000'0'100000000001100'1100000011100110101101110011000111100010100111101011101110000010
y                                                                    1100000011100110101101110011000111100010100111101011101110000010
z 000000000000000000000000000000000000000001000000'0'100000000001100'1100000011100110101101110011000111100010100111101011101110000010

但是,当发生错误时,它们如下所示:

x 000000000000000001111111100110001001110111101001'0'100000000001100'1100000011100110101101110011000111100010100111101011101110000010
y                                                                    1100000011100110101101110011000111100010100111101011110000000000
z 000000000000000001111111100110001001110111101001'0'100000000001100'1100000011100110101101110011000111100010100111101100000000000000

什么原因会导致此问题?

程序格式是否正确? 某处是否有 UB 或任何东西允许编译器进行一些奇怪的优化?

这是一个现场演示。但是,它的效用非常有限,因为它可以正常工作。 (它包括打印二进制表示的代码,此处省略了该代码,以免使示例太长。


更新1:

我修改了测试程序,以便在每次操作后打印二进制数据,以确定哪个确切指令导致数据丢失。 看起来有罪的指令是专门分配给 to 和 to 的。 既没有,也没有改变尾数。long doublestd::uintmax_tstd::uintmax_tlong doublestd::frexpstd::ldexp

以下是它在发生错误的计算机上的外观:

========== `long double` to `std::uintmax_t` ==========
Initial `long double`
000000000000000001111111001100101001101100000010'0'100000000001100'1100000011100110101101110011000111100010100111101011101110000010
Calling `frexp`...
000000000000000001111111001100101001101100000010'0'011111111111110'1100000011100110101101110011000111100010100111101011101110000010
Calling `ldexp`...
000000000000000001111111001100101001101100000010'0'100000000111110'1100000011100110101101110011000111100010100111101011101110000010
Converting to `std::uintmax_t`
                                                                   1100000011100110101101110011000111100010100111101011110000000000
========== `std::uintmax_t` to `long double` ==========
Initial `std::uintmax_t`
                                                                   1100000011100110101101110011000111100010100111101011110000000000
Converting to `long double`
000000000000000000000000000000000000000000000000'0'100000000111110'1100000011100110101101110011000111100010100111101100000000000000
Calling `ldexp`
000000000000000000000000000000000000000000000000'0'100000000001100'1100000011100110101101110011000111100010100111101100000000000000

更新2:

看起来问题与 WSL 有关。 当代码在实时 Linux 系统或虚拟机中的 Linux 上运行时,该代码在同一台计算机上正常工作。 我无法在 Windows 中安装编译器来测试它。

C++ 转换 浮点长 双精度

评论

1赞 Ben Voigt 2/18/2023
@MooingDuck:在我看来std::uintmax_t
0赞 Piotr Siupa 2/18/2023
@MooingDuck 在我正在测试的机器上,尾数似乎有 64 位(如 80 位),也有 64 位。我什至有一个检查整数是否足够大。long doublestd::uintmax_tstatic_assert
0赞 Mooing Duck 2/18/2023
、 和 的左边是什么?因此,这些值是不同的。xyzx
0赞 Piotr Siupa 2/18/2023
@MooingDuck 据我所知,为了更好地对齐,它正在填充变量以使变量为 128 位而不是 80 位。我有93%的把握。在 的情况下,我只是添加了空格以将二进制与浮点的尾数对齐。y
1赞 n. m. could be an AI 2/18/2023
单元测试和所有机器。另外,尝试以防万一。ldexpfrexpldexplfrexpl

答:

0赞 chux - Reinstate Monica 2/19/2023 #1

什么原因会导致此问题?

不同的机器/编译器具有不同的精度。long double


12345.67890123456789l根据精度的不同,存在不同的不清晰位模式的风险。更容易分析十六进制浮点常量的问题,或者可能使用易于理解的常量(如其重复模式):long double4.0L/3

// const long double x = 12345.67890123456789l;

// 4.0L/3
// In binary notation
// Odd number of significant bits
1.010101...010101
// Even number of significant bits
1.010101...0101011

integer = number;是有风险的。( 未定义,但注释暗示 。integeruintmax_t

long double,在各种机器/编译器上有各种风格:64 位、80 位和大小 80 位。80 位,由于填充,大小为 128 位,128 位等。

64:范围从 [-0x1F FFFF FFFF FFFF 到 +0x1F FFFF FFFF FFFF],类似于 .numberint54_t

80:范围从 [-0xFFFF FFFF FFFF FFFF 到 +0xFFFF FFFF FFFF FFFF],类似于 .numberint65_t

128:更宽。

保存不足以使用 80/128 重新创建所有值,并且只有 64 位。integer = numberuintmax_t

评论

0赞 Piotr Siupa 2/20/2023
再看一下我的代码。 是,我也有断言.在我测试的机器上,似乎有 80 位,即 63+1 位尾数。integerstd::uintmax_tstatic_assert(sizeof(std::uintmax_t) * CHAR_BIT >= std::numeric_limits<long double>::digits);long double
0赞 chux - Reinstate Monica 2/20/2023
@PiotrSiupa 即使您断言,63+1 位尾数作为 64 位整数仍然不足以将所有 [-0xFFFF FFFF FFFF FFFF 独特地编码为 +0xFFFF FFFF FFFF FFFF]。这需要 65 位。uintmax_t
0赞 Piotr Siupa 2/20/2023
好的,但让我们假设这个数字是正数,如代码中所述。在这种情况下,我们只需要对范围 [0 到 0xFFFF FFFF FFFF FFFF] 进行编码。
0赞 Piotr Siupa 4/6/2023 #2

在 WSL 上重新安装系统解决了该问题。 这可能是一个已经修复的错误。