[C++] Wektory i przybliżenia

@Ryan

Też się na początku na tym przejechałem i sobie ubzdurałem, że ma sprawdzić, czy dane trzy wektory tworzą trójkąt. Otóż nie, ma sprawdzić, czy z trzech wektorów po wyliczeniu ich długości można zbudować trójkąt. Wynika z tego, że położenie tych wektorów w układzie współrzędnych nie ma znaczenia.

Jeśli tak, to ich długość musi spełniać nierówność trójkąta i żaden nie może mieć długości zerowej, nic więcej. Sumując długości dwóch krótszych odcinków zminimalizuje błąd.

//edyta

Nie, wróć. Zminimalizuje błąd wykonując operację arytmetyczną na dwóch najbardziej zbliżonych do siebie wartościach.

Mógłbyś rozwinąć dlaczego tak?

To wynika ze sposobu konstrukcji floatów (i doubli i generalnie wszystkich zmiennoprzecinkowych reprezentacji liczb wzorowanych na IEEE-754). Na liczbę składa się mantysa (1.########…) i wykładnik (2^(#-offset)). Jeśli wykonujesz operacje dodawania lub odejmowania, tracisz tyle precyzji w mantysie, ile wynosi różnica w wykładnikach +1. To znaczy, że dodając dwie liczby o tym samym wykładniku, zwiększy się on o 1, a z wyniku sumowania mantys ucięty zostanie jeden bit. Jeśli jest on jedynką - straciłeś precyzję. Zasadniczo im większa jest różnica między wartościami dodawanymi/odejmowanymi, tym więcej bitów zostaje odciętych. Oczywiście statystycznie: jeśli tam są zera, to nic nie tracisz (np. wynik 2+32 będzie tak samo precyzyjny jak 2+2, bo zarówno 2 jak i 32 mają w mantysie same zera). Podobnie wygląda to też w przypadku wartości zdegenerowanych.

Hm, musiałbym sobie rozpisać parę liczb na bity, żeby to zobaczyć, ale dzięki za odpowiedź.

To zdanie zbija mnie trochę z tropu. Czyli suma zmiennych typu zmiennoprzecinkowego, których wartości to liczby naturalne powinna dać dokładny wynik? Czy też może będzie to wynik przybliżony, ale obarczony takim samym błędem w przypadku tych dwóch różnych działań? W pytaniu chodzi mi o to co masz na myśli mówiąc “wynik będzie tak samo precyzyjny”.

PS Jedno wydaje mi się być pewne. Jeśli nie trzeba przejmować się ilością pamięci operacyjnej i konieczne jest użycie typu zmiennoprzecinkowego to lepiej od razu wykonywać obliczenia na zmiennych typu double.

Nie chodzi o liczby naturalne a reprezentację wartości we float. 5/4 też będzie “ładnie” zapisane (jedynka na najstarszym bicie).

Przez “wynik będzie tak samo precyzyjny” rozumiem w tym konkretnym przypadku “wynik nie będzie obarczony żadnym błędem”. Obie liczby (2 i 32) mają reprezentację we float obarczoną zerowym błędem (nie będzie błędu przy konwersji int->float) a różnica ich wykładników nie jest większa niż rozmiar mantysy (23). Błędy dodawania/odejmowania wynikają z tego, że obie uczestniczące w operacji wartości należy sprowadzić do wspólnego wykładnika, zanim zsumuje się lub odejmie od siebie mantysy. Jeśli przesunięcie to (różnica wykładników) jest większe niż rozmiar mantysy (czy raczej: szerokość sumatora/subtraktora), to tracisz precyzję.

I nie, nie jest prawdą, że warto zawsze wykonywać operacje na liczbach podwójnej precyzji. Praktycznie nie istnieją zastosowania, w których wymagana jest wysoka precyzja, w których double oferuje wystarczający względem float wzrost precyzji. Jeśli zależy Ci na przemienności działań i przewidywalnym błędzie, użyjesz liczb stałoprzecinkowych. Z drugiej strony większość zastosowań wymagających liczb zmiennoprzecinkowych nie ucierpi z racji wykonywania operacji na floatach. Co więcej będą one szybsze i zjedzą mniej miejsca.

Czy floaty na pewno zawsze są szybsze?

A co do praktyki (zalecam próbę zgadnięcia różnicy wyniku przed uruchomieniem kodu):

#include 

#include 

#include 


int main()

{

    {

        double a = 12345678.0;

        double b = 12345679.0;

        double c = 1.01233995;

        double p = (a + b + c) / 2;

        double w = sqrt(p * (p - a) * (p - b) * (p - c));


        printf("Pole (double) = %.10f\n", w);

    }


    {

        float a = 12345678.0f;

        float b = 12345679.0f;

        float c = 1.01233995f;

        float p = (a + b + c) / 2;

        double w = sqrt(p * (p - a) * (p - b) * (p - c));


        printf("Pole (float) = %.10f\n", w);

    }

}

@somekind

Matko, co za absurdalny wynik -,-. Nawet zmniejszając ilość miejsc po przecinku do pięciu i samą liczbę do 12345 wynik jest niepoprawny. Dobry przykład. Ja natrafiłem na trochę inny test http://www.icm.edu.pl/kdm/Biuletyn_nr_11. W tym teście wykonano sumę liczb naturalnych od 1 do 1000000 operując na double i float. Wyniki również są ciekawe. Te dwa testy utwierdzają mnie tylko w przekonaniu, że jeśli można to lepiej jest użyć double.

@Ryan

Skoro już wspomniałeś o zmiennych stałoprzecinkowych … Jaki Twoim zdaniem jest lepszy sposób na przechowywanie waluty? Zmienna stałoprzecinkowa, czy też może dwie zmienne całkowite przechowujące grosze oraz złotówki?

Wiem, że pytanie nie do mnie, ale to nie jest przypadkiem technicznie to samo?

Dla mnie jedyna widoczna różnica to właśnie sposób zapisu. Mój błąd, że pytanie skierowałem do jednej osoby (dziwnie to wyszło). Uznajmy, że nie miało ono mieć konkretnego adresata, żeby każdy mógł wyrazić opinię. Jeszcze może trochę doprecyzuję pytanie:

Jakich typów używacie do przechowywania waluty zarówno w bazie danych, jak i w aplikacji? (w językach programowania oraz silnikach bazodanowych, których używacie). Ja na podstawie przewertowanych kiedyś tam dyskusji wolę rozwiązanie oparte o typ stałoprzecinkowy w bazie danych i typ System.Decimal (.NET) w aplikacji. Mimo tego, że ten drugi zajmuje w pamięci operacyjnej aż 16 bajtów (2x tyle co double).

To nie do mnie pytanie, ale jeśli język udostępnia typ stałoprzecinkowy, to czemu z niego nie skorzystać?

Z drugiej strony, sam typ stałoprzecinkowy nie zawsze gwarantuje poprawność obliczeń (np. czasami podział jakiejś kwoty może skutkować “zgubieniem” groszy, a w poważnym systemie to nie jest dopuszczalne), dlatego niektórzy zalecają tworzenie własnych klas do operowania na pieniądzach z uwzględnieniem specyfiki tych operacji.

Nie miałem okazji pisać czegoś takiego, ale prawdopodobnie zrobiłbym własną klasę opartą o dwa inty, tak jak sugeruje somekind.

Nie znam sprzętu, na którym nie byłyby.

Zdecydowanie nie. Sam staram się przechowywać cenę w groszach i jedynie odpowiednio formatować wartość przy wyświetlaniu go. Sformalizowana reprezentacja stałoprzecinkowa nie ma sensu, bo nie mnoży się waluty przez walutę (co to za jednostka: Zł^2? ;-] ), więc operacje inne niż mnożenie przez liczby naturalne i sumowanie/odejmowanie cen nie są potrzebne. Zamiast zatem bawić się w formalizmy, lepiej trzymać cenę z groszach. Robi tak część transakcyjnych API, np. platnosci.pl i najwyraźniej sprawdza się to u nich. :wink: Rozbijanie wartości jest IMO kompletnie chybionym pomysłem: wymuszasz budowanie logiki operującej na przenoszeniu nadmiarowej jedynki z sumy groszy do złotych… brrrr! Straszny pomysł.

