Czy porównywanie równości liczb zmiennoprzecinkowych wprowadza w błąd młodszych programistów, nawet jeśli w moim przypadku nie wystąpił błąd zaokrąglania?

31

Na przykład chcę wyświetlić listę przycisków od 0,0,5, ... 5, która przeskakuje za każde 0,5. Używam do tego pętli for i mam inny kolor pod przyciskiem STANDARD_LINE:

var MAX=5.0;
var DIFF=0.5
var STANDARD_LINE=1.5;

for(var i=0;i<=MAX;i=i+DIFF){
    button.text=i+'';
    if(i==STANDARD_LINE){
      button.color='red';
    }
}

W tym przypadku nie powinno być żadnych błędów zaokrąglania, ponieważ każda wartość jest dokładna w IEEE 754, ale walczę, czy powinienem ją zmienić, aby uniknąć porównania równości zmiennoprzecinkowej:

var MAX=10;
var STANDARD_LINE=3;

for(var i=0;i<=MAX;i++){
    button.text=i/2.0+'';
    if(i==STANDARD_LINE/2.0){
      button.color='red';
    }
}

Z jednej strony oryginalny kod jest prostszy i przesłany do mnie. Ale zastanawiam się nad jedną rzeczą: czy ja == STANDARD_LINE wprowadza w błąd młodszych kolegów z drużyny? Czy ukrywa to, że liczby zmiennoprzecinkowe mogą zawierać błędy zaokrąglania? Po przeczytaniu komentarzy do tego postu:

https://stackoverflow.com/questions/33646148/is-hardcode-float-precise-if-it-can-be-represented-by-binary-format-in-ieee-754

wydaje się, że wielu programistów nie wie, że niektóre liczby zmiennoprzecinkowe są dokładne. Czy powinienem unikać porównywania liczb zmiennoprzecinkowych, nawet jeśli jest to ważne w moim przypadku? A może zastanawiam się nad tym?

ocomfd
źródło
23
Zachowanie tych dwóch list kodów nie jest równoważne. 3 / 2,0 to 1,5, ale izawsze będą to liczby całkowite na drugiej liście. Spróbuj usunąć drugi /2.0.
candied_orange
27
Jeśli absolutnie musisz porównać dwa FP dla równości (co nie jest wymagane, jak wskazali inni w swoich drobnych odpowiedziach, ponieważ możesz po prostu użyć porównania licznika pętli z liczbami całkowitymi), ale jeśli tak, to komentarz powinien wystarczyć. Osobiście pracuję z IEEE FP od dłuższego czasu i nadal byłbym zdezorientowany, gdybym zobaczył, powiedzmy, bezpośrednie porównanie SPFP bez żadnego komentarza ani niczego. To po prostu bardzo delikatny kod - wart komentarza przynajmniej za każdym razem IMHO.
14
Bez względu na to, który wybierzesz, jest to jeden z tych przypadków, w których komentarz wyjaśniający, w jaki sposób i dlaczego jest absolutnie niezbędny. Późniejszy programista może nawet nie wziąć pod uwagę subtelności bez komentarza, aby zwrócić ich uwagę. Jestem również mocno rozproszony przez fakt, że buttonnic się nie zmienia w twojej pętli. Jak można wyświetlić listę przycisków? Poprzez indeks do tablicy lub inny mechanizm? Jeśli jest to przez dostęp indeksu do tablicy, jest to kolejny argument przemawiający za przejściem na liczby całkowite.
jpmc26
9
Napisz ten kod. Dopóki ktoś nie pomyśli, że 0,6 byłby lepszym rozmiarem kroku i po prostu zmieniłby tę stałą.
tofro
11
„... wprowadzać w błąd młodszych programistów” Będziesz także wprowadzać w błąd starszych programistów. Pomimo ilości myśli, którą w to włożyliście, założą, że nie wiedzieliście, co robicie, i prawdopodobnie zmienią to na wersję całkowitą.
GrandOpener

Odpowiedzi:

116

Zawsze unikałbym kolejnych operacji zmiennoprzecinkowych, chyba że wymaga tego model, który obliczam. Arytmetyka zmiennoprzecinkowa jest nieintuicyjna dla większości i jest głównym źródłem błędów. Mówienie o przypadkach, w których powoduje błędy od tych, których nie ma, jest jeszcze bardziej subtelnym rozróżnieniem!

