Odejmowanie, zapis liczb i przybliżenia w C.

0
  1. Powiedzmy, że mam fragment programu w C:

x=x-0.125;

Jak wygląda to działanie "od kuchni"? x-owi przypisywana jest wartość "x odjąć 0.125" czy "x dodać -0.125"? Komputer odejmuje liczbę czy dodaje liczbę ujemną?

  1. Jak zapisuje się liczby np. 0.1 i -0.1? Jak to wygląda "dla komputera"? Jakim systemem zapisywane są ułamki i ułamki ujemne?

  2. Jak komputer przybliża liczby?

0

odpowiedź na 2 i 3: przeczytaj sobie o systemie binarnym i U2

odpowiedź na 1:

  1. pobierana jest zawartość zmiennej x
  2. odejmowane jest od niej 0.125
  3. wynik jest zapisywany do pamieci pod adresem zmiennej x

wiecej: asembler, programowanie niskopoziomowe, budowa kompilatora, sposób pracy procesora

0

co do zapisu w kodzie binarnym U2 to masz np.

-0.1 w dziesiątkowym binarnie wygląda to tak
pierw musimy mieć w binarnym naturalnym somo 0.1 czyli :
0.1 * 2 = 0.2 daje nam 0;
0.2 * 2 = 0.4 daje nam 0;
0.4 * 2 = 0.8 daje nam 0;
0.8 * 2 = 1.6 daje nam 1;
0.6 * 2 = 1.2 daje nam 1;
i tak dalej(jak zauważysz będzie to miało okres ale w komuterze przybliżasz to do 2 okresów )
wynikiem będzie : 0 000110011 ( w dziesiątkowym 0.1)

teraz zamieniamy na kod U2 czyli w tym wypadku;

2 - 0.1 (robimy odejmowanie binarne)
co nam daje: 1 111001101 w kodzie U2 czyli tak wygląda liczba -0.1 w kodzie U2
a jak chcesz zamienić tą liczbe z binarnego na dziesiętny to robisz to tak:
2 - 0.1 tylko ,że tu wstawiasz kod z U2

w zapisie binarnym nie stosuje się przecinków tylko spacje by oddzielić całości od ułamku

0

O systemie binarnym i U2 to ja wiem, nie wiedziałem tylko, czy i jak to się stosuje do ułamków.

Moje pytania sprowadzają się do tego, żeby odpowiedzieć na pytanie dlaczego 0.1 + 0.1 + 0.1 - 0.1 - 0.1 - 0.1 nie jest równe 0. Doszedłem do tego, że inaczej zapisuje się liczby dodatnie i ujemne, w związku z tym - są też inne przybliżenia (dlatego nie wychodzi 0). Ale skoro komputer odejmuje liczbę dodatnią, a nie - tak jak przypuszczałem - dodaje ujemną, to z czego wynika wynik różny od zera dla 0.1 + 0.1 + 0.1 - 0.1 - 0.1 - 0.1?

0

Jeśli chodzi o niedokładność operacji zmiennoprzecinkowych, to musisz poczytać o standardzie IEEE 754 dotyczącym liczb zmiennoprzecinkowych. Najogólniej mówiąc, liczba ułamkowa jest przechowywana w postaci znak-cecha-mantysa. Wartość liczby można obliczyć według wzoru:

x = (-1)<sup>{s} \cdot 2</sup>{e} \cdot m

W tym wzorze s to znak (sign), e to wykładnik (exponent), a m to mantysa (mantissa). Więcej o tym możesz poczytać tutaj: http://www.eioba.pl/a2231/standard_ieee_754

Wspomniane kodowanie U2 dotyczy liczb całkowitych przechowywanych w komputerze, liczby zmiennoprzecinkowe działają inaczej. Operacje na liczbach całkowitych zawsze są dokładne.

// EDIT
Nie słuchaj Gelldura, bo jak zwykle wypowiada się na nieznajome sobie tematy ze znajomą sobie ignorancją. Jeśli chcesz wiedzieć, jak w pamięci przechowywane są liczby ułamkowe, to sprawdź sobie ten edytor (ale najpierw zapoznaj się z wcześniejszym linkiem): http://www.ajdesigner.com/fl_ieee_754_word/ieee_32_bit_word.php. Ten kalkulator dotyczy tylko liczb 32-bitowego typu Single (float w C/C++/C#/Java). Typ Double (podwójnej precyzji) używa do zapisu 64 bitów.

A skąd bierze się niedokładność przy operacji 0.1 + 0.1 + 0.1 - 0.1 - 0.1 - 0.1? Dlatego, że liczba 0.1 ma nieskończone rozwinięcie binarne (podobnie jak 1/3 w systemie dziesiętnym -- dokładna wartość to 0.333333333...33333...). A że dysponujemy tylko 64 bitami (a dokładnie 52 bitami ułamkowymi mantysy), nie jesteśmy w stanie dokładnie zapisać wartości 0.1. Przy kolejnych obliczeniach ten błąd narasta.

