Якщо я скопіюю поплавок на іншу змінну, вони будуть рівними?


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; саме проблема FPU 80x87 викликала проблему. x86-64 використовує SSE FPU. Додаткові біти спричиняють проблему, оскільки вони округлені при розливі значення на 32-бітний плаваючий.
MSalters

4
Якщо теорія MSalters правильна (і я підозрюю, що вона є), ви можете спростувати або компілюючи 32-розрядний ( -m32), або доручивши GCC використовувати x87 FPU ( -mfpmath=387).
Коді Грей

4
Змініть "48 біт" на "80 біт", і тоді ви можете видалити там "міфічний" прикметник @Hot. Саме це обговорювалося безпосередньо перед вашим коментарем. X87 (FPU для архітектури x86) використовує 80-бітні регістри, формат "розширеної точності".
Коді Грей

Відповіді:


125

Крім assert(NaN==NaN);випадку, на який вказує kmdreko, у вас може виникнути ситуації з x87-математикою, коли 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:


2
Здається дивним, що додаткові біти будуть враховані при порівнянні floatстандартної точності з додатковою точністю.
Нат

13
@Nat Це є дивним; це помилка .
Гонки легкості на орбіті

13
@ThomasWeller Ні, це розумна нагорода. Хоча я хотів би відповідь зазначити, що це невідповідна поведінка
гонки легкості в орбіті

4
Я можу поширити цю відповідь, вказавши, що саме відбувається в асемблерному коді, і що це фактично порушує стандарт - хоча я б не називав себе мовою-юристом, тому не можу гарантувати, що там немає незрозумілого. стаття, яка прямо дозволяє таку поведінку. Я припускаю, що ОП більше цікавились практичними ускладненнями на фактичних компіляторах, а не на повністю безвідмовних, повністю сумісних компіляторах (яких, фактично, не існує).
chtz

4
Варто згадати, що, як -ffloat-storeвидається, спосіб запобігти цьому.
OrangeDog

116

Це не буде вірно , якщо xце NaN, так як порівняння на NaNце завжди помилково (так, навіть NaN == NaN). У всіх інших випадках (нормальні значення, субнормальні значення, нескінченності, нулі) це твердження буде істинним.

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


Оцінка розширеної точності повинна бути проблемою, якщо дотримуватися стандарт. З <cfloat>успадкованих від C [5.2.4.2.2.8] ( акцент моїх ):

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

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


10
Що робити, якщо xобчислюється в регістрі в першому рядку, зберігаючи більш точність, ніж мінімум для а 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 Звичайно! Моя відповідь просто відповідає стандарту. Усі ставки знімаються, якщо ви скажете, що ваш компілятор не заважає, особливо при включенні швидкої математики.
kmdreko

11
@Voo Дивіться цитату в моїй відповіді. Значення RHS присвоюється змінній на LHS. Немає юридичного обґрунтування, щоб отримане значення LHS відрізнялося від значення РЗС. Я вдячний, що кілька компіляторів мають помилки в цьому плані. Але чи щось зберігається в реєстрі, це, мабуть, не має нічого спільного.
Гонки легкості на орбіті

6
@Voo: У ISO C ++ закріплення за шириною типу має відбуватися в будь-якому призначенні. У більшості компіляторів, націлених на x87, це дійсно відбувається лише тоді, коли компілятор вирішить пролити / перезавантажити. Ви можете примусити його gcc -ffloat-storeдо суворого дотримання. Але це питання стосується x=y; x==y; того, щоб нічого не робити між собою. Якщоy це вже округлене для розміщення в поплавці, перетворення в подвійне або довге подвійне і назад значення не змінить. ...
Пітер Кордес

34

Так, yобов'язково прийме значення x:

[expr.ass]/2: У простому призначенні (=) об'єкт, на який посилається лівий операнд, змінюється ([defns.access]) шляхом заміни його значення на результат правого операнда.

