2011-09-01

Problemy związane z liczbami zmiennoprzecinkowymi

Liczby zmiennoprzecinkowe są przechowywane w systemie binarnym, zaś wyświetlane i zapisywane z reguły w formacie dziesiętnym. Każda konwersja liczby zmiennoprzecinkowej na string i z powrotem może ale nie musi zmienić jej wartości. Głównie chodzi o to, że liczba wymierna w jednym formacie może być niewymierna w drugim. Jeśli mamy zamiar porównywać takie liczby to możemy zawsze przed porównaniem wymusić taką konwersję.

W ostateczności liczby możemy przechowywać w postaci tablicy bajtów (BitConverter), a w tekście w postaci HEX, w ten sposób zachowamy jej bitowy obraz.

Ale nie zawsze. Biorąc losowy ciąg bajtów i robiąc z niego liczbę zmiennoprzecinkową, może być nie poprawna, i w momencie załadowania jej do FPU jej wartość zostanie zmieniona. Tak długo jak to się nie stanie jej wygląd bitowy się nie zmieni. Dotyczy to np. niepoprawnych liczb które podpadają pod nieskończoności.

Porównywanie liczb zmiennoprzecinkowych bezpośrednio nawet jeśli nic z powyższego nie miało miejsca nie jest dobrym pomysłem. Wyniki mogą się różnić.

Spodziewamy się wartości znormalizowanej wektora o długości 1. Możemy dostać coś bliskiego 1, i zawsze z różną precyzją, zależnie czy np. jest to normalna policzona dla sfery, czy trójkąta. Nie pozostaje nam wtedy nic innego jak napisać kod porównujący liczby z pewną precyzją i kontrolować ją w debug assertami. W razie asserta musimy sami zdecydować czy to błąd, czy może brak precyzji i nasza stała precyzji musi się stać bardziej tolerancyjna.

Żeby nie było za łatwo wyniki w debug, release, x86, x64 mogą się od siebie różnic. Przyczyn może być wiele. Główna to taka, że liczby zmiennoprzecinkowe są przechowywane wewnętrznie w FPU w formacie 80-bitowym i taka jest domyślny jego tryb działania w C#. Każdy zapis takiej liczby do pamięci to jej zaokrąglenie. Może ona nastąpić np. na skutek braku optymalizacji pomiędzy wersją debug, a release. A np. pomiędzy x86, a x64 w wyniku innej optymalizacji operacji.

Szczególnie fatalnym przypadkiem takiego zaokrąglania z 80 do 64 bitów jest pojawienie się zera albo nieskończoności.

Liczby zmiennoprzecinkowe mogą występować w dwóch postaciach znormalizowanej i dla bardzo małych liczb w postaci zdenormalizowanej. W moim przypadku operacje na liczbach zdenormalizowanych są 7 razy wolniejsze. Liczby te są bardzo małe i są daleko poza zakresem wszystkich dokładności używanych w raytracerze. Liczby zdenormalizowane służą zasypaniu luki w możliwych do uzyskania liczb zmiennoprzecinkowych w okolicach zera. Istnieje pewna możliwość, że wejdziemy w zakres liczb zdenormalizowanych i znacząco spowolnimy renderowanie. Na razie nie przejmuje się tym.

Odjęcie od siebie dwóch różnych liczb zmiennoprzecinkowych może dać w wyniku zero. Podobnie jak podzielenie jakiejś liczby przez np. 2. Dzieje się tak jeśli brakuje nam ekspomenty na zapisanie wyniku. Podobnie jak w zero możemy w pewnym momencie wejść w nieskończoności. Jednak to wejście w zero jest najmnniej intuicyjne. O błędach tego rodzaju trzeba pamiętać w pętlach. Podczas obliczeń można ich spórbować uniknąć tak przekształcając równanie by zastąpić odejmowanie dodawaniem. Dla raytracingu nie to sensu. Z reguły są to tak małe liczby daleko poza zakresem precyzji z jaką operujemy na obiektach sceny. Klasycznym przykładem podawanym w różnych miejscach jest takie przepisanie wzorów na pierwiastki kwadratowe by wyeliminować odejmowanie jeśli b>0:

$\begin{align*}
x_{1,2} = & \frac{-b-\sqrt{b^2-4ac}}{2ac}=\\& \frac{(-b-\sqrt{b^2-4ac})(-b+\sqrt{b^2-4ac})}{2ac(-b+\sqrt{b^2-4ac})}=\\& \frac{4ac}{2ac(-b+\sqrt{b^2-4ac})}=\\&\frac{2}{-b+\sqrt{b^2-4ac}}
\end{align*}$

Brak komentarzy:

Prześlij komentarz