Можно ли считать тесты на равенство и неравенство с плавающей точкой последовательными и повторяемыми?

Предположим, что два числа с плавающей точкой x и y, ни один из которых не является Nan.

Безопасно ли предполагать, что тесты на равенство и неравенство будут:

а. Согласитесь друг с другом: например, гарантируется следующее истина: (x >= y) == ((x > y) || (x == y))

б. Будьте повторяемыми, например, (x == y) всегда будет давать один и тот же результат каждый раз, когда он оценивался, если ни x, ни y не были изменены. Я спрашиваю об этом из-за проблемы, заключающейся в том, что модуль с плавающей запятой может сохранять промежуточные результаты с более высокой точностью, чем те, которые хранятся в памяти, и поэтому значение переменной может измениться в зависимости от того, было ли это результатом недавнего вычисления, которое все еще находится в FPU, или это пришло из памяти. Вероятно, первый тест может быть в первом случае, а позже - во втором.

Всего 2 ответа


При условии, что x и y в вопросе являются идентификаторами (а не аббревиатурами для выражений в целом, например, x обозначает b + sqrt(c) ), тогда стандарт C ++ требует (x >= y) == (x > y || x == y) чтобы быть правдой.

C ++ 2017 (черновик N4659) 8 13 позволяет вычислять выражения с плавающей запятой с большей точностью и дальностью, чем требуется для их номинальных типов. Например, при оценке оператора с операндами с float реализация может использовать double арифметику. Однако в сноске 64 мы ссылаемся на 8.4, 8.2.9 и 8.18, чтобы понять, что операторы приведения и присваивания должны выполнять свои конкретные преобразования, которые создают значение, представимое в номинальном типе.

Таким образом, после того, как x и y были назначены значения, нет никакой избыточной точности, и они не имеют разных значений в разных применениях. Тогда (x >= y) == (x > y || x == y) должно быть истинным, потому что оно оценивается по мере появления и обязательно математически верно.

Наличие ошибки GCC 323 означает, что вы не можете полагаться на GCC при компиляции для i386, но это связано с ошибкой в ​​GCC, которая нарушает стандарт C ++. Стандарт C ++ не допускает этого.

Если сравнения сделаны между выражениями, как в:

double y = b + sqrt(c);
if (y != b + sqrt(c))
    std::cout << "Unequal
";

тогда значение, присвоенное y может отличаться от значения, вычисленного для правого оператора b + sqrt(c) , и строка может быть напечатана, потому что b + sqrt(c) может иметь избыточную точность, тогда как y не должен.

Так как приведения также необходимы для удаления избыточной точности, тогда y != (double) (b + sqrt(c)) всегда должно быть ложным (учитывая определение y выше).


Независимо от стандарта C ++ такие несоответствия встречаются на практике в различных условиях.

Есть два примера, которые легко вызвать:

Для 32-битного x86 все не так хорошо. Добро пожаловать в gcc bug номер 323, из-за которого 32-битные приложения не соответствуют стандарту. Что происходит, так это то, что регистры с плавающей запятой в x86 имеют 80 бит, независимо от типа в программе (C, C ++ или Fortran). Это означает, что следующее обычно сравнивает 80-битные значения, а не 64-битные:

bool foo(double x, double y) 
{
     // comparing 80 bits, despite sizeof(double) == 8, i.e., 64 bits
     return x == y;
}

Это не будет большой проблемой, если gcc сможет гарантировать, что double всегда занимает 80 бит. К сожалению, число регистров с плавающей запятой конечно, и иногда значение сохраняется в памяти. Таким образом, для тех же значений x и y x==y может оцениваться как true после разлива в память и false без разлива в память. Нет гарантии относительно (отсутствия) утечки в память. Поведение изменяется, по-видимому, случайным образом в зависимости от флагов компиляции и, по-видимому, несущественных изменений кода.

Таким образом, даже если x и y должны быть логически равны, а x становится разлитым, то x == y может оцениваться как false поскольку y содержит 1 бит в его младшем значащем бите мантиссы, но x урезал этот бит из-за разлив. Тогда ответ на ваш второй вопрос x ==y может возвращать разные результаты в разных местах, в зависимости от разлива или отсутствия в 32-битной программе x86.

Аналогично, x >= y может возвращать true , даже если y должно быть немного больше x . Это может произойти, если после разлива в 64-битную переменную в памяти значения станут равными. В том случае, если раньше в коде x > y || x == y x > y || x == y вычисляется без потери памяти, тогда он будет оцениваться как false . Чтобы сделать вещи более запутанными, замена одного выражения на другое может привести к тому, что компилятор сгенерирует немного другой код с разной потерей памяти. Разница в разливе для двух выражений может привести к непоследовательному различию результатов.

Та же проблема может возникнуть в любой системе, где операции с плавающей запятой выполняются с другой шириной (например, 80 бит для 32-битной x86), чем то, что хочет код (64 бита). Единственный способ обойти это несоответствие - заставить разлив после каждой операции с плавающей запятой усечь превышение точности. Большинство программистов не заботятся об этом из-за снижения производительности.

Второй случай, который может вызвать несоответствия , это небезопасные оптимизации компилятора. По умолчанию многие коммерческие компиляторы выбрасывают согласованность FP из окна, чтобы получить несколько процентов времени выполнения. Компилятор может решить изменить порядок операций FP, даже если они могут давать разные результаты. Например:

v1 = (x + y) + z;
v2 = x + (y + z);
bool b = (v1 == v2);

Понятно, что скорее всего v1 != v2 , из-за разного округления. Например, если x == -y , y > 1e100 и z == 1 то v1 == 1 но v2 == 0 . Если компилятор слишком агрессивен, он может просто подумать об алгебре и сделать вывод, что b должно быть true , даже не оценивая ничего. Это то, что происходит при запуске gcc -ffast-math .

Вот пример, который показывает это.

Такое поведение может сделать x == y несовместимым и сильно зависеть от того, что компилятор может вывести в конкретном фрагменте кода.


Есть идеи?

10000