Tak wygląda binarny zapis liczby Double o wartości 0.1:
0 01111111011 1001100110011001100110011001100110011001100110011001

W mantysie widać okres rozwinięcia ułamka -- jest nieskończone, a zapisane na skończonej ilości bitów daje błąd.

0

Znak mantysy? Znak cechy? Pierwsze słyszę... Radzę zapoznać się ze standardem, bo próbujesz zdeprecjonować to, co mówię, ale coś ci nie wychodzi.

co do zapisu w kodzie binarnym U2 to masz np.

Liczby zmiennoprzecinkowe nie są kodowane w kodzie U2. U2 dotyczy liczb całkowitych lub stałoprzecinkowych, których w PC-tach się raczej nie używa.

Kod U2 używa się do wyrażania całkowitych wartości ujemnych. W floating-point numbers nie ma takiej potrzeby, gdyż osobno kodowany jest bit znaku liczby. Mantysa przyjmuje wartości z przedziału [1..2), lub w specjalnym przypadku liczby nieznormalizowaniej z przedziału [0..1). Jedynie cecha liczby przyjmuje wartości ujemne, ale ona kodowana jest z nadmiarem (U1), a nie uzupełnieniem do dwóch (U2).

w zapisie binarnym nie stosuje się przecinków tylko spacje by oddzielić całości od ułamku

Nie wiem, co miałeś tu na myśli. Znakiem oddzielającym może być cokolwiek, ja najczęściej spotykałem się z kropką. Ale taki zapis dotyczy tylko liczb stałoprzecinkowych, faktycznie złożonych z dwóch liczb typu integer. Floatów to nie dotyczy. I nie ma przełożenia na właściwą reprezentację liczby w komputerze, to tylko konwencja zapisu na kartce papieru.

od tego ,że ty odejmujesz raz kodem naturalnym a raz U2 musisz wszystko sprowadzić np do U2 i wyjdzie 0

Totalna bzdura. Chcesz powiedzieć, że komputer w różny sposób wykonuje działania i stąd pojawiają się błędy zaokrąglenia? :|

Ponadto, wykonywanie działań w U2 niczym nie różni się od działań w kodzie "naturalnym", więc nie mam pojęcia, jak wpadłeś tutaj na możliwość błędu.

0

Ok, to ja spróbuję z 32-bitowym. Mniej więcej poczytałem, teraz mam parę pytań.

0.1 to będzie 0 01111011 10011001100110011001100 (tylko tu mam wątpliwości, czy mantysę uciąć tak jak jest, czy przybliżyć do 10011001100110011001101?)

-0.1 to 1 01111011 10011001100110011001100 (tu podobna wątpliwość dla mantysy).

Ale teraz jak wpisuję te ciągi do tego http://www.ajdesigner.com/fl_ieee_754_word/ieee_32_bit_word.php to uzyskuję dwie liczby przeciwne -0.1 i 0.1, których suma jest zerem, ew. dwie liczby których wartość bezwzględna to niecałe 0.1, ale to też daje w końcu 0. To o co tu chodzi?

0

Cóż, operacja 0.1 - 0.1, tudzież 0.1 + (-0.1) zawsze da w wyniku dokładnie 0. Jednak w wyrażeniu 0.1 + 0.1 + 0.1 - 0.1 - 0.1 - 0.1 dzieje się coś innego. Co takiego?

Prosty program w C++ pozwala sprawdzić wartość bitową danej liczby zmiennoprzecinkowej. Oto jego źródło:

#include <iostream>
#include <sstream>
#include <string>

using namespace std;

struct fl
{
	union
	{
		float val;
		unsigned int bit;
	};
	
	string print() const
	{
		ostringstream os;
		
		// sign
		os << (bit >> 31) << " ";
		
		// exponent
		for (int i = 0; i < 8; ++i)
			os << ((bit >> (30 - i)) & 1);
		os << " ";
		
		// mantissa
		for (int i = 0; i < 23; ++i)
			os << ((bit >> (22 - i)) & 1);
		
		// value
		os << " [" << (*(float*)&val) << "]";
		
		return os.str();
	}
};

int main()
{
	fl x;
	x.val = 0.1f;
	cout << x.print() << endl;
	x.val += 0.1f;
	cout << x.print() << endl;
	x.val += 0.1f;
	cout << x.print() << endl;
	x.val -= 0.1f;
	cout << x.print() << endl;
	x.val -= 0.1f;
	cout << x.print() << endl;
	x.val -= 0.1f;
	cout << x.print() << endl;

	return 0;
}

Program ten krok po kroku oblicza wspomniane wyrażenie. Startuje od wartości 0.1, potem dodaje dwukrotnie 0.1, a następnie trzykrotnie odejmuje. W wynikach programu można zauważyć, co dzieje się z mantysą liczby:

0 01111011 10011001100110011001101 [0.1]
0 01111100 10011001100110011001101 [0.2]
0 01111101 00110011001100110011010 [0.3]
0 01111100 10011001100110011001110 [0.2]
0 01111011 10011001100110011001111 [0.1]
0 01100101 00000000000000000000000 [1.49012e-08]