Dlatego użycie liczb zmiennoprzecinkowych jako liczników pętli jest defektem, który czeka na wystąpienie i wymagałby co najmniej grubego komentarza w tle wyjaśniającego, dlaczego warto tutaj używać 0,5, i że zależy to od konkretnej wartości liczbowej. W tym momencie przepisanie kodu w celu uniknięcia liczników zmiennoprzecinkowych będzie prawdopodobnie bardziej czytelną opcją. A czytelność jest obok poprawności w hierarchii wymagań zawodowych.

Kilian Foth
źródło
48
Lubię „defekt czekający na zdarzenie”. Jasne, może teraz działać , ale lekka bryza przechodzącej przez kogoś przechodzącego ją przerwie.
AakashM
10
Załóżmy na przykład, że wymagania się zmieniają, więc zamiast 11 przycisków w równych odstępach od 0 do 5 z „standardową linią” na 4 przycisku, masz 16 przycisków w równych odstępach od 0 do 5 z „standardową linią” na 6 przycisk. Więc ktokolwiek odziedziczył ten kod od ciebie zmienia 0,5 na 1,0 / 3,0 i zmienia 1,5 na 5,0 / 3,0. Co się wtedy stanie?
David K
8
Tak, nie czuję się komfortowo z myślą, że zmiana czegoś, co wydaje się dowolną liczbą (tak „normalną”, jak mogłaby być liczba) na inną dowolną liczbę (która wydaje się równie „normalna”), faktycznie wprowadza defekt.
Alexander - Przywróć Monikę
7
@Alexander: tak, potrzebujesz komentarza DIFF must be an exactly-representable double that evenly divides STANDARD_LINE. Jeśli nie chcesz pisać tego komentarza (i polegać na tym, że wszyscy przyszli programiści wiedzą wystarczająco dużo o zmiennoprzecinkowym systemie IEEE754 binary64, aby go zrozumieć), nie pisz tego kodu w ten sposób. tzn. nie pisz kodu w ten sposób. Zwłaszcza, że ​​prawdopodobnie nie jest nawet bardziej wydajny: dodawanie FP ma większe opóźnienie niż dodawanie liczb całkowitych i jest zależnością od pętli. Ponadto kompilatory (nawet kompilatory JIT?) Prawdopodobnie lepiej radzą sobie z tworzeniem pętli z licznikami całkowitymi.
Peter Cordes
39

Zasadniczo pętle należy pisać w taki sposób, aby myśleć o zrobieniu czegoś n razy. Jeśli używasz wskaźników zmiennoprzecinkowych, nie jest to już kwestia robienia czegoś n razy, ale raczej bieganie do momentu spełnienia warunku. Jeśli ten warunek jest bardzo podobny do tego, i<nktórego spodziewa się tak wielu programistów, kod wydaje się robić jedną rzecz, podczas gdy faktycznie robi inną, co może być łatwo błędnie zinterpretowane przez programistów przeglądających kod.

Jest to nieco subiektywne, ale moim skromnym zdaniem, jeśli możesz przepisać pętlę, aby użyć indeksu liczb całkowitych do zapętlenia określonej liczby razy, powinieneś to zrobić. Rozważ więc następującą alternatywę:

var DIFF=0.5;                           // pixel increment
var MAX=Math.floor(5.0/DIFF);           // 5.0 is max pixel width
var STANDARD_LINE=Math.floor(1.5/DIFF); // 1.5 is pixel width

for(var i=0;i<=MAX;i++){
    button.text=(i*DIFF)+'';
    if(i==STANDARD_LINE){
      button.color='red';
    }
}

Pętla działa pod względem liczb całkowitych. W tym przypadku ijest liczbą całkowitą i STANDARD_LINEjest również wymuszana na liczbę całkowitą. To oczywiście zmieniłoby położenie linii standardowej, gdyby zdarzyło się zaokrąglić i podobnie MAX, więc powinieneś nadal starać się zapobiegać zaokrągleniu w celu dokładnego renderowania. Jednak nadal masz tę zaletę, że zmieniasz parametry w kategoriach pikseli, a nie liczb całkowitych, bez martwienia się o porównanie punktów zmiennoprzecinkowych.

