分配两个双精度值,保证产生相同的位集模式

is assigning two doubles guaranteed to yield the same bitset patterns?

本文关键字:模式 两个 双精度 分配      更新时间:2023-10-16

这里有几篇关于浮点数及其性质的文章。很明显,必须始终谨慎地比较浮点数和双精度数。还讨论了要求平等的问题,建议显然是远离它。

但是,如果有直接分配怎么办:

double a = 5.4;
double b = a;

假设a是任何非 NaN 值 -a == b会是假的吗?

答案显然是否定的,但我找不到任何在C++环境中定义这种行为的标准。IEEE-754 指出,具有相等(非 NaN)位集模式的两个浮点数相等。现在这是否意味着我可以继续以这种方式比较我的双打,而不必担心可维护性?我是否必须担心其他编译器/操作系统及其在这些行上的实现?或者也许是一个编译器,可以优化一些位并破坏它们的相等性?

我写了一个小程序,它永远生成和比较非 NaN 随机双精度 - 直到它找到a == b产生false的情况。我可以在将来的任何地方和任何时间编译/运行此代码而不必期望停止吗?(忽略字节序并假设符号、指数和尾数位大小/位置保持不变)。

#include <iostream>
#include <random>
struct double_content {
std::uint64_t mantissa : 52;
std::uint64_t exponent : 11;
std::uint64_t sign : 1;
};
static_assert(sizeof(double) == sizeof(double_content), "must be equal");

void set_double(double& n, std::uint64_t sign, std::uint64_t exponent, std::uint64_t mantissa) {
double_content convert;
memcpy(&convert, &n, sizeof(double));
convert.sign = sign;
convert.exponent = exponent;
convert.mantissa = mantissa;
memcpy(&n, &convert, sizeof(double_content));
}
void print_double(double& n) {
double_content convert;
memcpy(&convert, &n, sizeof(double));
std::cout << "sign: " << convert.sign << ", exponent: " << convert.exponent << ", mantissa: " << convert.mantissa << " --- " << n << 'n';
}
int main() {
std::random_device rd;
std::mt19937_64 engine(rd());
std::uniform_int_distribution<std::uint64_t> mantissa_distribution(0ull, (1ull << 52) - 1);
std::uniform_int_distribution<std::uint64_t> exponent_distribution(0ull, (1ull << 11) - 1);
std::uniform_int_distribution<std::uint64_t> sign_distribution(0ull, 1ull);
double a = 0.0;
double b = 0.0;
bool found = false;
while (!found){
auto sign = sign_distribution(engine);
auto exponent = exponent_distribution(engine);
auto mantissa = mantissa_distribution(engine);
//re-assign exponent for NaN cases
if (mantissa) {
while (exponent == (1ull << 11) - 1) {
exponent = exponent_distribution(engine);
}
}
//force -0.0 to be 0.0
if (mantissa == 0u && exponent == 0u) {
sign = 0u;
}

set_double(a, sign, exponent, mantissa);
b = a;
//here could be more (unmodifying) code to delay the next comparison
if (b != a) { //not equal!
print_double(a);
print_double(b);
found = true;
}
}
}

使用 Visual Studio Community 2017 版本 15.9.5

C++标准在 [basic.types]#3 中明确规定:

对于任何平凡可复制的类型T,如果指向T的两个指针指向不同的T对象obj1obj2,其中obj1obj2都不是潜在重叠的子对象,如果构成obj1的底层字节([intro.memory])被复制到obj2中,obj2随后应保持与obj1相同的值。

它给出了这个例子:

T* t1p;
T* t2p;
// provided that t2p points to an initialized object ...
std::memcpy(t1p, t2p, sizeof(T));
// at this point, every subobject of trivially copyable type in *t1p contains
// the same value as the corresponding subobject in *t2p

剩下的问题是什么是value。我们在 [basic.fundamental]#12 中找到(强调我的):

有三种浮点类型:floatdoublelong doubledouble型提供的精度至少与float一样多,而long double型提供的精度至少与double一样多。float类型的值集是double类型的值集的子集;double类型的值集是long double类型的值集的子集。浮点类型的值表示形式是实现定义的。

