гарантировано ли присвоение двух двойных одинаковых битрейтов? - PullRequest
5 голосов
/ 27 марта 2019

Здесь есть несколько постов о числах с плавающей запятой и их природе.Ясно, что сравнение чисел с плавающей запятой и удвоений всегда должно выполняться осторожно. Просьба о равенстве также обсуждалась, и рекомендация явно избегать этого.

Но что, если существует прямое назначение:

double a = 5.4;
double b = a;

assumg 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 Version 15.9.5

Ответы [ 2 ]

6 голосов
/ 27 марта 2019

Стандарт C ++ четко указывает на [basic.types] # 3 :

Для любого тривиально копируемого типа T, если два указателя на T указывают на отдельные T объекты obj1 и obj2, где ни 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

Остается вопрос, что такое value. Мы находим в [basic.fundamental] # 12 (выделено мной):

Существует три типа с плавающей точкой: float, double и long double. Тип double обеспечивает, по крайней мере, такую ​​же точность, как 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-битным округленным результатом, что может привести к неравенству.

3 голосов
/ 27 марта 2019

Здесь есть некоторые осложнения.Во-первых, обратите внимание, что заголовок задает другой вопрос, чем вопрос.Заголовок спрашивает:

назначает два двойных, гарантированно приводящих к одинаковым шаблонам битов?

, в то время как вопрос задает:

== b когда-либо будет ложным?

Первый из них спрашивает, могут ли разные биты происходить из присваивания (что может быть связано с тем, что присвоение не записывает то же значение, что и его правый операнд, или из-заприсваивание с использованием другой битовой комбинации, которая представляет одно и то же значение), в то время как вторая спрашивает, должны ли сохраненные значения сравниваться с какими-либо битами, записанными присваиванием.

В полном обобщении ответк первому вопросу нет.Используя двоичные форматы IEEE-754 с плавающей запятой, существует взаимно-однозначное сопоставление между ненулевыми числовыми значениями и их кодированием в битовых комбинациях.Однако это допускает несколько случаев, когда присвоение может привести к разной битовой комбинации:

  • Правым операндом является объект IEEE-754 -0, но +0 сохраняется.Это не правильная операция IEEE-754, но C ++ не требуется для соответствия IEEE 754. И -0, и +0 представляют математический ноль и будут соответствовать требованиям C ++ для присваивания, поэтому реализация C ++ может сделать это.
  • Десятичные форматы IEEE-754 имеют однозначные сопоставления между числовыми значениями и их кодировками.В качестве иллюстрации триста могут быть представлены битами, прямое значение которых равно 3 • 10 2 , или битами, прямое значение которых составляет 300 • 10 0 .Опять же, поскольку они представляют собой одно и то же математическое значение, в соответствии со стандартом C ++ было бы допустимо хранить один в левом операнде присваивания, когда правым операндом является другой.
  • IEEE-754 включает в себя множество нечисловыхсущности, называемые NaN (для Not a Number), и реализация C ++ может хранить NaN, отличный от правого операнда.Это может включать в себя либо замену любого NaN «каноническим» NaN для реализации, либо, после назначения сигнального Nan, указание сигнала каким-либо образом, а затем преобразование сигнального NaN в тихий NaN и его сохранение.
  • Не-IEEE-754 форматы могут иметь схожие проблемы.

Что касается последнего вопроса, может ли a == b быть ложным после a = b, где оба типа a и b имеют тип 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;, выражение x == 3.4 + 1.2 разрешено стандартом C ++ для оценки в false. Это связано с тем, что стандарт позволяет оценивать выражения с плавающей запятой с большей точностью, чем требует номинальный тип. Таким образом, 3.4 + 1.2 может быть оценено с точностью до long double. Когда результат присваивается x, стандарт требует, чтобы избыточная точность была «отброшена», поэтому значение преобразуется в double. Как и в приведенном выше примере float, это преобразование может изменить значение. Тогда сравнение x == 3.4 + 1.2 может сравнивать значение double в x с тем, что по существу является значением long double, полученным с помощью 3.4 + 1.2.
...