Błędy na floatach

0

Jak procesor oblicza różnicę/sumę dwóch liczb rzeczywistych, jak float lub double?

Żeby było jaśniej możemy chyba założyć, że procesor używa arytmetyki na liczbach dziesiętnych;
ponadto ograniczamy się do 3 cyfr precyzji.

Konkretnie pytanie wygląda tak:
float3(10.0 - 9.99) = ?

co taki komputer wyliczyłby na tych floatach - z trzema cyframi precyzji?

0

Liczby zmiennoprzecinkowe są zwykle przechowywane w formie (znak, mantysa, cecha) - liczba to znak * mantysa * 2^cecha - mantysa to jakaś znormalizowana liczba (np z zakresu [1...2)) - dokładność określa (pośrednio, trzeba ilość bitów mantysy. Cecha może być oczywiście dodatnia albo ujemna. Przy dodawaniu i odejmowaniu przesuwa się mantysę w taki sposób, aby obydwa argumenty miały tę samą cechę (mniejszej liczbie denormalizuje się mantysę) a następnie dodaje/odejmuje takie zdenormalizowane mantysy. Mantysa wyniku jest normalizowana i może być użyta w dalszych obliczeniach.

0

OK. Zatem jaki byłby wynik na maszynie, która używa arytmetyki float na trzech cyfrach precyzji:
float3(10.0 - 9.99) = ?

0

Jaki jest w ogóle standard - wymóg, odnośnie obliczeń na takich liczbach?

np.
2.0 - 1.999999999999999999999 = ?

na binarnych wyglądałoby to tak:

2^1 x 0.100000000 - 2^0 x 0.111111111111;
53 cyfry dla double (ewentualnie 64 dla long double na FPU);

i co tu wyliczy komputer: 0.00 czy też coś innego?

Należy chyba użyć SSE2 z double, i tyle
albo na FPU, ale wtedy to pójdzie zapewne na 64 bitach...

podobnie:
4 - 3.999999999999999 = ?
8 - 7.999999999999999 = ?
itd.

jakie wyniki produkują procesory?

0
jarekr000000 napisał(a):

"Standard" https://en.wikipedia.org/wiki/IEEE_floating_point

http://stackoverflow.com/questions/2234468/do-any-real-world-cpus-not-use-ieee-754

Zatem jaki jest wynik?

2^2 * 0.100000000000000 - 2^1 * 0.1111111111111111111 = ?

co wyprodukuje procesor: 0.000000000000 czy też 0.000000000001?

1
pelikan napisał(a):

Konkretnie pytanie wygląda tak:
float3(10.0 - 9.99) = ?

Na pewno nie wygląda tak jakbyś chciał.

Pytanie jak wygląda 0.3 - 0.1 - 2.0 * 0.1 ?
Wynik to dokładnie 1.49011611938477e-08

#include <iostream>
#include <iomanip>

int main() 
{
    float a,b,c;
    a = 0.3;
    b = 0.1;
    c = 0.1;
    
    float d = a - b - 2.0f * c;
    std::cout << std::setprecision (15) << d << std::endl;
    
    return 0;
}

Chcesz liczyć kwoty pieniężne czy robisz obliczenia inżynierskie?

0
vpiotr napisał(a):
pelikan napisał(a):

Konkretnie pytanie wygląda tak:
float3(10.0 - 9.99) = ?

Na pewno nie wygląda tak jakbyś chciał.

Pytanie jak wygląda 0.3 - 0.1 - 2.0 * 0.1 ?
Wynik to dokładnie 1.49011611938477e-08

#include <iostream>
#include <iomanip>

int main() 
{
    float a,b,c;
    a = 0.3;
    b = 0.1;
    c = 0.1;
    
    float d = a - b - 2.0f * c;
    std::cout << std::setprecision (15) << d << std::endl;
    
    return 0;
}

Chcesz liczyć kwoty pieniężne czy robisz obliczenia inżynierskie?

Pytam się o wynik w zadanej reprezentacji, czyli w takiej, w jakiej jest to faktycznie obliczane.

Przykłady były:

a= 2.0 = 2^2 x 0.1000000000....b = 4 x 0.5
zatem bierzemy teraz najmniejszą z możliwych, czyli 2 - eps:
b= 2^1 x 0.11111111111111......1b = 2 x (1-eps) < 2.0

liczba cyfr precyzji jest tu ustalona, np. double ma 53.

cechy są różne - jak widać: 2 i 1, zatem jaki będzie wynik odejmowania - na realnym procesorze?
a-b = ?

0

To jest test, więc należy to sprawdzić na różnych procesorach - jak amd, intel i plus różne wersje;

ponadto odejmowanie należy wykonywać z taką samą precyzją w jakiej są te liczby, tz.:

  • w przypadku operacji na FPU należy użyć long double = 80 bit, w tym 64 precyzji
  • na SSE2 należy użyć double - 53 bity precyzji
  • single float też można sprawdzić - 24 bity precyzji... raczej na SSE, bo FPU oblicza zazwyczaj na full 64bits = long double.
0

Ale to nie będzie proste z kilku powodów.

  1. format wewnętrzny w FPU może byc różny w zalezności od fpu - w zasadzie od 64 do 128 bitów w takich spotykanych obecnie lub dawniej.
  2. sam format przy tej samej bitowości może być różny (niekoniecznie zgodny ze standardem)
  3. operacje o ile operacja 1 + 2 jest w miarę przewidywalna, to joz 1 + 2 * 3 niekoniecznie, bo może być kwestia taka, że operacje zostana wykonane albo z przeformatowaniem na format wynikowy, albo wewnątrz koprocesora, w zależności od możliwości/kompilatora/koprocesora.
0
kaczus napisał(a):

Ale to nie będzie proste z kilku powodów.

  1. format wewnętrzny w FPU może byc różny w zalezności od fpu - w zasadzie od 64 do 128 bitów w takich spotykanych obecnie lub dawniej.
  2. sam format przy tej samej bitowości może być różny (niekoniecznie zgodny ze standardem)
  3. operacje o ile operacja 1 + 2 jest w miarę przewidywalna, to joz 1 + 2 * 3 niekoniecznie, bo może być kwestia taka, że operacje zostana wykonane albo z przeformatowaniem na format wynikowy, albo wewnątrz koprocesora, w zależności od możliwości/kompilatora/koprocesora.

SSE liczy zarówno na floatach jak na double, i tam nie ma żadnych tajnych rejestrów po 80, bo są 4 po 32 albo 2 x 64.
Na FPU należy obliczać na 80 bitowych - long double;
ewentualnie można próbować zmienić precyzję obliczeń, ustawiając flagę koprocesora na: single precyzję, double,
bo domyślnie jest zazwyczaj: full - 64 bity precyzji = long double (kompilator z MS - visual studio ustawia chyba precyzję na double, bo tam nie ma typu long double).

0

Operacje typu: a + bc, są robione osobno, tz.: u = bc a potem u + a, czyli błąd może przekraczać eps - precyzję maszyny.

Gdybyś chciał to obliczyć od razu (z błędem eps), należy użyć instrukcji typu FMA.

0
  1. format wewnętrzny w FPU może byc różny w zalezności od fpu - w zasadzie od 64 do 128 bitów w takich spotykanych obecnie lub dawniej.

FPU w procesorach x86 (czyli x87) ma rejestry 80-bitowe i na takich liczy. Zewnętrznie można oglądać 32-bitowe single (czyli float), 64-bitowe double albo 80-bitowe extended (czyli long double).

SSE liczy zarówno na floatach jak na double

SSE liczy wyłącznie na floatach.
SSE2 liczy na floatach i double'ach.

Jednak obliczenia SSE2 są trochę mniej precyzyjne niż na FPU (nawet ograniczając się do samych double'i) bo FPU liczy w 80 bitach i ewentualnie zaokrągla do 64, a SSE2 liczy na 64 bitach.

kompilator z MS - visual studio ustawia chyba precyzję na double, bo tam nie ma typu long double).

Kompilator Microsoftu nie obsługuje 80-bitowych long double, typ ten oznacza w nim to samo co double.
Ale pod GCC można liczyć na prawdziwych long double, również pod Windows.

0

Jednak obliczenia SSE2 są trochę mniej precyzyjne niż na FPU (nawet ograniczając się do samych double'i) bo FPU liczy w 80 bitach i ewentualnie zaokrągla do 64, a SSE2 liczy na 64 bitach.

Tylko że takie obliczanie jest do kitu, bo możesz otrzymać różne wyniki zależnie od zapisu obliczeń jak i od kompilatora.

Przykładowo gdy obliczamy:
a + bc, wówczas procesor obliczy bc = u, i trzyma to na stosie, więc w long double, potem + a.

Ale może być nieco inna sytuacja:
double u = b*c;

// tu liczymy coś ze zmienną u

return u + a;

i wynik może być już inny, ponieważ u jest już faktycznie tylko double, zamiast long;
zakładając że kompilator użyje tej zmiennej, bo może to stale trzymać w rejestrze FPU (zoptymalizować).

0

Tylko że takie obliczanie jest do kitu, bo możesz otrzymać różne wyniki zależnie od zapisu obliczeń jak i od kompilatora.

Owszem, dlatego projektując algorytmy należy brać pod uwagę błędy jakie moga się pojawiać w kolejnych etapach obliczeń.

0

Sprawdziłem to na long double, i wynik jest jednak poprawny.

   WORD a[5], b[5]; // long double = 10 bytes

   a[4] = 16383+1; a[3] = 0x8000; a[2] = a[1] = a[0] = 0; // 2 x 1.000000 = 2
   b[4] = 16383+0; b[3] = b[2] = b[1] = b[0] = 0xFFFF; // 1 x (1 + 1/2 + 1/4 + ... 1/2^63) = 2 - 1/2^63 = 1.99999999...

   double r = *(long double*)&a - *(long double*)&b;
   sprintf(s, "%Lf - %.28Lf = %e", *(long double*)&a, *(long double*)&b, r);

niemniej ta wartość b jest tu drukowana jak 2.0000000000000000000000000;

faktycznie to jest: b = 2-1/2^63 = 1.999999999999999999891579782751449556599254719913005828857421875;
czyli straszliwie długie... w zapisie dziesiętnym - z 50 cyfr.

2 - (2 - 1/2^63) = 1.08420217248550443400745280086994171142578125 × 10^-19
:)

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