Neil
źródło
3
Możesz również rozważyć zaokrąglenie zamiast podłogi w zadaniach, w zależności od tego, co chcesz. Jeśli podział ma dać wynik w postaci liczby całkowitej, podłoga może zaskoczyć, jeśli natkniesz się na liczby, w których podział jest nieco odbiegający.
ilkkachu
1
@ilkkachu True. Myślałem, że jeśli ustawisz 5.0 jako maksymalną liczbę pikseli, to zaokrąglając, wolisz raczej być na dolnej stronie tego 5.0 niż nieco więcej. 5.0 byłoby skutecznie maksimum. Chociaż zaokrąglanie może być lepsze w zależności od tego, co musisz zrobić. W obu przypadkach nie ma znaczenia, czy podział i tak utworzy liczbę całkowitą.
Neil
4
Definitywnie się z tym nie zgadzam. Najlepszym sposobem na zatrzymanie pętli jest stan, który w naturalny sposób wyraża logikę biznesową. Jeśli logika biznesowa polega na tym, że potrzebujesz 11 przycisków, pętla powinna zatrzymać się na iteracji 11. Jeśli logika biznesowa polega na tym, że przyciski są oddalone od siebie o 0,5, aż do zapełnienia linii, pętla powinna zatrzymać się, gdy linia jest pełna. Istnieją inne względy, które mogą popchnąć wybór w kierunku jednego lub drugiego mechanizmu, ale nieobecne w tych rozważaniach, wybierz mechanizm, który najbardziej odpowiada wymaganiom biznesowym.
Przywróć Monikę
Twoje wyjaśnienie byłoby całkowicie poprawna dla Java / C ++ / Ruby / Python / ... Ale Javascript nie ma liczb całkowitych, tak ii STANDARD_LINEtylko patrzeć jak całkowitymi. Nie ma przymusu w ogóle, a DIFF, MAXa STANDARD_LINEto wszystko po prostu Numbers. Numbers używane jako liczby całkowite powinny być bezpieczne poniżej 2**53, ale wciąż są liczbami zmiennoprzecinkowymi.
Eric Duminil
@EricDuminil Tak, ale to połowa tego. Druga połowa to czytelność. Wspominam o tym jako o głównym celu robienia tego w ten sposób, a nie optymalizacji.
Neil
20

Zgadzam się ze wszystkimi pozostałymi odpowiedziami, że użycie zmiennej pętli niecałkowitej jest ogólnie złym stylem, nawet w przypadkach takich jak ta, w której będzie działać poprawnie. Ale wydaje mi się, że jest jeszcze jeden powód, dla którego jest to zły styl.

Twój kod „wie”, że dostępne szerokości linii to dokładnie wielokrotności 0,5 od 0 do 5,0. Powinien? Wygląda na to, że decyzja podejmowana przez interfejs użytkownika może się łatwo zmienić (np. Może chcesz, aby odstępy między dostępnymi szerokościami były większe w miarę ich szerokości. 0,25, 0,5, 0,75, 1,0, 1,5, 2,0, 2,5, 3,0, 4,0, 5.0 lub coś takiego).

Twój kod „wie”, że wszystkie dostępne szerokości linii mają „ładne” reprezentacje zarówno jako liczby zmiennoprzecinkowe, jak i dziesiętne. To także wydaje się coś, co może się zmienić. (W pewnym momencie możesz chcieć 0.1, 0.2, 0.3, ...)

Twój kod „wie”, że tekst na przyciskach jest po prostu tym, w co JavaScript zamienia te wartości zmiennoprzecinkowe. To także wydaje się coś, co może się zmienić. (Na przykład, być może kiedyś będziesz potrzebować szerokości takich jak 1/3, których prawdopodobnie nie chciałbyś wyświetlać jako 0,333333333333333 itp. A może chcesz zobaczyć „1.0” zamiast „1”, aby zachować spójność z „1.5” .)

Wszystko to wydaje mi się przejawem pojedynczej słabości, która jest rodzajem mieszania warstw. Te liczby zmiennoprzecinkowe są częścią wewnętrznej logiki oprogramowania. Tekst wyświetlany na przyciskach jest częścią interfejsu użytkownika. Powinny być bardziej osobne niż są w kodzie tutaj. Pojęcia typu „który z nich jest domyślny i należy go wyróżnić?” są sprawami związanymi z interfejsem użytkownika i prawdopodobnie nie powinny być powiązane z tymi zmiennoprzecinkowymi wartościami. Twoja pętla jest naprawdę (a przynajmniej powinna być) pętlą nad przyciskami , a nie nad szerokością linii . Napisane w ten sposób pokusa, by użyć zmiennej pętli przyjmującej wartości niecałkowite, znika: będziesz po prostu używał kolejnych liczb całkowitych lub pętli for ... in / for ....

