Если я скопирую float в другую переменную, будут ли они равны?

167

Я знаю что используя == для проверки равенства переменных с плавающей точкой не очень хороший способ. Но я просто хочу знать это со следующими утверждениями:

float x = ...

float y = x;

assert(y == x)

Так как yэто скопировано x, будет ли утверждение верным?

Вэй Ли
источник
78
Позвольте мне предоставить вознаграждение в размере 50 для тех, кто на самом деле доказывает неравенство с помощью демонстрации реального кода. Я хочу увидеть 80-битную и 64-битную вещь в действии. Плюс еще 50 для объяснения сгенерированного ассемблерного кода, который показывает, что одна переменная находится в регистре, а другая нет (или какова бы ни была причина неравенства, я бы хотел, чтобы это объяснили на низком уровне).
Томас Веллер
1
@ThomasWeller ошибка GCC по этому поводу: gcc.gnu.org/bugzilla/show_bug.cgi?id=323 ; тем не менее, я только что попытался воспроизвести его в системе x86-64, и это не удалось, даже с -ffast-math. Я подозреваю, что вам нужен старый GCC на 32-битной системе.
pjc50
5
@ pjc50: на самом деле вам нужна 80-битная система для воспроизведения ошибки 323; это 80x87 FPU, который вызвал проблему. x86-64 использует SSE FPU. Лишние биты вызывают проблему, потому что они округляются при выдаче значения с плавающей запятой 32 бита.
MSalters
4
Если теория MSalters верна (и я подозреваю, что это так), то вы можете выполнить репродукцию, либо скомпилировав 32-bit ( -m32), либо указав GCC использовать x87 FPU ( -mfpmath=387).
Коди Грей
4
Измените «48 бит» на «80 бит», и тогда вы можете удалить «мифическое» прилагательное, @Hot. Это именно то, что обсуждалось непосредственно перед вашим комментарием. В x87 (FPU для архитектуры x86) используются 80-битные регистры, формат «повышенной точности».
Коди Грей

Ответы:

125

Кроме assert(NaN==NaN); случая, указанного kmdreko, у вас может быть ситуация с x87-math, когда 80-разрядные числа с плавающей запятой временно сохраняются в памяти и позже сравниваются со значениями, которые все еще хранятся в регистре.

Возможный минимальный пример, который завершается с gcc9.2 при компиляции с -O2 -m32:

#include <cassert>

int main(int argc, char**){
    float x = 1.f/(argc+2);
    volatile float y = x;
    assert(x==y);
}

Демонстрация Godbolt: https://godbolt.org/z/X-Xt4R

volatile, Вероятно , может быть опущена, если вам удастся создать достаточное количество регистров давления , чтобы иметьy сохраняются и загружаются из памяти (но путать компилятор достаточно, чтобы не опускать сравнение все вместе).

См. Ссылку GCC FAQ:

ЧТЗ
источник
2
Кажется странным, что дополнительные биты будут рассматриваться при сравнении floatстандартной точности с дополнительной точностью.
Nat
13
@Nat Это является странным; это ошибка , .
Гонки легкости на орбите
13
@ TomasWeller Нет, это разумная награда. Хотелось бы, чтобы в ответе было указано, что это несоответствующее поведение
гонки на легкости на орбите
4
Я могу расширить этот ответ, указав, что именно происходит в коде ассемблера, и что это на самом деле нарушает стандарт - хотя я бы не назвал себя юристом по языку, поэтому я не могу гарантировать, что нет неясного пункт, который явно допускает такое поведение. Я предполагаю, что OP больше интересовали практические сложности на реальных компиляторах, а не на полностью безошибочных, полностью совместимых компиляторах (которых, я думаю, де-факто не существует).
ЧТЗ
4
Стоит упомянуть, что, -ffloat-storeпохоже, это способ предотвратить это.
OrangeDog
116

Это не будет верно , если xэто NaN, так как сравнения на NaNэто всегда ложно (да, даже NaN == NaN). Для всех остальных случаев (нормальные значения, субнормальные значения, бесконечности, нули) это утверждение будет верным.

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


Оценка расширенной точности не должна быть проблемой при соблюдении стандарта. Из <cfloat>унаследованного от C [5.2.4.2.2.8] ( выделено мое ):

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

Однако, как отмечалось в комментариях, некоторые случаи с определенными компиляторами, опциями сборки и целями могут сделать это парадоксально ложным.

kmdreko
источник
10
Что, если xвычисляется в регистре в первой строке, сохраняя большую точность, чем минимум для a float. y = xМожет находиться в памяти, сохраняя только floatточность. Затем будет проведен тест на равенство с памятью в регистре, с разной точностью, и, следовательно, без гарантии.
Дэвид Шварц
5
x+pow(b,2)==x+pow(a,3)может отличаться от того, auto one=x+pow(b,2); auto two=y+pow(a,3); one==twoчто можно сравнивать с использованием большей точности, чем с другой (если одно / два являются 64-битными значениями в оперативной памяти, а промежуточные значения - 80-ю битами в fpu). Так что назначение может что-то сделать, иногда.
Якк - Адам Невраумонт
22
@evg Конечно! Мой ответ просто следует стандарту. Все ставки не принимаются, если вы указываете компилятору, что он не является конфайнментом, особенно при включении быстрой математики.
кмдреко
11
@ Voo Смотрите цитату в моем ответе. Значение RHS присваивается переменной на LHS. Не существует никаких юридических оснований для того, чтобы результирующее значение LHS отличалось от значения RHS. Я ценю, что в некоторых компиляторах есть ошибки в этом отношении. Но предполагается, что то, что хранится в реестре, не имеет к этому никакого отношения.
Гонки легкости на орбите
6
@Voo: в ISO C ++ округление до ширины типа должно происходить при любом назначении. В большинстве компиляторов, предназначенных для x87, это действительно происходит только тогда, когда компилятор решает разлить / перезагрузить. Вы можете заставить это gcc -ffloat-storeдля строгого соответствия. Но этот вопрос о том, x=y; x==y; чтобы ничего не делать между ними. Если значение yуже округлено до значения типа float, преобразование в double или long double и обратно не изменит значения. ...
Питер Кордес
34

