分配两个double是否保证产生相同的位模式?

时间:2019-03-27 14:29:59

标签: c++ floating-point language-lawyer

这里有几篇有关浮点数及其性质的文章。显然,comparing floats and doubles必须始终谨慎进行。 Asking for equality也已经讨论过,建议显然不要使用它。

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

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

2 个答案:

答案 0 :(得分:6)

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

  

对于任何平凡可复制的类型T,如果两个指向T的指针指向不同的T对象obj1obj2,则两个{{1} }或obj1都不是潜在重叠的子对象,如果将构成obj2的基础字节([intro.memory])复制到obj1中,则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 是什么。我们发现[basic.fundamental]#12(重点是我):

  

共有三种浮点类型:valuefloatdouble。   类型long double的精度至少与double相同,而类型float的精度至少与long double相同。   类型double的值集是类型float的值集的子集; double类型的值集是double类型的值集的子集。   浮点类型的值表示形式是实现定义的。

由于C ++标准对浮点值的表示方式没有任何进一步的要求,因此,这是该标准的保证,因为只需要赋值即可保存({{3 }}):

  

在简单分配(long double)中,通过用右操作数的结果替换其值来修改左操作数所引用的对象。

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


事实上,您的代码

=

不应允许double a = 5.4; double b = a; 返回false。但是,一旦您用更复杂的表达式替换(a == b)时,这些优点就消失了。这不是本文的确切主题,但是[expr.ass]#2提到了几种看起来无辜的代码可以产生不同结果的可能方式(这打破了“与位模式相同”的断言)。特别是,您可能正在将80位中间结果与64位舍入结果进行比较,可能会产生不等式。

答案 1 :(得分:3)

这里有些并发症。首先,请注意标题提出的问题与问题不同。标题问:

  

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

当问题问到时:

  

a == b永远是假的吗?

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

从总体上讲,第一个问题的答案为否。使用IEEE-754二进制浮点格式,在非零数值和它们在位模式下的编码之间存在一对一的映射。但是,这允许分配在几种情况下产生不同的位模式:

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

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

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

请注意,以上内容专门适用于从简单的b操作数分配的double对象。这不应使读者感到自满。几种类似但不同的情况可能会导致数学上似乎直观的失败,例如:

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