Mam wrażenie, że większość przypadków, w których można by pokusić się o zapętlanie liczb niecałkowitych, jest taka: istnieją inne powody, całkowicie niezwiązane z numerycznymi zagadnieniami, dlaczego kod powinien być inaczej zorganizowany. (Nie wszystkie przypadki; mogę sobie wyobrazić, że niektóre algorytmy matematyczne mogą być najdokładniej wyrażone w postaci pętli ponad wartościami niecałkowitymi).

Gareth McCaughan
źródło
8

Jeden zapach kodu używa takich pływaków w pętli.

Pętlę można wykonać na wiele sposobów, ale w 99,9% przypadków powinieneś trzymać się przyrostu 1, w przeciwnym razie na pewno będzie zamieszanie, nie tylko przez młodszych programistów.

Pieter B.
źródło
Nie zgadzam się, myślę, że wielokrotności liczby całkowitej 1 nie są mylące w pętli for. Nie uważałbym, że pachnie kodem. Tylko ułamki.
CodeMonkey
3

Tak, chcesz tego uniknąć.

Liczby zmiennoprzecinkowe są jedną z największych pułapek dla niczego niepodejrzewającego programisty (co z mojego doświadczenia wynika, że ​​prawie wszyscy). Od polegania na testach równości zmiennoprzecinkowej, po reprezentowanie pieniędzy jako zmiennoprzecinkowych - wszystko to jest wielkie bagno. Dodanie jednego pływaka do drugiego jest jednym z największych przestępców. Istnieje wiele tomów literatury naukowej na temat takich rzeczy.

Używaj liczb zmiennoprzecinkowych dokładnie tam, gdzie są one odpowiednie, na przykład podczas wykonywania rzeczywistych obliczeń matematycznych tam, gdzie ich potrzebujesz (takich jak trygonometria, wykreślanie wykresów funkcji itp.) I bądź bardzo ostrożny podczas wykonywania operacji szeregowych. Równość jest na wyciągnięcie ręki. Wiedza na temat tego, który konkretny zestaw liczb jest zgodny ze standardami IEEE, jest bardzo tajemnicza i nigdy bym na nim nie polegał.

W twoim przypadku, nie będzie , zgodnie z prawem Murphys, przyjść do punktu, gdzie zarządzanie chce nie mieć 0.0, 0.5, 1.0 ... ale 0,0, 0,4, 0,8 ... lub cokolwiek; zostaniesz natychmiast przekupiony, a twój młodszy programista (lub sam) będzie debugował długo i mocno, dopóki nie znajdziesz problemu.

W twoim konkretnym kodzie rzeczywiście miałbym zmienną z pętlą całkowitą. Reprezentuje iprzycisk th, a nie numer bieżący.

I prawdopodobnie, dla większej jasności, nie pisałbym, i/2ale i*0.5dzięki temu jest jasne, co się dzieje.

var BUTTONS=11;
var STANDARD_LINE=3;

for(var i=0; i<BUTTONS; i++) {
    button.text = (i*0.5)+'';
    if (i==STANDARD_LINE) {
      button.color='red';
    }
}

