提问人:Piotr Siupa 提问时间:2/18/2023 最后编辑:Piotr Siupa 更新时间:4/6/2023 访问量:151
“long double”和“std::uintmax_t”之间的转换会失去整数的精度
Conversion between `long double` and `std::uintmax_t` loses precision on whole numbers
问:
我创建了一个类,允许拆分为整数值和二进制指数(用于一些精确的计算)。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 double
float
double
- GCC 和 Clang 的行为相似,我可以在两者上重现该问题。
- 如果我将所有代码放入一个文件中,它就会开始工作,这可能是因为在编译过程中内联或计算了函数。
- 我在 Ubuntu 的 WSL(适用于 Linux 的 Windows 子系统)上遇到了错误。
- 这可能与硬件配置有关。
我尝试打印数字的二进制表示形式(格式化以提高可读性)。 (我很确定第二组是符号,第三组是指数,第四组是尾数。我不确定第一组是什么,但它可能只是填充。
通常二进制值如下(因为我只打印):y
integer
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 double
std::uintmax_t
std::uintmax_t
long double
std::frexp
std::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 中安装编译器来测试它。
答:
什么原因会导致此问题?
不同的机器/编译器具有不同的精度。long double
12345.67890123456789l
根据精度的不同,存在不同的不清晰位模式的风险。更容易分析十六进制浮点常量的问题,或者可能使用易于理解的常量(如其重复模式):long double
4.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;
是有风险的。( 未定义,但注释暗示 。integer
uintmax_t
long double
,在各种机器/编译器上有各种风格:64 位、80 位和大小 80 位。80 位,由于填充,大小为 128 位,128 位等。
64:范围从 [-0x1F FFFF FFFF FFFF 到 +0x1F FFFF FFFF FFFF],类似于 .number
int54_t
80:范围从 [-0xFFFF FFFF FFFF FFFF 到 +0xFFFF FFFF FFFF FFFF],类似于 .number
int65_t
128:更宽。
保存不足以使用 80/128 重新创建所有值,并且只有 64 位。integer = number
uintmax_t
评论
integer
std::uintmax_t
static_assert(sizeof(std::uintmax_t) * CHAR_BIT >= std::numeric_limits<long double>::digits);
long double
uintmax_t
在 WSL 上重新安装系统解决了该问题。 这可能是一个已经修复的错误。
评论
std::uintmax_t
long double
std::uintmax_t
static_assert
x
y
z
x
y
ldexp
frexp
ldexpl
frexpl