(.1f+.2f==.3f)!= (.1f+.2f).Równa(.3f) dlaczego?

Moje pytanie brzmi Nie o pływającą precyzję. Chodzi o to, dlaczego Equals() różni się od ==.

Rozumiem, dlaczego .1f + .2f == .3f jest false (podczas gdy .1m + .2m == .3m jest true).
Rozumiem, że == jest referencją, a {[8] } jest porównaniem wartości. (Edit : wiem, że jest w tym coś więcej.)

Ale dlaczego jest (.1f + .2f).Equals(.3f) true, while (.1d+.2d).Equals(.3d) is still false?

 .1f + .2f == .3f;              // false
(.1f + .2f).Equals(.3f);        // true
(.1d + .2d).Equals(.3d);        // false
Author: Daniel Pelsmaeker, 2013-02-27

5 answers

Pytanie jest myląco sformułowane. Podzielmy to na wiele mniejszych pytań:

Dlaczego jedna dziesiąta plus dwie dziesiąte nie zawsze równa się trzem dziesiątym w arytmetyce zmiennoprzecinkowej?

Pozwól, że przedstawię ci analogię. Załóżmy, że mamy system matematyczny, w którym wszystkie liczby są zaokrąglane do dokładnie pięciu miejsc po przecinku. Załóżmy, że powiesz:
x = 1.00000 / 3.00000;

Spodziewasz się, że x wyniesie 0,33333, prawda? Ponieważ jest to najbliższa liczba w naszym system do odpowiedzi real. Załóżmy, że powiedziałeś

y = 2.00000 / 3.00000;

Spodziewasz się, że y wyniesie 0,66667, prawda? Bo znowu jest to najbliższy numer w naszym systemie do prawdziwej odpowiedzi. 0.66666 jest dalej z dwóch trzecich niż 0.66667 jest.

Zauważ, że w pierwszym przypadku zaokrąglamy w dół, a w drugim zaokrąglamy w górę.

Teraz Kiedy mówimy

q = x + x + x + x;
r = y + x + x;
s = y + y;

Co dostaniemy? Gdybyśmy wykonali dokładną arytmetykę to każdy z tych oczywiście być cztery trzecie i wszystkie będą równe. Ale nie są równi. Chociaż 1.33333 jest najbliższą liczbą w naszym systemie do czterech trzecich, tylko r ma tę wartość.

Q wynosi 1.33332 -- ponieważ x było trochę małe, każdy dodatek skumulował ten błąd, a wynik końcowy jest trochę za mały. Podobnie, s jest za duże; jest to 1.33334, ponieważ y było trochę za duże. r otrzymuje prawidłową odpowiedź, ponieważ zbyt duża liczba y jest anulowana przez zbyt małą liczbę x i wynik jest poprawny.

Czy liczba miejsc precyzji ma wpływ na wielkość i kierunek błędu?

Tak; większa precyzja sprawia, że wielkość błędu jest mniejsza, ale może zmienić to, czy obliczenia generują stratę, czy zysk z powodu błędu. Na przykład:

b = 4.00000 / 7.00000;

B wynosi 0,57143, co zaokrągla się z prawdziwej wartości 0,571428571... Gdybyśmy poszli do ośmiu miejsc, które byłyby 0.57142857, co ma znacznie, znacznie mniejsze wielkość błędu, ale w przeciwnym kierunku; zaokrąglony w dół.

Ponieważ zmiana precyzji może zmienić, czy błąd jest zyskiem, czy stratą w każdym indywidualnym obliczeniu, może to zmienić, czy błędy danego obliczenia zbiorczego wzmacniają się nawzajem lub usuwają się nawzajem. Wynik netto jest taki, że czasami obliczenia o niższej precyzji są bliższe" prawdziwemu " rezultatowi niż obliczenia o wyższej precyzji, ponieważ w obliczeniach o niższej precyzji masz szczęście i błędy są w różnych kierunkach.

Spodziewamy się, że wykonanie obliczeń z większą precyzją zawsze daje odpowiedź bliższą prawdziwej odpowiedzi, ale ten argument pokazuje inaczej. To wyjaśnia, dlaczego czasami obliczenia w pływakach dają "właściwą" odpowiedź, ale obliczenia w podwójnych-które mają dwukrotnie większą precyzję-dają " złą " odpowiedź, poprawną?

Tak, to jest dokładnie to, co dzieje się w Twoich przykładach, z tym, że zamiast pięciu cyfry precyzji dziesiętnej mamy pewną liczbę cyfr binarnych precyzji. Podobnie jak jedna trzecia nie może być dokładnie przedstawiona w pięciu-lub dowolnej skończonej liczbie-cyfr dziesiętnych, 0.1, 0.2 i 0.3 nie może być dokładnie przedstawiona w dowolnej skończonej liczbie cyfr binarnych. Niektóre z nich zostaną zaokrąglone w górę, niektóre z nich zostaną zaokrąglone w dół, a to, czy ich dodanie zwiększy błąd lub anuluje błąd zależy od konkretnych szczegółów ile cyfr binarnych jest w każdym systemie. Oznacza to, że zmiany w precyzji mogą zmienić odpowiedź na lepsze lub gorsze. Generalnie im większa precyzja, tym bliższa jest prawdziwa odpowiedź, ale nie zawsze.