由于C++标准对浮点值的表示方式没有进一步的要求,因此您将从标准中找到所有保证,因为只需要分配来保留([expr.ass]#2):

在简单赋值(=)中,左操作数所指的对象被修改为右操作数的结果,将其值替换为右操作数的结果。

正如您正确观察到的那样,IEEE-754 要求非 NaN、非零浮点数在当且仅当它们具有相同的位模式时进行比较相等。因此,如果您的编译器使用符合 IEEE-754 标准的浮点数,您应该发现非 NaN、非零浮点数的赋值会保留位模式。


事实上,你的代码

double a = 5.4;
double b = a;

永远不应该允许(a == b)返回 false。但是,一旦你用更复杂的表情替换5.4,大部分这种美好就会消失。这不是本文的确切主题,但 https://randomascii.wordpress.com/2013/07/16/floating-point-determinism/提到了几种可能的方式,在这些方式中,看起来无辜的代码可以产生不同的结果(这打破了"与位模式相同"的断言)。特别是,您可能会将 80 位中间结果与 64 位舍入结果进行比较,从而可能产生不等式。

这里有一些复杂情况。首先,请注意,标题提出的问题与问题不同。标题问:

分配两个双精度值是否保证产生相同的位集模式?

而问题问:

A == B 会是假的吗?

第一个询问赋值是否会出现不同的位(这可能是由于赋值没有记录与其右操作数相同的值,或者由于赋值使用了表示相同值的不同位模式),而第二个询问赋值是否写入了什么位, 存储的值必须等于操作数。

总的来说,第一个问题的答案是否定的。使用 IEEE-754 二进制浮点格式,非零数值与其位模式编码之间存在一对一映射。但是,这允许赋值可能产生不同位模式的几种情况:

  • 正确的操作数是 IEEE-754 −0 实体,但存储了 +0。这不是正确的 IEEE-754 操作,但C++不需要符合 IEEE 754。−0 和 +0 都表示数学零,并且满足C++赋值要求,因此C++实现可以做到这一点。
  • IEEE-754 十进制格式在数值及其编码之间具有一对多映射。举例来说,三百可以用直接含义为 3•102的位或直接含义为 300•100 的位表示。同样,由于它们表示相同的数学值,因此在C++标准下,当右操作数是另一个操作数时,允许将一个存储在赋值的左操作数中。
  • IEEE-754 包括许多称为 NaN(表示非数字)的非数字实体,C++实现可能会存储与正确操作数不同的 NaN。这可能包括用"规范"NaN 替换任何 NaN 以实现,或者在分配信令 Nan 时,以某种方式指示信号,然后将信令 NaN 转换为安静的 NaN 并存储它。
  • 非 IEEE-754 格式可能存在类似的问题。

关于后一个问题,a = ba == b假吗,其中ab都有类型double,答案是否定的。C++标准确实要求赋值将左操作数的值替换为右操作数的值。所以,在a = b之后,a必须具有b的值,因此它们是相等的。

请注意,C++标准没有对浮点运算的准确性施加任何限制(尽管我只在非规范性注释中看到了这一点)。因此,从理论上讲,人们可能会将浮点值的赋值或比较解释为浮点运算,并说它们不需要是准确性,因此赋值可能会更改值或比较可能会返回不准确的结果。我不认为这是对标准的合理解释;对浮点精度没有限制,旨在允许在表达评估和文库例程中具有自由度,而不是简单的分配或比较。

应该注意的是,上述内容专门适用于从简单double操作数分配的double对象。这不应使读者沾沾自喜。几种相似但不同的情况可能会导致数学上看似直观的失败,例如:

  • float x = 3.4;之后,表达式x == 3.4的计算结果一般为假,因为3.4是一个double,必须转换为赋值的float。这种转换会降低精度并更改值。
  • double x = 3.4 + 1.2;之后,C++标准允许表达式x == 3.4 + 1.2计算结果为假。这是因为该标准允许以比标称类型要求的更高的精度计算浮点表达式。因此,3.4 + 1.2可以以long double的精度进行评估。当结果分配给x时,标准要求"丢弃"多余的精度,因此该值被转换为double。与上面的float示例一样,此转换可能会更改值。然后,比较x == 3.4 + 1.2可以将x中的double值与本质上由3.4 + 1.2产生的long double值进行比较。