Ważna uwaga Wszelkie operacje wewnątrz koprocesora wykonywane są z maksymalną możliwą precyzją. Często jest to precyzja 80-bitowa! Dopiero po wykonaniu działania, wyliczona wartość jest konwertowana do precyzji takiej, jaka jest żądana.

Można prześledzić, co się dzieje. Jak widać, na najmniej znaczącym bicie mantysy od początku mamy niedokładność, która zaburza okres 00110011001100... . Przy pierwszym dodaniu wartości 0.1, wartość mantysy się nie zmieniła. Dlaczego? bo dla 0.1 + 0.1 błąd jest zbyt mały i w wyniku zaokrąglenia (ucięcia) mantysy do długości 23 bitów został zniwelowany.

Jednak przy drugim dodawaniu mantysa psuje się. W ogóle, operacje dodawania i odejmowania liczb zmiennoprzecinkowych są obarczone największym błędem, szczególnie wtedy, gdy ich cechy są różne (tak jak teraz). Jest tak dlatego, że aby liczby mogły zostać dodane lub odjęte, muszą zostać

  1. sprowadzone do wspólnej, większej cechy (utrata precyzji) oraz
  2. zdenormalizowane.
    Oznacza to tyle, po sprowadzeniu do wspólnej cechy, mantysa obu operandów jest przesuwana bitowo w prawo o 2, a cecha zwiększana o wartość 2. Po co? Aby "wciągnąć" do mantysy ten domyślny bit jedności, oraz po jego lewej stronie zostawić dodatkowe miejsce na ewentualne przeniesienie. Może lepiej będzie to widoczne na przykładzie (mocno uproszczonym, z mantysą == 1):
x = 0 01111100 00000000000000000000000 [0.125]
y = 0 01111110 00000000000000000000000 [0.5]

Sumowanie

1) Sprowadzenie do wspólnej cechy
Większą cechę ma liczba y, więc do takiej wartości cechy sprowadzany jest x (co już jest denormalizacją):
x = 0 01111110 01000000000000000000000 [0.125]

2) Denormalizacja
Należy zwiększyć cechę y o 2, tak, aby "domyślny" bit znajdował się w mantysie i przed nim było jeszcze miejsce na ewentualne przeniesienie:
y = 0 10000000 01000000000000000000000 [0.5]

Wciąż musimy zachować zgodność cechy w obu liczbach, więc również o 2 zwiększamy cechę x:
x = 0 10000000 000100000000000000000 [0.125]

Teraz w normalny sposób dodajemy do siebie mantysy:
res = 0 10000000 010100000000000000000 [0.625]

Teraz następuje normalizacja wyniku -- to znaczy przesuwamy mantysę o tyle bitów w lewo, aż utniemy pierwszy zapalony bit, a mantysę zmniejszamy o wartość równą przesunięciu. W tym przypadku przesuwamy w lewo o 2 bity i cechę zmniejszamy o 2:
res = 0 01111110 01000000000000000000000 [0.625]

Wszystkie te operacje, które pokazałem, nie są możliwe do zauważenia -- denormalizacja etc. zachodzi wewnątrz koprocesora arytmetycznego.

I teraz: dlaczego dodawanie i odejmowanie powodują tak duże błędy zaokrąglenia? Zauważmy, że ostatni bit mantysy jest zaokrągleniem powodującym błąd -- rzeczywista wartość liczby jest nieco większa od wartości 0.1. Przy kolejnych dodawaniach ten błąd się powiększa i propaguje na bardziej znaczące bity mantysy. Po drugim dodawaniu mantysa jest zaburzona, i to na bardziej znaczącym miejscu niż dla wartości wyjściowej. Teraz operacje odejmowania będą propagować ten błąd dalej.

// EDIT
Teraz przyjrzałem się temu, co napisałem, i obawiam się, że mogłem trochę skomplikować. Dygresje kiedyś się na mnie zemszczą. ;) Jeśli wciąż jest to niejasne, oto wyjaśnienie w pigułce: rzeczywista wartość jest odrobinę większa od 0.1 ze względu na zaokrąglenie w górę ostatniego bitu mantysy. Podczas dodawania ten błąd również się dodaje. Dlaczego odejmowanie nie niweluje go? Ponieważ odejmowanie działa odrobinę inaczej, zasady przenoszenia i zaokrąglania są trochę inne. Łatwo można sprawdzić, że wyrażenie 0.1 + 0.1 + 0.1 - (0.1 + 0.1 + 0.1) jest dokładnie równe 0.

// EDIT 2

0.1 to będzie 0 01111011 10011001100110011001100 (tylko tu mam wątpliwości, czy mantysę uciąć tak jak jest, czy przybliżyć do 10011001100110011001101?)

Mantysa zawsze jest zaokrąglana do najbliższej wartości możliwej do reprezentacji -- w tym wypadku do 10011001100110011001101.

0

Ok, wszystko już do mnie dotarło. Dziękuję bardzo za pomoc.

1 użytkowników online, w tym zalogowanych: 0, gości: 1