Dlaczego NaN - NaN == 0.0 z kompilatorem Intel C ++?

300

Powszechnie wiadomo, że NaN rozmnażają się w arytmetyce, ale nie mogłem znaleźć żadnych demonstracji, więc napisałem mały test:

#include <limits>
#include <cstdio>

int main(int argc, char* argv[]) {
    float qNaN = std::numeric_limits<float>::quiet_NaN();

    float neg = -qNaN;

    float sub1 = 6.0f - qNaN;
    float sub2 = qNaN - 6.0f;
    float sub3 = qNaN - qNaN;

    float add1 = 6.0f + qNaN;
    float add2 = qNaN + qNaN;

    float div1 = 6.0f / qNaN;
    float div2 = qNaN / 6.0f;
    float div3 = qNaN / qNaN;

    float mul1 = 6.0f * qNaN;
    float mul2 = qNaN * qNaN;

    printf(
        "neg: %f\nsub: %f %f %f\nadd: %f %f\ndiv: %f %f %f\nmul: %f %f\n",
        neg, sub1,sub2,sub3, add1,add2, div1,div2,div3, mul1,mul2
    );

    return 0;
}

Przykład ( transmisja na żywo tutaj ) generuje w zasadzie to, czego bym się spodziewał (negatyw jest trochę dziwny, ale ma sens):

neg: -nan
sub: nan nan nan
add: nan nan
div: nan nan nan
mul: nan nan

MSVC 2015 produkuje coś podobnego. Jednak Intel C ++ 15 produkuje:

neg: -nan(ind)
sub: nan nan 0.000000
add: nan nan
div: nan nan nan
mul: nan nan

W szczególności qNaN - qNaN == 0.0.

To ... nie może być w porządku, prawda? Co na ten temat mówią odpowiednie normy (ISO C, ISO C ++, IEEE 754) i dlaczego istnieje różnica w zachowaniu między kompilatorami?

imallett
źródło
18
JavaScript i Python (numpy) nie mają tego zachowania. Nan-NaNjest NaN. Perl i Scala również zachowują się podobnie.
Paul,
33
Może włączyłeś niebezpieczne optymalizacje matematyczne (odpowiednik -ffast-mathgcc)?
Matteo Italia,
5
@nm: Nieprawda. Załącznik C, która jest opcjonalna, ale normatywna gdy obsługiwane i konieczne są przestawne zachowanie określonego punktu w ogóle zasadniczo zawiera IEEE 754 do C
R .. GitHub przestali pomagać ICE
5
Jeśli chcesz zapytać o standard IEEE 754, podaj go gdzieś w pytaniu.
n. „zaimki” m.
68
Byłem pewien, że to pytanie dotyczy JavaScript w tytule.
MikeTheLiar,

Odpowiedzi:

300

Domyślna obsługa zmiennoprzecinkowa w kompilatorze Intel C ++ to /fp:fast, która obsługuje się NaNniepewnie (co również skutkuje na przykład NaN == NaNbyciem true). Spróbuj określić /fp:strictlub /fp:precisei sprawdź, czy to pomoże.

Petr Abdulin
źródło
15
Właśnie próbowałem tego sam. Rzeczywiście, określenie dokładnej lub ścisłej rozwiązuje problem.
imallett,
67
Chciałbym poprzeć decyzję Intela, by domyślnie /fp:fast: jeśli chcesz czegoś bezpiecznego , prawdopodobnie lepiej unikaj pojawienia się NaN i generalnie nie używaj ==liczb zmiennoprzecinkowych. Poleganie na dziwnej semantyce, którą IEEE754 przypisuje NaN, wymaga kłopotów.
leftaroundabout
10
@leftaroundabout: Co wydaje ci się dziwne w NaN, oprócz okropnej decyzji IMHO, aby NaN! = NaN zwrócił prawdę?
supercat
21
NaN mają ważne zastosowania - potrafią wykrywać wyjątkowe sytuacje bez konieczności przeprowadzania testów po każdym obliczeniu. Nie każdy programista zmiennoprzecinkowy potrzebuje ich, ale nie odrzucaj ich.
Bruce Dawson,
6
@ superupat Z ciekawości zgadzasz się z decyzją o NaN==NaNpowrocie false?
Kyle Strand,
53

To . . nie może mieć racji, prawda? Moje pytanie: co na ten temat mówią odpowiednie normy (ISO C, ISO C ++, IEEE 754)?

Petr Abdulin już odpowiedział, dlaczego kompilator udziela 0.0odpowiedzi.

Oto, co mówi IEEE-754: 2008:

(6.2 Operacje z NaN) "[...] W przypadku operacji z cichymi wejściami NaN, innymi niż operacje maksymalne i minimalne, jeśli ma być dostarczony wynik zmiennoprzecinkowy, wynikiem powinien być cichy NaN, który powinien być jednym z wprowadź NaNs. ”

Zatem jedynym prawidłowym wynikiem odejmowania dwóch cichych operandów NaN jest cichy NaN; jakikolwiek inny wynik jest nieprawidłowy.

Standard C mówi:

(C11, F.9.2 Transformacje ekspresji p1) „[...]

x - x → 0. 0 "Wyrażenia x - x i 0. 0 nie są równoważne, jeśli x jest NaN lub nieskończonym"

(gdzie tutaj NaN oznacza cichy NaN zgodnie z F.2.1p1 „Ta specyfikacja nie definiuje zachowania sygnalizacyjnych NaN. Zasadniczo używa terminu NaN do oznaczenia cichych NaN”)

ouah
źródło
20

Ponieważ widzę odpowiedź podważającą zgodność standardów kompilatora Intela i nikt o tym nie wspominał, zaznaczę, że zarówno GCC, jak i Clang mają tryb, w którym robią coś całkiem podobnego. Ich domyślne zachowanie jest zgodne z IEEE -

$ g++ -O2 test.cc && ./a.out 
neg: -nan
sub: nan nan nan
add: nan nan
div: nan nan nan
mul: nan nan

$ clang++ -O2 test.cc && ./a.out 
neg: -nan
sub: -nan nan nan
add: nan nan
div: nan nan nan
mul: nan nan

- ale jeśli poprosisz o szybkość kosztem poprawności, otrzymasz to, o co prosisz -

$ g++ -O2 -ffast-math test.cc && ./a.out 
neg: -nan
sub: nan nan 0.000000
add: nan nan
div: nan nan 1.000000
mul: nan nan

$ clang++ -O2 -ffast-math test.cc && ./a.out 
neg: -nan
sub: -nan nan 0.000000
add: nan nan
div: nan nan nan
mul: nan nan

Uważam, że jest całkowicie słuszne krytykowanie wyboru domyślnego ICC , ale nie przeczytałbym całej wojny uniksowej z powrotem do tej decyzji.

zwol
źródło
Zauważ, że z -ffast-math, gccnie jest już zgodny z ISO 9899: 2011 w odniesieniu do arytmetyki zmiennoprzecinkowej.
fuz
1
@FUZxxl Tak, chodzi o to, że oba kompilatory mają niezgodny tryb zmiennoprzecinkowy, po prostu icc domyślnie działa w tym trybie, a gcc nie.
zwol
4
Aby rzucić paliwo w ogień, naprawdę podoba mi się wybór Intela, aby domyślnie włączyć szybką matematykę. Istotą używania pływaków jest uzyskanie dużej przepustowości.
Navin