Jak mogę uzyskać dokładne obliczenia arytmetyczne dziesiętne, jeśli float i double używają cyfr binarnych?

Jeśli potrzebujesz dokładnej matematyki dziesiętnej, użyj typu decimal; używa ułamków dziesiętnych, nie binarnych ułamki. Cena, którą płacisz, jest znacznie większa i wolniejsza. I oczywiście, jak już widzieliśmy, ułamki takie jak jedna trzecia lub cztery siódme nie będą dokładnie reprezentowane. Każdy ułamek, który w rzeczywistości jest ułamkiem dziesiętnym, będzie jednak reprezentowany z zerowym błędem, do około 29 znaczących cyfr.

Ok, zgadzam się, że wszystkie schematy zmiennoprzecinkowe wprowadzają nieścisłości z powodu błędu reprezentacji i że te nieścisłości mogą się czasem kumulować lub anulować się nawzajem na podstawie liczby bitów precyzji użytej w obliczeniach. Czy mamy przynajmniej gwarancję, że te nieścisłości będą spójne?

Nie, Nie masz takiej gwarancji na pływaki lub duble. Zarówno kompilator, jak i runtime mogą wykonywać obliczenia zmiennoprzecinkowe z dokładnością wyższą niż jest to wymagane przez specyfikację. W szczególności kompilator i runtime mogą wykonywać jedną precyzję (32 bit) arytmetyka W 64 bitach, 80 bitach, 128 bitach lub dowolnych bitach większych niż 32, które lubią.

Kompilator i runtime mogą to zrobić jednak wtedy tak się czują. Nie muszą być spójne od Maszyny do maszyny, od biegu do biegu i tak dalej. Ponieważ może to tylko sprawić, że obliczenia będą bardziej dokładne, nie jest to uważane za błąd. To cecha. Funkcja, która sprawia, że niezwykle trudno jest pisać programy, które zachowują przewidywalne, ale jednak cecha.

To znaczy, że obliczenia wykonywane w czasie kompilacji, jak literały 0.1 + 0.2, mogą dać inne wyniki niż te same obliczenia wykonywane w czasie pracy ze zmiennymi?

Tak.

A co z porównaniem wyników 0.1 + 0.2 == 0.3 do (0.1 + 0.2).Equals(0.3)?

Ponieważ pierwszy jest obliczany przez kompilator, a drugi przez runtime, i właśnie powiedziałem, że mogą arbitralnie używać więcej precyzji niż wymaga specyfikacji na ich kaprys, tak, te mogą dać różne wyniki. Być może jeden z nich wybiera obliczenia tylko w 64 bitowej precyzji, podczas gdy drugi wybiera 80 bitową lub 128 bitową precyzję dla części lub całości obliczeń i otrzymuje odpowiedź na różnicę.

Poczekaj chwilę. Twierdzisz, że nie tylko 0.1 + 0.2 == 0.3 może być inna niż (0.1 + 0.2).Equals(0.3). Mówisz, że 0.1 + 0.2 == 0.3 można obliczyć jako true lub false w całości na kaprys kompilatora. Może produkować true we wtorki i false w czwartki, może produkować true na jednej maszynie i false na drugiej, może produkować true i false, jeśli wyrażenie pojawi się dwa razy w tym samym programie. Wyrażenie to może mieć dowolną wartość z dowolnego powodu; kompilator może być całkowicie zawodny.

Zgadza się.

Sposób, w jaki jest to zwykle zgłaszane do zespołu kompilatorów C# jest taki, że ktoś ma jakieś wyrażenie, które generuje true podczas kompilacji w debug i false podczas kompilacji w trybie release. Jest to najczęstsza sytuacja, w której pojawia się to, ponieważ generowanie kodu debugowania i Wydania zmienia Schematy alokacji rejestru. Ale kompilator ma pozwolenie na robienie z tym wyrażeniem wszystkiego, co mu się podoba, o ile wybierze true lub false. (Nie może, powiedzmy, spowodować błędu w czasie kompilacji.)

To szaleństwo.

Zgadza się.

Kogo mam winić za ten bałagan?

Nie ja, to na pewno.

Intel postanowił stworzyć układ matematyczny zmiennoprzecinkowy, w którym uzyskiwanie spójnych wyników było o wiele droższe. Małe wybory w kompilatorze dotyczące tego, jakie operacje należy zapisać, a jakie zachować na stosie, mogą sumować się do dużych różnic w wynikach.

Jak zapewnić spójne wyniki?

Użyj typu decimal, Jak mówiłem wcześniej. Lub zrobić wszystko twoja matematyka w liczbach całkowitych.

Muszę używać podwójnych lub pływaków; Czy Mogę zrobić cokolwiek , aby zachęcić do konsekwentnych wyników?