Інших значень не може бути присвоєно.

(Інші вже вказували, що порівняння еквівалентності ==все-таки буде оцінено falseдля NaN-значень.)

Звичайна проблема з плаваючою комою ==полягає в тому, що легко не мати того значення, яке ти думаєш. Тут ми знаємо, що дві величини, якими б вони не були, однакові.


7
@ThomasWeller Це відома помилка, яка, відповідно, не відповідає вимогам. Приємно згадати це, хоча!
Гонки легкості на орбіті

Спочатку я думав, що мовне правознавство розмежування "значення" та "результат" буде викривленим, але це розрізнення не повинно бути без різниці мовою C2.2, 7.1.6; C3.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 Так, все одно не слідкую за тобою. Ваше перше посилання переходить до індексу Додатка С (нічого не говорить мені). Усі ваші наступні чотири посилання переходять на [expr]. Якщо я ігнорую посилання та зосереджуюсь на цитатах, я залишаюсь з сум'яттям, що, наприклад, C.5.3 , схоже, не стосується використання терміна "значення" або терміна "результат" (хоча це і є використовувати "результат" один раз у звичайному англійському контексті). Можливо, ви могли б більш чітко описати те, де ви думаєте, що стандарт розрізняє, і надати єдине чітке посилання на це, що відбувається. Дякую!
Гонки легкості на орбіті

3

Так, у всіх випадках (ігнорування NaNs та 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

У звичайних випадках це було б оцінкою до істини. (або твердження про твердження нічого не зробить)

Редагувати :

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

Зважаючи на відсутність 8087 мікросхем в сьогоднішньому контексті, питання є досить ізольованим, і питання може бути застосованим у поточному стані використовуваної архітектури з плаваючою комою, що відповідає всім випадкам, крім NaN.

(довідка про 8087 - https://home.deec.uc.pt/~jlobo/tc/artofasm/ch14/ch143.htm )

Кудо до @chtz за відтворення хорошого прикладу, а @kmdreko за згадування NaNs - раніше про них не знав!


1
Я думав, що цілком можливо xбути в реєстрі з плаваючою комою, поки yзавантажується з пам'яті. Пам'ять може мати меншу точність, ніж регістр, через що порівняння не вдається.
Девід Шварц

1
Це може бути один випадок помилки, я ще не думав про це. (оскільки ОП не передбачала жодних особливих випадків, я не припускаю жодних додаткових обмежень)
Anirban166,

1
Я не дуже розумію, що ти кажеш. Як я розумію питання, ОП запитує, чи можна скопіювати флоат і потім перевірити рівність, щоб досягти успіху. Здається, ваша відповідь говорить "так". Я запитую, чому відповідь - ні.
Девід Шварц

6
Редагування робить цю відповідь неправильною. Стандарт C ++ вимагає, щоб присвоєння перетворило значення в тип призначення - надлишкова точність може використовуватися при оцінках виразів, але не може бути збережена через присвоєння. Немає значення, чи зберігається значення в регістрі чи пам'яті; стандарт C ++ вимагає, щоб це було, як написано в коді, floatзначення без додаткової точності.
Eric Eric Postpischil

2
@AProgrammer З огляду на те, що (n надзвичайно) баггі-компілятор теоретично може спричинити int a=1; int b=a; assert( a==b );закидання твердження, я думаю, що має сенс відповісти на це питання стосовно правильно функціонуючого компілятора (при цьому, можливо, зазначаючи, що деякі версії деяких компіляторів мають / мають -відомо - зрозуміти це неправильно). На практиці, якщо компілятор з якихось причин не видаляє зайву точність з результату призначення, що зберігається в регістрі, він повинен зробити це, перш ніж використовувати це значення.
TripeHound

-1

Так, він завжди поверне True , за винятком випадків, якщо це NaN . Якщо значення змінної - NaN, то воно завжди повертає помилкове !

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.