Fakt, pomieszałem z tym rozbiciem waluty na dwie części. Sprowadzenie wszystkiego do groszy (czytaj najmniejszej możliwej jednostki) jest lepszym pomysłem.

A teraz dlaczego wolę zmienne stałoprzecinkowe. Mówisz, że mnożenie waluty przez walutę nie pojawi się. To prawda, ale już np. mnożenie waluty przez podatek albo waluty przez jakiś inny procent może się pojawić. Mało tego, dla danej kwoty może się pojawić konieczność wykonania kilku takich mnożeń przez różne procenty (np. zastosowanie różnych zniżek - choć może nie jest to najlepszy przykład, bo zniżki można przecież najpierw zsumować). Zastanawiam się jak w sytuacji zapamiętywania wszystkiego jako grosze poradzić sobie z taką operacją. Mnożenie wartości całkowitych i zapamiętywanie pozycji przecinka w jakiejś osobnej zmiennej?

Jeżeli będziesz trzymać pozycje przecinka w osobnej zmiennej to dostaniesz praktycznie to samo co zmienna zmiennoprzecinkowa.

Zasady operacji na walutach już dawno opracowane w księgowości.

Stosuje się obliczenia w 1/100 grosza lub nawet 1/10000 grosza, czyli 4 lub 6 znaków po przecinku.

A nawet przy takim stosowaniu nie unikniesz tak zwanego (w księgowości) odchylenia praktycznie po każdej operacji.

Przykład przy założeniu że wartości obliczamy do 4-go znaku:

Masz w magazynie 1 litr kleju w cenie 12.35 za litr o wartości 1*12.35 = 12.3500

Wydajesz 1 ml kleju o wartości 12.35*0.001 = 0.0124

Powinno ci zostać 0.999 litra o wartości: 12.35*0.999 = 12.3377

Z tym że 12.3500 - 0.0124 = 12.3376

Czyli skądś się pojawiła 1/100 grosza (w innych przypadkach na odwrót - znika).

Przy obliczeniu jakichkolwiek procentów nie ma możliwości aby nic nie stracić no chyba że dla reprezentacja liczby zmiennoprzecinkowej użyjesz nieskończonej ilości bitów.

Prosty przykład odliczasz 30% rabatu z 10 zł wynik 6.(6) czyli nie ważne ile cyfr po przecinku zapiszesz (oprócz nieskończonej ilości) i tak będzie zaokrąglenie.

Powyższy przykład dotyczy reprezentacji dziesiętnej, dla wewnętrznej reprezentacji binarnej dobrym przykładem będzie 10%.