Nieporozumienie arytmetyki zmiennoprzecinkowej i jej niedociągnięć jest główną przyczyną zaskoczenia i zamieszania w programowaniu (rozważ liczbę pytań na temat przepełnienia stosu dotyczących „nieprawidłowego dodawania liczb”). Biorąc pod uwagę, że wielu programistów jeszcze nie zrozumiało jego konsekwencji, może wprowadzić wiele subtelnych błędów (szczególnie w oprogramowaniu finansowym). Co mogą zrobić języki programowania, aby uniknąć pułapek dla tych, którzy nie znają pojęć, a jednocześnie oferują szybkość, gdy dokładność nie jest krytyczna dla tych, którzy rozumieją pojęcia?
language-design
Adam Paynter
źródło
źródło
Odpowiedzi:
Mówisz „specjalnie dla oprogramowania finansowego”, co przywołuje jedno z moich ulubionych pomysłów: pieniądze nie są zmienne, to int .
Jasne, wygląda jak pływak. Ma tam przecinek dziesiętny. Ale to tylko dlatego, że jesteś przyzwyczajony do jednostek, które mylą problem. Pieniądze zawsze przychodzą w liczbach całkowitych. W Ameryce to centy. (W niektórych kontekstach myślę, że mogą to być młyny , ale na razie zignoruj to.)
Kiedy powiesz 1,23 USD, to naprawdę 123 centy. Zawsze, zawsze, zawsze wykonuj matematykę w tych kategoriach, a wszystko będzie dobrze. Aby uzyskać więcej informacji, zobacz:
Odpowiadając bezpośrednio na pytanie, języki programowania powinny zawierać typ pieniędzy jako rozsądną prymitywność.
aktualizacja
Ok, powinienem był powiedzieć „zawsze” dwa razy, a nie trzy razy. Pieniądze są rzeczywiście zawsze int; Ci, którzy myślą inaczej, mogą spróbować wysłać mi 0,3 centa i pokazać mi wynik na wyciągu bankowym. Ale jak zauważają komentatorzy, są rzadkie wyjątki, kiedy trzeba wykonywać matematykę zmiennoprzecinkową na liczbach podobnych do pieniędzy. Np. Niektóre rodzaje kalkulacji cen lub odsetek. Nawet wtedy należy je traktować jak wyjątki. Pieniądze przychodzą i wychodzą jako liczby całkowite, więc im bardziej twój system się do tego zbliży, tym będzie zdrowszy.
źródło
Decimal
jest jedynym rozsądnym systemem radzenia sobie z tym, a twój komentarz „zignoruj to na razie” jest zwiastunem zagłady dla programistów na całym świecie: PZapewnienie obsługi typu dziesiętnego pomaga w wielu przypadkach. Wiele języków ma typ dziesiętny, ale są one w niepełnym użyciu.
Ważne jest zrozumienie przybliżenia występującego podczas pracy z reprezentacją liczb rzeczywistych. Używanie zarówno liczb dziesiętnych, jak i zmiennoprzecinkowych
9 * (1/9) != 1
jest poprawną instrukcją. Gdy stałe optymalizator może zoptymalizować obliczenia, aby były poprawne.Pomocne byłoby podanie przybliżonego operatora. Takie porównania są jednak problematyczne. Pamiętaj, że 0,9999 biliona dolarów to w przybliżeniu 1 bilion dolarów. Czy mógłbyś zdeponować różnicę na moim koncie bankowym?
źródło
0.9999...
bilion dolarów to dokładnie dokładnie 1 bilion dolarów.0.99999...
. Wszystkie one w pewnym momencie obcinają się, co powoduje nierówność.0.9999
jest wystarczający dla inżynierii. Dla celów finansowych tak nie jest.Powiedziano nam, co robić na pierwszym roku (drugiego) wykładu z informatyki, kiedy poszedłem na uniwersytet (ten kurs był warunkiem koniecznym dla większości kursów informatycznych)
Pamiętam, że wykładowca powiedział: „Liczby zmiennoprzecinkowe są przybliżone. Używaj typów całkowitych dla pieniędzy. Używaj FORTRAN lub innego języka z liczbami BCD, aby uzyskać dokładne obliczenia”. (a następnie wskazał przybliżenie, używając tego klasycznego przykładu 0,2 niemożliwego do dokładnego przedstawienia w binarnym zmiennoprzecinkowym). To również pojawiło się w tym tygodniu w ćwiczeniach laboratoryjnych.
Ten sam wykład: „Jeśli musisz uzyskać większą dokładność od liczb zmiennoprzecinkowych, posortuj terminy. Dodawaj małe liczby razem, a nie duże.” To utkwiło mi w pamięci.
Kilka lat temu miałem pewną sferyczną geometrię, która musiała być bardzo dokładna i wciąż szybka. 80-bitowe podwojenie na komputerach PC nie powodowało cięcia, więc dodałem do programu kilka typów, które posortowały terminy przed wykonaniem operacji przemiennych. Problem rozwiązany.
Zanim narzekasz na jakość gitary, naucz się grać.
Cztery lata temu miałem współpracownika, który pracował dla JPL. Wyraził niedowierzanie, że użyliśmy FORTRAN do niektórych celów. (Potrzebowaliśmy bardzo dokładnych symulacji numerycznych obliczonych offline.) „Zastąpiliśmy cały ten FORTRAN C ++” - powiedział z dumą. Przestałem się zastanawiać, dlaczego przegapili planetę.
źródło
1.0 + 0.1 + ... + 0.1
(powtarzane 10 razy) powracają,1.0
gdy każdy wynik pośredni zostanie zaokrąglony. Robi to w drugą stronę, można uzyskać wyniki pośrednie0.2
,0.3
...,1.0
a na końcu2.0
. Jest to skrajny przykład, ale przy realistycznych liczbach zmiennoprzecinkowych zdarzają się podobne problemy. Podstawową ideą jest to, że dodanie liczb o podobnej wielkości prowadzi do najmniejszego błędu. Zacznij od najmniejszych liczb, ponieważ ich suma jest większa i dlatego lepiej nadaje się do dodawania do większych.Nie wierzę, że cokolwiek można lub należy zrobić na poziomie językowym.
źródło
Decimal
przypadku testów równości. Różnica między1.0m/7.0m*7.0m
i1.0m
może być o wiele rzędów wielkości mniejsza niż różnica między1.0/7.0*7.0
, ale nie jest równa zero.Domyślnie języki powinny używać argumentów o dowolnej dokładności dla liczb niecałkowitych.
Ci, którzy muszą zoptymalizować, zawsze mogą poprosić o pływaki. Używanie ich jako domyślnych miało sens w językach programowania C i innych systemach, ale nie w większości popularnych obecnie języków.
źródło
double
. Jeśli obliczenia muszą być dokładne dla części na milion, lepiej poświęcić mikrosekundę na obliczenie z dokładnością do kilku części na miliard, niż spędzić sekundę na obliczeniu absolutnie dokładnie.Dwa największe problemy dotyczące liczb zmiennoprzecinkowych to:
Pierwszego rodzaju awarii można zaradzić tylko poprzez podanie typu złożonego, który zawiera informacje o wartości i jednostce. Na przykład a
length
lubarea
wartość, która zawiera jednostkę (odpowiednio metry lub metry kwadratowe lub stopy i stopy kwadratowe). W przeciwnym razie musisz bardzo uważać, aby zawsze pracować z jednym rodzajem jednostki miary i konwertować na inny tylko wtedy, gdy dzielimy się odpowiedzią z człowiekiem.Drugi rodzaj niepowodzenia to błąd koncepcyjny. Niepowodzenia objawiają się, gdy ludzie myślą o nich jako o liczbach bezwzględnych . Wpływa na operacje równościowe, skumulowane błędy zaokrąglania itp. Na przykład może być poprawne, że dla jednego systemu dwa pomiary są równoważne w ramach pewnego marginesu błędu. Tj .999 i 1.001 są mniej więcej takie same jak 1.0, gdy nie przejmujesz się różnicami mniejszymi niż +/- .1. Jednak nie wszystkie systemy są tak łagodne.
Jeśli potrzebne jest jakieś narzędzie na poziomie językowym, nazwałbym to precyzją równości . W NUnit, JUnit i podobnie skonstruowanych ramach testowych możesz kontrolować dokładność uważaną za poprawną. Na przykład:
Gdyby na przykład C # lub Java zostały zmienione w celu włączenia operatora precyzyjnego, mogłoby to wyglądać mniej więcej tak:
Jeśli jednak podasz taką funkcję, musisz również wziąć pod uwagę przypadek, w którym równość jest dobra, jeśli strony +/- nie są takie same. Na przykład + 1 / -10 uważa, że dwie liczby są równoważne, jeśli jedna z nich była w odległości 1 więcej lub 10 mniej niż pierwsza liczba. Aby poradzić sobie z tą sprawą, może być konieczne dodanie
range
słowa kluczowego:źródło
Co potrafią języki programowania? Nie wiem, czy jest jedna odpowiedź na to pytanie, ponieważ wszystko, co kompilator / tłumacz robi w imieniu programisty, aby jego życie było łatwiejsze, zwykle działa wbrew wydajności, przejrzystości i czytelności. Myślę, że zarówno sposób C ++ (zapłać tylko za to, czego potrzebujesz), jak i sposób Perla (zasada najmniejszego zaskoczenia) są poprawne, ale zależy to od aplikacji.
Programiści wciąż muszą pracować z językiem i rozumieć, w jaki sposób obsługuje zmiennoprzecinkowe, ponieważ jeśli nie, przyjmą założenia, a pewnego dnia określone zachowanie nie będzie zgodne z ich założeniami.
Moje zdanie na temat tego, co powinien wiedzieć programista:
źródło
Użyj rozsądnych wartości domyślnych, np. Wbudowana obsługa decmials.
Groovy robi to całkiem nieźle, choć przy odrobinie wysiłku możesz nadal pisać kod, aby wprowadzić nieprecyzyjną zmiennoprzecinkową.
źródło
Zgadzam się, że nie ma nic do zrobienia na poziomie językowym. Programiści muszą zrozumieć, że komputery są dyskretne i ograniczone, a wiele przedstawionych na nich pojęć matematycznych jest jedynie przybliżeniem.
Nie wspominając o zmiennoprzecinkowym. Trzeba zrozumieć, że połowa wzorów bitowych jest używana dla liczb ujemnych i że 2 ^ 64 jest w rzeczywistości dość małe, aby uniknąć typowych problemów z arytmetyką liczb całkowitych.
źródło
x
==y
nie oznacza, że wykonanie obliczeń nax
da taki sam wynik jak wykonanie tego samego obliczenia nay
).Języki mogą zrobić jedną rzecz - usunąć porównanie równości z typów zmiennoprzecinkowych inne niż bezpośrednie porównanie z wartościami NAN.
Testy równości istniałyby tylko jako wywołanie funkcji, które wzięło dwie wartości i deltę, lub dla języków takich jak C #, które pozwalają typom mieć metody EqualsTo, który przyjmuje drugą wartość i deltę.
źródło
Wydaje mi się dziwne, że nikt nie wskazał racjonalnej sztuczki liczbowej rodziny Lisp.
Poważnie, otwórz sbcl i zrób to:
(+ 1 3)
a dostaniesz 4. Jeśli*( 3 2)
dostaniesz 6. Teraz spróbuj(/ 5 3)
i dostaniesz 5/3, czyli 5 trzecich.To powinno trochę pomóc w niektórych sytuacjach, prawda?
źródło
Jedną rzecz chciałbym zobaczyć byłoby uznanie, że
double
nafloat
należy traktować jako rozszerzającej nawrócenia, podczas gdyfloat
dodouble
zwęża (*). Może się to wydawać sprzeczne z intuicją, ale zastanów się, co faktycznie oznaczają te typy:Jeśli ktoś ma
double
najlepszą reprezentację wielkości „jedna dziesiąta” i konwertuje jąfloat
, wynikiem będzie „13 421 773,5 / 134 217 728, plus lub minus 1 / 268,435,456”, co jest poprawnym opisem wartości.Dla kontrastu, jeśli ktoś ma
float
najlepszą reprezentację wielkości „jedna dziesiąta” i konwertuje jądouble
, wynik będzie wynosić „13 421 7733,5 / 134 217 728, plus lub minus 1/72 057 594,037,927,936 lub więcej” - poziom implikowanej dokładności co jest błędne ponad 53 milionami razy.Chociaż standard IEEE-744 wymaga, aby matematyka zmiennoprzecinkowa była wykonywana tak, jakby każda liczba zmiennoprzecinkowa reprezentowała dokładną liczbę liczbową dokładnie w środku jej zakresu, nie należy zakładać, że wartości zmiennoprzecinkowe faktycznie reprezentują te dokładne wielkości liczbowe. Wymóg, aby przyjąć, że wartości znajdują się w środku ich zakresów, wynika z trzech faktów: (1) obliczenia muszą być wykonane tak, jakby argumenty miały pewne szczególne dokładne wartości; (2) spójne i udokumentowane założenia są bardziej pomocne niż niespójne lub nieudokumentowane; (3) jeśli ktoś zamierza przyjąć spójne założenie, żadne inne spójne założenie nie może być lepsze niż założenie, że ilość reprezentuje środek jego zakresu.
Nawiasem mówiąc, pamiętam jakieś 25 lat temu, ktoś wymyślił pakiet numeryczny dla C, który używał „typów zakresów”, z których każdy składał się z pary 128-bitowych liczb zmiennoprzecinkowych; wszystkie obliczenia zostałyby wykonane w taki sposób, aby obliczyć minimalną i maksymalną możliwą wartość dla każdego wyniku. Jeśli ktoś wykona duże, długie obliczenie iteracyjne i uzyska wartość [12.53401391134 12.53902812673], można mieć pewność, że choć wiele cyfr precyzji zostało utraconych z powodu błędów zaokrąglania, wynik nadal można rozsądnie wyrazić jako 12,54 (i to nie było t naprawdę 12,9 lub 53,2). Dziwię się, że nie widziałem żadnego wsparcia dla takich typów w żadnym z głównych języków, zwłaszcza że wydaje się, że dobrze pasują do jednostek matematycznych, które mogą działać na wielu wartościach równolegle.
(*) W praktyce często pomocne jest stosowanie wartości podwójnej precyzji do przechowywania obliczeń pośrednich podczas pracy z liczbami o pojedynczej precyzji, dlatego użycie wszystkich typów operacji może być denerwujące. Języki mogłyby pomóc, mając typ „rozmytego podwójnego”, który wykonywałby obliczenia jako podwójny i mógłby być swobodnie przesyłany do iz pojedynczego; byłoby to szczególnie pomocne, gdyby funkcje, które pobierają parametry typu
double
i powrotu,double
mogły zostać oznaczone, aby automatycznie generowały przeciążenie, które akceptuje i zwraca zamiast tego „rozmyte podwójne”.źródło
Jeśli więcej języków programowania pobierze stronę z baz danych i pozwoli programistom określić długość i precyzję liczbowych typów danych, mogą znacznie zmniejszyć prawdopodobieństwo wystąpienia błędów związanych z liczbą zmiennoprzecinkową. Jeśli język pozwolił deweloperowi zadeklarować zmienną jako zmiennoprzecinkową (2), co wskazuje, że potrzebował liczby zmiennoprzecinkowej z dwiema cyframi dokładności dziesiętnej, może wykonywać operacje matematyczne znacznie bezpieczniej. Gdyby to zrobił reprezentując wewnętrznie zmienną jako liczbę całkowitą i dzieląc przez 100 przed ujawnieniem wartości, mógłby poprawić prędkość, stosując szybsze ścieżki arytmetyczne liczb całkowitych. Semantyka Float (2) pozwoliłaby także programistom uniknąć ciągłej potrzeby zaokrąglania danych przed wysłaniem ich, ponieważ Float (2) z natury zaokrągla dane do dwóch miejsc po przecinku.
Oczywiście musisz zezwolić programistom na zapytanie o zmiennoprzecinkową maksymalną precyzję, gdy musi ona mieć tę precyzję. I wprowadzilibyśmy problemy, w których nieco inne wyrażenia tej samej operacji matematycznej dają potencjalnie różne wyniki z powodu pośrednich operacji zaokrąglania, gdy programiści nie mają wystarczającej precyzji w swoich zmiennych. Ale przynajmniej w świecie baz danych nie wydaje się to zbyt dużym problemem. Większość ludzi nie wykonuje takich obliczeń naukowych, które wymagają dużej precyzji w wynikach pośrednich.
źródło
Float(2)
jak proponujesz, nie należy nazywaćFloat
, ponieważ nic tu nie unosi się, z pewnością nie „kropka dziesiętna”.Powyższe mają zastosowanie w niektórych przypadkach, ale tak naprawdę nie są ogólnym rozwiązaniem do obsługi wartości zmiennoprzecinkowych. Prawdziwym rozwiązaniem jest zrozumienie problemu i nauczenie się, jak sobie z nim radzić. Jeśli korzystasz z obliczeń zmiennoprzecinkowych, zawsze powinieneś sprawdzić, czy algorytmy są stabilne numerycznie . Istnieje ogromna dziedzina matematyki / informatyki związana z problemem. Nazywa się to analizą numeryczną .
źródło
Jak zauważyły inne odpowiedzi, jedynym prawdziwym sposobem uniknięcia pułapek zmiennoprzecinkowych w oprogramowaniu finansowym jest nieużywanie go w tym miejscu. Może to być faktycznie wykonalne - jeśli zapewnisz dobrze zaprojektowaną bibliotekę poświęconą matematyce finansowej .
Funkcje zaprojektowane do importowania liczb zmiennoprzecinkowych powinny być wyraźnie oznaczone jako takie i wyposażone w parametry odpowiednie dla tej operacji, np .:
Jedynym prawdziwym sposobem uniknięcia pułapek zmiennoprzecinkowych w ogóle jest edukacja - programiści muszą przeczytać i zrozumieć coś takiego, co każdy programista powinien wiedzieć o arytmetyce zmiennoprzecinkowej .
Kilka rzeczy, które mogą pomóc:
isNear()
funkcji.źródło
Większość programistów zdziwiłaby się, że COBOL ma rację… w pierwszej wersji COBOL nie było zmiennoprzecinkowych, tylko dziesiętne, a tradycja w języku COBOL trwa do dziś, że pierwszą rzeczą, o której myślisz, deklarując liczbę, jest dziesiętna. ... zmiennoprzecinkowy byłby używany tylko wtedy, gdy naprawdę byłby potrzebny. Kiedy pojawił się C, z jakiegoś powodu nie było prymitywnego typu dziesiętnego, więc moim zdaniem, tam zaczęły się wszystkie problemy.
źródło