Tak. Jeśli zapiszesz dowolny wynik do dowolnego statycznego pola , dowolnego pola instancji klasy lub elementu tablicy typu float lub double, to gwarantuje się, że zostanie on obcinany z powrotem do 32 lub 64-bitowej precyzji. (Ta gwarancja jest wyraźnie Nie wykonane dla sklepów lokalnych lub parametrów formalnych.) Także jeśli runtime oddane do (float) lub (double) na wyrażeniu, które już jest tego typu, kompilator emituje specjalny kod, który wymusza obcięcie wyniku tak, jakby był przypisany do elementu pola lub tablicy. (Rzuty, które wykonują się w czasie kompilacji - czyli rzuty na stałe wyrażenia - nie są gwarantowane, aby to zrobić.)

Aby wyjaśnić ten ostatni punkt: czy specyfikacja języka C # daje te gwarancje?

Nie. Na runtime gwarantuje, że przechowuje się w tablicy lub polu truncate. Specyfikacja C# nie gwarantuje, że dana tożsamość zostanie okrojona, ale implementacja Microsoft posiada testy regresji, które zapewniają, że każda nowa wersja kompilatora ma takie zachowanie.

Specyfikacja języka ma do powiedzenia na ten temat, że operacje zmiennoprzecinkowe mogą być wykonywane z większą precyzją według uznania implementacji.

 130
Author: Eric Lippert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-02-28 15:38:01

Kiedy piszesz

double a = 0.1d;
double b = 0.2d;
double c = 0.3d;

właściwie , to nie są do końca 0.1, 0.2 i 0.3. Z kodu IL;

  IL_0001:  ldc.r8     0.10000000000000001
  IL_000a:  stloc.0
  IL_000b:  ldc.r8     0.20000000000000001
  IL_0014:  stloc.1
  IL_0015:  ldc.r8     0.29999999999999999

Istnieje lof pytania w tak wskazując ten problem jak (różnica między dziesiętne, float i double W. NET? i czynienia z błędami zmiennoprzecinkowymi w. NET ) ale proponuję przeczytać fajny artykuł o nazwie; {15]}

What Every Computer Scientist Should Know About Floating-Point Arithmetic

to, co powiedział leppie, jest bardziej logiczne. The real sytuacja jest tutaj, W sumie zależy OD compiler / computer lub cpu.

Na podstawie kodu leppiego kod ten działa na moim Visual Studio 2010 i Linqpad , w wyniku True/False, ale kiedy próbowałem go na ideone.com , wynik będzie True/True

Sprawdź DEMO.

Tip : kiedy napisałem Console.WriteLine(.1f + .2f == .3f); Resharper ostrzega mnie;

Porównanie liczby zmiennoprzecinkowej z operatorem równości. Możliwe utrata precyzji podczas zaokrąglania wartości.

Tutaj wpisz opis obrazka

 8
Author: Soner Gönül,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:02:11

Jak wspomniano w komentarzach, wynika to z tego, że kompilator robi stałą propagację i wykonuje obliczenia z większą precyzją(uważam, że jest to zależne od procesora).

  var f1 = .1f + .2f;
  var f2 = .3f;
  Console.WriteLine(f1 == f2); // prints true (same as Equals)
  Console.WriteLine(.1f+.2f==.3f); // prints false (acts the same as double)

@Caramiriel zwraca również uwagę, że .1f+.2f==.3f jest emitowane jako false w IL, stąd kompilator wykonał obliczenia w czasie kompilacji.

Aby potwierdzić stałą optymalizację kompilatora składającego / propagującego

  const float f1 = .1f + .2f;
  const float f2 = .3f;
  Console.WriteLine(f1 == f2); // prints false
 5
Author: leppie,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-02-27 17:21:57

FWIW po przejściu testu

float x = 0.1f + 0.2f;
float result = 0.3f;
bool isTrue = x.Equals(result);
bool isTrue2 = x == result;
Assert.IsTrue(isTrue);
Assert.IsTrue(isTrue2);

Więc problem jest rzeczywiście z tą linią

0.1 f + 0.2 f==0.3 f

Który, jak stwierdzono, jest prawdopodobnie specyficzny dla kompilatora / pc

Większość ludzi skacze na to pytanie z niewłaściwego kąta myślę, że do tej pory

UPDATE:

Kolejny ciekawy test chyba

const float f1 = .1f + .2f;
const float f2 = .3f;
Assert.AreEqual(f1, f2); passes
Assert.IsTrue(f1==f2); doesnt pass

Implementacja równości pojedynczej:

public bool Equals(float obj)
{
    return ((obj == this) || (IsNaN(obj) && IsNaN(this)));
}
 2
Author: Valentin Kuzub,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-02-27 17:10:42

== polega na porównywaniu dokładnych wartości pływaków.

Equals jest metodą logiczną, która może zwracać wartość true lub false. Konkretne wdrożenie może się różnić.

 0
Author: njzk2,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-02-27 17:32:49