Uwaga: jak wskazano w komentarzach, JavaScript nie ma osobnego typu dla liczb całkowitych. Ale liczby całkowite do 15 cyfr są gwarantowane jako dokładne / bezpieczne (patrz https://www.ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer ), stąd dla takich argumentów („czy to bardziej mylące / podatne na działanie z liczbami całkowitymi lub niecałkowitymi ”), jest to odpowiednio bliskie oddzielnemu typowi„ ducha ”; w codziennym użytkowaniu (pętle, współrzędne ekranu, indeksy tablic itp.) nie będzie niespodzianek z liczbami całkowitymi reprezentowanymi Numberjako JavaScript.

AnoE
źródło
Zmieniłbym nazwę BUTTONS na coś innego - w końcu jest 11 przycisków, a nie 10. Może FIRST_BUTTON = 0, LAST_BUTTON = 10, STANDARD_LINE_BUTTON = 3. Poza tym tak, tak powinieneś to zrobić.
gnasher729
To prawda, @EricDuminil, i dodałem trochę o tym do odpowiedzi. Dziękuję Ci!
AnoE
1

Nie sądzę, aby któraś z twoich sugestii była dobra. Zamiast tego wprowadziłbym zmienną dla liczby przycisków na podstawie wartości maksymalnej i odstępów. Następnie wystarczy przejrzeć indeksy samego przycisku.

function precisionRound(number, precision) {
  let factor = Math.pow(10, precision);
  return Math.round(number * factor) / factor;
}

var maxButtonValue = 5.0;
var buttonSpacing = 0.5;

let countEstimate = precisionRound(maxButtonValue / buttonSpacing, 5);
var buttonCount = Math.floor(countEstimate) + 1;

var highlightPosition = 3;
var highlightColor = 'red';

for (let i=0; i < buttonCount; i++) {
    let buttonValue = i / buttonSpacing;
    button.text = buttonValue.toString();
    if (i == highlightPosition) {
        button.color = highlightColor;
    }
}

Może to być więcej kodu, ale jest też bardziej czytelny i bardziej niezawodny.

Jared Goguen
źródło
0

Możesz tego uniknąć, obliczając wyświetlaną wartość, zamiast używać licznika pętli jako wartości:

var MAX=5.0;
var DIFF=0.5
var STANDARD_LINE=1.5;

for(var i=0; (i*DIFF) < MAX ; i=i+1){
    var val = i * DIFF

    button.text=val+'';

    if(val==STANDARD_LINE){
      button.color='red';
    }
}
Arnab Datta
źródło
-1

Arytmetyka zmiennoprzecinkowa jest powolna, a arytmetyka liczb całkowitych jest szybka, więc kiedy używam zmiennoprzecinkowej, nie używałbym jej niepotrzebnie w przypadku, gdy można użyć liczb całkowitych. Przydatne jest zawsze myśleć o liczbach zmiennoprzecinkowych, nawet stałych, jako przybliżonych, z niewielkim błędem. Jest to bardzo przydatne podczas debugowania, aby zastąpić natywne liczby zmiennoprzecinkowe obiektami zmiennoprzecinkowymi plus / minus, w których każdą liczbę traktuje się jako zakres zamiast punktu. W ten sposób odkrywasz rosnące niedokładności po każdej operacji arytmetycznej. Zatem „1,5” należy traktować jako „pewną liczbę między 1,45 a 1,55”, a „1,50” należy traktować jako „pewną liczbę między 1,495 a 1,505”.

Jacquez
źródło
5
Różnica w wydajności między liczbami całkowitymi i liczbami zmiennoprzecinkowymi jest ważna podczas pisania kodu C dla małego mikroprocesora, ale współczesne procesory pochodzące z procesorów x86 wykonują zmiennoprzecinkowe tak szybko, że wszelkie kary można łatwo zlikwidować dzięki narzutom związanym z używaniem dynamicznego języka. W szczególności, czy JavaScript faktycznie nie reprezentuje każdej liczby jako liczby zmiennoprzecinkowej, używając w razie potrzeby ładunku NaN?
lewo wokół
1
„Arytmetyka zmiennoprzecinkowa jest powolna, a arytmetyka liczb całkowitych jest szybka” jest historycznym truizmem, którego nie powinieneś zachowywać, gdy ewangelia idzie naprzód. Aby dodać do tego, co powiedział @leftaroundabout, nie jest prawdą tylko, że kara byłaby prawie nieistotna, może się okazać, że operacje zmiennoprzecinkowe będą szybsze niż ich równoważne operacje na liczbach całkowitych, dzięki magii autowektoryzujących kompilatorów i zestawów instrukcji, które mogą ulec awarii duże ilości pływaków w jednym cyklu. To pytanie nie ma znaczenia, ale podstawowe założenie „liczba całkowita jest szybsza niż liczba zmiennoprzecinkowa” nie było prawdziwe od dłuższego czasu.
Jeroen Mostert
1
@JeroenMostert SSE / AVX ma wektoryzowane operacje zarówno na liczbach całkowitych, jak i na liczbach zmiennoprzecinkowych, i możesz być w stanie użyć mniejszych liczb całkowitych (ponieważ żadne bity nie są marnowane na wykładnik), więc w zasadzie często nadal można wycisnąć większą wydajność z wysoce zoptymalizowanego kodu liczb całkowitych niż z pływakami. Ale znowu, nie dotyczy to większości aplikacji i zdecydowanie nie dotyczy tego pytania.
leftaroundabout
1
@leftaroundabout: Jasne. Nie chodziło mi o to, który z nich jest zdecydowanie szybszy w danej sytuacji, po prostu „Wiem, że FP jest wolny, a liczba całkowita jest szybka, więc użyję liczb całkowitych, jeśli to w ogóle możliwe”, nie jest dobrą motywacją, nawet zanim podejmiesz pytanie, czy to, co robisz, wymaga optymalizacji.
Jeroen Mostert