Twierdzisz, że w liczbach zmiennoprzecinkowych są nieodłączne niedokładności. Myślę, że zasługuje na to, aby najpierw trochę to zbadać.
Decydując się na system liczbowy do reprezentowania liczby (czy to na kartce papieru, w obwodzie komputerowym, czy w innym miejscu), są dwa oddzielne kwestie do rozważenia:
-
jego podstawa; i
-
jego format .
Wybierz podstawę, dowolną podstawę…
Ograniczone skończoną przestrzenią, nie można reprezentować dowolnego członka nieskończonego zbioru
. Na przykład:bez względu na to, ile kupisz papieru lub jak małe pismo odręczne, zawsze będzie można znaleźć liczbę całkowitą, która nie zmieści się w danym miejscu (możesz po prostu dodawać dodatkowe cyfry, aż skończy się papier). Tak więc z liczbami całkowitymi , zwykle ograniczamy naszą skończoną przestrzeń do reprezentowania tylko tych, które mieszczą się w określonym przedziale — np. jeśli mamy miejsce na znak dodatni/ujemny i trzy cyfry, możemy ograniczyć się do przedziału [-999,+999]
.
Każdy niepusty przedział
zawiera nieskończony zbiór liczb rzeczywistych. Innymi słowy, niezależnie od tego, w jakim przedziale zajmiemy liczby rzeczywiste — czy to [-999,+999]
, [0,1]
, [0.000001,0.000002]
lub cokolwiek innego — w tym przedziale wciąż istnieje nieskończony zbiór liczb rzeczywistych (wystarczy tylko dodawać (niezerowe) cyfry ułamkowe)! Dlatego dowolne liczby rzeczywiste muszą zawsze być „zaokrąglonym” do czegoś, co może być reprezentowane w skończonej przestrzeni.
Zbiór liczb rzeczywistych, które można przedstawić w skończonej przestrzeni, zależy od używanego systemu liczbowego. W naszym (znanym) pozycjonowaniu
base-10
systemu, skończona przestrzeń wystarczy na połowę (0.510
), ale nie dla jednej trzeciej (0.33333…10
); przeciwnie, w (mniej znanym) pozycyjnym base-9
system, jest odwrotnie (te same liczby to odpowiednio 0.44444…9
i 0.39
). Konsekwencją tego wszystkiego jest to, że niektóre liczby, które można przedstawić za pomocą niewielkiej ilości miejsca w pozycyjnej podstawie 10 (i dlatego pojawiają się być bardzo „okrągłym” dla nas ludzi), m.in. jedna dziesiąta wymagałaby dokładnego przechowywania nieskończonych obwodów binarnych (i dlatego nie wydaje się być bardzo „okrągła” dla naszych cyfrowych przyjaciół)! Warto zauważyć, że ponieważ 2 jest współczynnikiem 10, to samo nie jest w odwrotnej kolejności:każda liczba, którą można przedstawić za pomocą skończonej liczby binarnej, może być również przedstawiona za pomocą skończonej liczby dziesiętnej.
Nie możemy zrobić nic lepszego dla ciągłych ilości. Ostatecznie takie wielkości muszą używać skończonej reprezentacji w niektórych system liczbowy:jest arbitralne, czy ten system jest łatwy dla obwodów komputerowych, dla ludzkich palców, dla czegoś innego lub w ogóle – bez względu na to, który system jest używany, wartość musi być zaokrąglone i dlatego zawsze powoduje „błąd reprezentacji”.
Innymi słowy, nawet jeśli ktoś ma idealnie dokładny przyrząd pomiarowy (co jest fizycznie niemożliwe), to każdy raportowany przez niego pomiar zostanie już zaokrąglony do liczby, która akurat mieści się na jego wyświetlaczu (w dowolnej podstawie, której używa — zwykle dziesiętnej, z oczywistych powodów). Tak więc „86,2 uncji” nigdy nie jest tak naprawdę „86,2 uncji " ale raczej reprezentacją "coś między 86.1500000... oz a 86.2499999... oz (Właściwie, ponieważ w rzeczywistości instrument jest niedoskonały, wszystko, co możemy naprawdę powiedzieć, to to, że mamy jakiś stopień pewności że rzeczywista wartość mieści się w tym przedziale — ale to zdecydowanie odbiega od tego punktu).
Ale możemy zrobić lepiej dla dyskretnych ilości . Takie wartości nie są "dowolnymi liczbami rzeczywistymi" i dlatego żadne z powyższych nie ma do nich zastosowania:można je przedstawić dokładnie w systemie liczbowym, w którym zostały zdefiniowane — i rzeczywiście powinny być (ponieważ konwersja do innego systemu liczbowego i skrócenie do skończonej długości spowodowałoby zaokrąglenie do niedokładnej liczby). Komputery mogą (nieefektywnie) radzić sobie z takimi sytuacjami, przedstawiając liczbę jako ciąg:np. rozważ ASCII lub BCD kodowanie.
Zastosuj format…
Ponieważ jest to właściwość (nieco arbitralnej) podstawy systemu liczbowego, to, czy wartość wydaje się być „okrągła”, nie ma wpływu na jej precyzję . To naprawdę ważna obserwacja , co jest sprzeczne z intuicją wielu ludzi (i to jest powód, dla którego spędziłem tak dużo czasu na wyjaśnieniu powyżej podstaw liczbowych).
Dokładność jest natomiast określana przez ilość znaczących cyfr
reprezentacja ma . Potrzebujemy formatu przechowywania, który będzie w stanie rejestrować nasze wartości co najmniej tyle znaczących liczb ile uważamy za poprawne . Biorąc przykładowe wartości, które uważamy za poprawne, gdy są określone jako 86.2
i 0.0000862
, dwie najpopularniejsze opcje to:
-
Stały punkt , gdzie liczba cyfr znaczących zależy od wielkości :np. w stałej reprezentacji z 5 punktami dziesiętnymi nasze wartości byłyby przechowywane jako
86.20000
i0.00009
(a zatem mają odpowiednio 7 i 1 cyfry znaczącej precyzji). W tym przykładzie utracono precyzję w tej ostatniej wartości (i rzeczywiście, nie zajęłoby nam wiele więcej, abyśmy byli całkowicie niezdolni do reprezentowania niczego znaczenie); i poprzednia wartość przechowywana fałszywa precyzja , co jest marnotrawstwem naszej skończonej przestrzeni (i rzeczywiście, nie zajęłoby dużo więcej, aby wartość stała się tak duża, że przepełniłaby pojemność pamięci).Typowym przykładem tego, kiedy ten format może być odpowiedni, jest system księgowy:sumy pieniężne muszą być zwykle śledzone co do grosza niezależnie od ich wielkości (dlatego w przypadku małych wartości wymagana jest mniejsza precyzja, a w przypadku dużych wartości wymagana jest większa precyzja). Tak się składa, że waluta jest zwykle również uważana za dyskretną (grosze są niepodzielne), więc jest to również dobry przykład sytuacji, w której określona podstawa (dziesiętna dla większości nowoczesnych walut) jest pożądana, aby uniknąć omówionych powyżej błędów reprezentacji.
-
Punkt zmiennoprzecinkowy , gdzie liczba cyfr znaczących jest stała niezależnie od wielkości :np. w pięciocyfrowej reprezentacji dziesiętnej nasze wartości byłyby przechowywane jako
86.200
i0.000086200
(i, z definicji, mają 5 cyfr znaczących precyzji za każdym razem). W tym przykładzie obie wartości zostały zapisane bez utraty precyzji; i oboje mają również tę samą ilość fałszywej precyzji, co jest mniej marnotrawstwem (i dlatego możemy wykorzystać naszą skończoną przestrzeń do reprezentowania znacznie większego zakresu wartości — zarówno dużych, jak i małych).Typowym przykładem tego, kiedy ten format może być odpowiedni, jest rejestrowanie wszelkich rzeczywistych pomiarów :precyzja przyrządów pomiarowych (które wszystkie cierpią z powodu zarówno systematycznej i losowe błędów) jest dość stała niezależnie od skali, więc biorąc pod uwagę wystarczającą liczbę cyfr znaczących (zwykle około 3 lub 4 cyfr), absolutnie nie traci się precyzji, nawet jeśli zmiana podstawy skutkowała zaokrągleniem do innej liczby .
Ale jak dokładne są formaty zapisu zmiennoprzecinkowego używane przez nasze komputery?
-
IEEE754 zmiennoprzecinkowe pojedynczej precyzji (binary32) liczba ma 24 bity, czyli
log10(2)
(powyżej 7) cyfr o znaczeniu – tj. ma tolerancję mniejszą niż±0.000006%
. Innymi słowy, jest to bardziej precyzyjne niż powiedzenie „86.20000
". -
IEEE754 zmiennoprzecinkowy podwójnej precyzji (binary64) liczba ma 53 bity, czyli
log10(2)
(prawie 16) cyfr o znaczeniu – tj. ma tolerancję nieco ponad±0.00000000000001%
. Innymi słowy, jest to bardziej precyzyjne niż powiedzenie „86.2000000000000
".
Najważniejszą rzeczą do zrealizowania jest to, że te formaty to odpowiednio ponad dziesięć tysięcy i ponad jeden bilion razy bardziej precyzyjne niż powiedzenie „86,2” — mimo że dokładna konwersja binarnego z powrotem na dziesiętny zawiera błędną fałszywą precyzję (którą musimy zignorować:więcej o tym wkrótce)!
-
Zwróć też uwagę, że oba naprawiono i Formaty zmiennoprzecinkowe spowodują utratę precyzji, gdy wartość jest znana dokładniej niż obsługuje format. Takie błędy zaokrąglania
może propagować w operacjach arytmetycznych, dając pozornie błędne wyniki (co bez wątpienia wyjaśnia twoje odniesienie do „wrodzonych niedokładności” liczb zmiennoprzecinkowych):na przykład ⁄3 × 3000
w 5-miejscowym punkcie stałym dałoby 999.99000
zamiast 1000.00000
; i ⁄7 − ⁄50
w 5-znacznej cyfrze zmiennoprzecinkowa dałaby 0.0028600
zamiast 0.0028571
.
Dziedzina analizy numerycznej poświęca się zrozumieniu tych efektów, ale ważne jest, aby zdać sobie sprawę, że dowolny użyteczny system (nawet wykonywanie obliczeń w głowie) jest podatny na takie problemy, ponieważ żadna metoda obliczeń, która ma gwarancję zakończenia, nigdy nie oferuje nieskończonej precyzji :zastanów się, na przykład, jak obliczyć pole koła — z konieczności nastąpi utrata precyzji wartości użytej do π, która przeniesie się na wynik.
Wniosek
-
Pomiary w świecie rzeczywistym powinny używać binarnych liczb zmiennoprzecinkowych :jest szybki, kompaktowy, niezwykle precyzyjny i nie gorszy niż cokolwiek innego (w tym wersja dziesiętna, od której zacząłeś). Ponieważ zmiennoprzecinkowe typy danych MySQL są zgodne ze standardem IEEE754, to jest dokładnie to, co oferują.
-
Aplikacje walutowe powinny używać stałego punktu denarowego :chociaż jest powolny i marnuje pamięć, zapewnia zarówno, że wartości nie są zaokrąglane do niedokładnych ilości, jak i grosze nie są tracone przy dużych sumach pieniężnych. Ponieważ typy danych stałopunktowych MySQL są ciągami zakodowanymi w BCD, dokładnie to oferują.
Na koniec pamiętaj, że języki programowania zwykle reprezentują wartości ułamkowe za pomocą binarnych liczb zmiennoprzecinkowych typy:więc jeśli twoja baza danych przechowuje wartości w innym formacie, musisz uważać, jak są one wprowadzane do twojej aplikacji, w przeciwnym razie mogą zostać przekonwertowane (ze wszystkimi wynikającymi z tego problemami) w interfejsie.
Która opcja jest najlepsza w tym przypadku?
Mam nadzieję, że przekonałem Cię, że Twoje wartości mogą bezpiecznie (i powinny ) być przechowywane w typach zmiennoprzecinkowych, nie martwiąc się zbytnio o „niedokładności”? Pamiętaj, są więcej precyzyjne niż kiedykolwiek była twoja marna 3-cyfrowa reprezentacja dziesiętna:musisz po prostu zignorować fałszywą precyzję (ale trzeba zawsze zrób to mimo wszystko, nawet jeśli używasz formatu dziesiętnego ze stałym przecinkiem).
Jeśli chodzi o twoje pytanie:wybierz opcję 1 lub 2 zamiast opcji 3 — ułatwia to porównania (na przykład, aby znaleźć maksymalną masę, można po prostu użyć MAX(mass)
, podczas gdy zrobienie tego efektywnie w dwóch kolumnach wymagałoby pewnego zagnieżdżenia).
Między tymi dwoma nie ma znaczenia, który z nich wybierzesz — liczby zmiennoprzecinkowe są przechowywane ze stałą liczbą znaczących bitów niezależnie od ich skali .
Ponadto, podczas gdy w ogólnym przypadku może się zdarzyć, że niektóre wartości są zaokrąglane do liczb binarnych, które są bliższe ich oryginalnej reprezentacji dziesiętnej za pomocą opcji 1, podczas gdy jednocześnie inne są zaokrąglane do liczb binarnych, które są bliższe ich oryginalnej reprezentacji dziesiętnej za pomocą opcji 2, ponieważ wkrótce zobaczymy, że takie błędy reprezentacji przejawiają się tylko w fałszywej precyzji, którą należy zawsze ignorować.
Jednak w tym przypadku, ponieważ zdarza się, że do 1 funta przypada 16 uncji (a 16 to potęga 2), względne różnice między oryginalnymi wartościami dziesiętnymi a przechowywanymi liczbami binarnymi przy użyciu tych dwóch podejść są identyczne :
-
5.387510
(nie5.3367187510
jak podano w pytaniu) będzie przechowywany w zmiennej zmiennoprzecinkowej binary32 jako101.0110001100110011001102
(czyli5.3874998092651367187510
):to jest0.0000036%
od pierwotnej wartości (ale, jak omówiono powyżej, „pierwotna wartość” była już dość kiepską reprezentacją fizycznej wielkości, którą reprezentuje).Wiedząc, że liczba zmiennoprzecinkowa binary32 przechowuje tylko 7 cyfr dziesiętnych precyzji, nasz kompilator wie pewne że wszystko od ósmej cyfry wzwyż jest zdecydowanie fałszywa precyzja i dlatego musi być ignorowane w co wielkość liter — zatem pod warunkiem, że nasza wartość wejściowa nie wymagała większej precyzji (a jeśli tak, to binary32 był oczywiście złym wyborem formatu), to gwarantuje powrót do wartości dziesiętnej, która wygląda tak samo okrągłą jak ta, od której zaczęliśmy:
5.38750010
. Jednak naprawdę powinniśmy zastosować wiedzę o domenie w tym momencie (jak powinniśmy w każdym formacie przechowywania), aby odrzucić wszelkie dalsze fałszywe precyzje, które mogą istnieć, takie jak te dwa końcowe zera. -
86.210
będzie przechowywany w zmiennej zmiennoprzecinkowej binary32 jako1010110.001100110011001102
(czyli86.199996948242187510
):jest to również0.0000036%
od pierwotnej wartości. Tak jak poprzednio, ignorujemy fałszywą precyzję, aby powrócić do naszych pierwotnych danych wejściowych.
Zwróć uwagę, że binarne reprezentacje liczb są identyczne, z wyjątkiem umieszczenia punktu podstawy (co jest oddalone o cztery bity):
101.0110 00110011001100110 101 0110.00110011001100110
Dzieje się tak, ponieważ 5,3875 × 2 =86,2.