Да, yбезусловно, примет значение x:

[expr.ass]/2: В простом присваивании (=) объект, на который ссылается левый операнд, изменяется ([defns.access]), заменяя его значение результатом правого операнда.

Не существует возможности для других назначаемых значений.

(Другие уже указывали, что сравнение эквивалентности ==, тем не менее, будет оценивать falseдля значений NaN.)

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

Гонки легкости на орбите
источник
7
@ThomasWeller Это известная ошибка в несоответствующей реализации. Хорошо упомянуть об этом, хотя!
Гонки легкости на орбите
Сначала я думал, что в языке юристов различие между «значением» и «результатом» будет извращенным, но это различие не обязательно должно быть без различия языком C2.2, 7.1.6; С3.3, 7.1.6; C4.2, 7.1.6 или C5.3, 7.1.6 проекта стандарта, который вы цитируете.
Эрик Тауэрс
@EricTowers Извините, вы можете уточнить эти ссылки? Я не могу найти то, на что вы указываете
Гонки Легкости на орбите
@ LightnessRacesBY-SA3.0: C . C2.2 , C3.3 , C4.2 и C5.3 .
Эрик Тауэрс
@EricTowers Да, все еще не следую за тобой. Ваша первая ссылка идет в индекс Приложения C (ничего мне не говорит). Ваши следующие четыре ссылки все идут на[expr] . Если я игнорирую ссылки и сосредотачиваюсь на цитатах, я остаюсь в замешательстве, что, например, C.5.3 , кажется, не касается использования термина «значение» или термина «результат» (хотя это и делает используйте «результат» один раз в обычном английском контексте). Возможно, вы могли бы более четко описать, где, по вашему мнению, стандарт проводит различие, и дать однозначную ссылку на это событие. Спасибо!
Гонки легкости на орбите
3

Да, во всех случаях (без учета NaN и проблем с x87) это будет правдой.

Если вы сделаете для memcmpних проверку, вы сможете проверить на равенство и одновременно сравнить NaN и sNaN. Это также потребует от компилятора взять адрес переменной, которая приведет значение к 32-битному, floatа не к 80-битному. Это устранит проблемы с x87. Второе утверждение здесь не предназначено для демонстрации того, что ==NaN не будут сравниваться как истинные:

#include <cmath>
#include <cassert>
#include <cstring>

int main(void)
{
    float x = std::nan("");
    float y = x;
    assert(!std::memcmp(&y, &x, sizeof(float)));
    assert(y == x);
    return 0;
}

Обратите внимание, что если NaN имеют другое внутреннее представление (то есть различную мантиссу), memcmpсравнение не будет истинным.

СС Энн
источник
1

В обычных случаях это будет оценено как истинное. (или утверждение assert ничего не сделает)

Редактировать :

Под «обычными случаями» я подразумеваю исключение вышеупомянутых сценариев (таких как значения NaN и единицы с плавающей запятой 80x87), как указано другими пользователями.

Принимая во внимание устаревшее количество микросхем 8087 в сегодняшнем контексте, проблема является довольно изолированной, и вопрос применим к текущему состоянию используемой архитектуры с плавающей запятой, что справедливо для всех случаев, за исключением NaN.

(ссылка около 8087 - https://home.deec.uc.pt/~jlobo/tc/artofasm/ch14/ch143.htm )

Спасибо @chtz за хороший пример и @kmdreko за упоминание NaNs - раньше о них не знали!

Anirban166
источник
1
Я думал, что вполне возможно xбыть в регистре с плавающей запятой, пока yзагружен из памяти. Память может иметь меньшую точность, чем регистр, что приводит к сбою сравнения.
Дэвид Шварц
1
Это может быть один случай для ложного, я не думал, что далеко. (поскольку ОП не предоставил никаких особых случаев, я предполагаю, что никаких дополнительных ограничений нет)
Anirban166
1
Я не очень понимаю, что вы говорите. Насколько я понимаю, ОП спрашивает, гарантированно ли успешно выполнено копирование с плавающей точкой, а затем проверка на равенство. Ваш ответ, кажется, говорит «да». Я спрашиваю, почему нет ответа.
Дэвид Шварц
6
Редактирование делает этот ответ неверным. Стандарт C ++ требует, чтобы присваивание преобразовывало значение в тип назначения - избыточная точность может использоваться в вычислениях выражений, но не может сохраняться посредством присваивания. Неважно, хранится ли значение в регистре или памяти; Стандарт C ++ требует, чтобы при написании кода оно было floatзначением без дополнительной точности.
Эрик Постпишил
2
@AProgrammer Учитывая, что (n чрезвычайно) глючный компилятор теоретически может заставить выдать int a=1; int b=a; assert( a==b );утверждение, я думаю, что имеет смысл ответить на этот вопрос только в отношении правильно функционирующего компилятора (хотя, возможно, отмечая, что некоторые версии некоторых компиляторов имеют / имеют -известно-чтобы понять это неправильно). С практической точки зрения, если по какой-то причине компилятор не удаляет дополнительную точность из результата присваиваемого регистру присваивания, он должен сделать это, прежде чем использовать это значение.
TripeHound
-1

Да, он вернет True всегда, кроме случаев, когда это NaN . Если значение переменной равно NaN, оно всегда возвращает False !

Валентин Попеску
источник