Udostępnij za pośrednictwem


Precyzja i dokładność w obliczeniach zmiennoprzecinkowych

Oryginalny numer KB: 125056

Podsumowanie

Istnieje wiele sytuacji, w których precyzja, zaokrąglanie i dokładność w obliczeniach zmiennoprzecinkowych mogą działać w celu wygenerowania wyników, które są zaskakujące dla programisty. Powinny one być zgodne z czterema ogólnymi regułami:

  1. W obliczeniu obejmującym zarówno pojedynczą, jak i podwójną precyzję wynik zwykle nie będzie bardziej dokładny niż pojedyncza precyzja. Jeśli wymagana jest podwójna precyzja, należy mieć pewność, że wszystkie terminy w obliczeniach, w tym stałe, są określone w podwójnej precyzji.

  2. Nigdy nie zakładaj, że prosta wartość liczbowa jest dokładnie reprezentowana na komputerze. Większość wartości zmiennoprzecinkowych nie może być dokładnie reprezentowana jako skończona wartość binarna. Na przykład .1 jest .0001100110011... w pliku binarnym (powtarza się na zawsze), więc nie może być reprezentowana z pełną dokładnością na komputerze przy użyciu arytmetyki binarnej, która zawiera wszystkie komputery.

  3. Nigdy nie zakładaj, że wynik jest dokładny dla ostatniego miejsca dziesiętnego. Zawsze istnieją małe różnice między odpowiedzią "true" a tym, co można obliczyć z skończoną dokładnością dowolnej jednostki przetwarzania zmiennoprzecinkowych.

  4. Nigdy nie należy porównywać dwóch wartości zmiennoprzecinkowych, aby sprawdzić, czy są równe, czy nie. Jest to następstwo reguły 3. Prawie zawsze będą występować niewielkie różnice między liczbami, które "powinny" być równe. Zamiast tego zawsze sprawdzaj, czy liczby są prawie równe. Innymi słowy, sprawdź, czy różnica między nimi jest mała czy nieistotna.

Więcej informacji

Ogólnie rzecz biorąc, opisane powyżej reguły mają zastosowanie do wszystkich języków, w tym C, C++ i assembler. W poniższych przykładach przedstawiono niektóre reguły przy użyciu programu FORTRAN PowerStation. Wszystkie przykłady zostały skompilowane przy użyciu fortran powerstation 32 bez żadnych opcji, z wyjątkiem ostatniego, który jest napisany w języku C.

Przykład 1

Pierwszy przykład przedstawia dwie rzeczy:

  • Stałe FORTRAN są domyślnie pojedynczą precyzją (stałe C są domyślnie podwójną precyzją).
  • Obliczenia zawierające pojedyncze terminy precyzji nie są dużo dokładniejsze niż obliczenia, w których wszystkie terminy są pojedynczą precyzją.

Po zainicjowaniu z wartością 1.1 (pojedynczą stałą precyzji) y jest tak niedokładna jak pojedyncza zmienna precyzji.

x = 1.100000000000000  y = 1.100000023841858

Wynik pomnożenia pojedynczej wartości precyzji przez dokładną podwójną precyzję jest prawie tak zły, jak pomnożenie dwóch pojedynczych wartości dokładności. Oba obliczenia mają tysiące razy więcej błędów niż pomnożenie dwóch podwójnych wartości dokładności.

true = 1.320000000000000 (multiplying 2 double precision values)
y    = 1.320000052452087 (multiplying a double and a single)
z    = 1.320000081062318 (multiplying 2 single precision values)

Przykładowy kod

C Compile options: none

       real*8 x,y,z
       x = 1.1D0
       y = 1.1
       print *, 'x =',x, 'y =', y
       y = 1.2 * x
       z = 1.2 * 1.1
       print *, x, y, z
       end

Przykład 2

W przykładzie 2 użyto równania kwadratowego. Pokazuje ona, że nawet obliczenia podwójnej precyzji nie są doskonałe i że wynik obliczenia powinien zostać przetestowany, zanim będzie zależał od tego, czy małe błędy mogą mieć drastyczne wyniki. Dane wejściowe do funkcji pierwiastka kwadratowego w przykładzie 2 są tylko nieznacznie ujemne, ale nadal są nieprawidłowe. Jeśli obliczenia podwójnej precyzji nie miały niewielkich błędów, wynik będzie następujący:

Root =   -1.1500000000

Zamiast tego generuje następujący błąd:

błąd czasu wykonywania M6201: MATH

  • sqrt: błąd DOMENY

Przykładowy kod

C Compile options: none

       real*8 a,b,c,x,y
       a=1.0D0
       b=2.3D0
       c=1.322D0
       x = b**2
       y = 4*a*c
       print *,x,y,x-y
       print "(' Root =',F16.10)",(-b+dsqrt(x-y))/(2*a)
       end

Przykład 3

Przykład 3 pokazuje, że z powodu optymalizacji, które występują, nawet jeśli optymalizacja nie jest włączona, wartości mogą tymczasowo zachować większą precyzję niż oczekiwano i nierozsądne jest testowanie dwóch zmiennoprzecinkowych wartości pod kątem równości.

W tym przykładzie dwie wartości są równe i nie równe. W pierwszym if wartość Z jest nadal w stosie koprocesora i ma taką samą precyzję jak Y. W związku z tym X nie jest równe Y i pierwszy komunikat jest drukowany. W czasie drugiego if, Z musiał być załadowany z pamięci i dlatego miał taką samą precyzję i wartość jak X, a drugi komunikat również jest drukowany.

Przykładowy kod

C Compile options: none

       real*8 y
       y=27.1024D0
       x=27.1024
       z=y
       if (x.ne.z) then
         print *,'X does not equal Z'
       end if
       if (x.eq.z) then
         print *,'X equals Z'
       end if
       end

Przykład 4

Pierwsza część przykładowego kodu 4 oblicza najmniejszą możliwą różnicę między dwiema liczbami zbliżonymi do 1,0. Robi to przez dodanie pojedynczego bitu do reprezentacji binarnej 1.0.

x   = 1.00000000000000000  (one bit more than 1.0)
y   = 1.00000000000000000  (exactly 1.0)
x-y =  .00000000000000022  (smallest possible difference)

Niektóre wersje fortranu zaokrąglają liczby podczas ich wyświetlania, dzięki czemu nieodłączna nieprecyzyjność liczbowa nie jest tak oczywista. Dlatego x i y wyglądają tak samo, gdy są wyświetlane.

Druga część przykładowego kodu 4 oblicza najmniejszą możliwą różnicę między dwiema liczbami zbliżonymi do 10,0. Ponownie robi to przez dodanie pojedynczego bitu do reprezentacji binarnej 10.0. Zwróć uwagę, że różnica między liczbami w pobliżu 10 jest większa niż różnica w pobliżu 1. Pokazuje to ogólną zasadę, że im większa wartość bezwzględna liczby, tym mniej dokładnie może być przechowywana w danej liczbie bitów.

x   = 10.00000000000000000  (one bit more than 10.0)
y   = 10.00000000000000000  (exactly 10.0)
x-y =   .00000000000000178

Zostanie również wyświetlona binarna reprezentacja tych liczb, aby pokazać, że różnią się one tylko o 1 bit.

x = 4024000000000001 Hex
y = 4024000000000000 Hex

Ostatnia część przykładowego kodu 4 pokazuje, że proste, nie powtarzające się wartości dziesiętne często mogą być reprezentowane w pliku binarnym tylko przez powtarzający się ułamek. W tym przypadku x=1.05, który wymaga współczynnika powtarzania CCCCCCCC.... (Hex) w mantyssie. W fortece FORTRAN ostatnia cyfra "C" jest zaokrąglana w górę do "D", aby zachować najwyższą możliwą dokładność:

x = 3FF0CCCCCCCCCCCD (Hex representation of 1.05D0)

Nawet po zaokrągleniu wynik nie jest idealnie dokładny. Wystąpił błąd po najmniejszej cyfrze znaczącej, który możemy zobaczyć, usuwając pierwszą cyfrę.

x-1 = .05000000000000004

Przykładowy kod

C Compile options: none

       IMPLICIT real*8 (A-Z)
       integer*4 i(2)
       real*8 x,y
       equivalence (i(1),x)

       x=1.
       y=x
       i(1)=i(1)+1
       print "(1x,'x  =',F20.17,'  y=',f20.17)", x,y
       print "(1x,'x-y=',F20.17)", x-y
       print *

       x=10.
       y=x
       i(1)=i(1)+1
       print "(1x,'x  =',F20.17,'  y=',f20.17)", x,y
       print "(1x,'x-y=',F20.17)", x-y
       print *
       print "(1x,'x  =',Z16,' Hex  y=',Z16,' Hex')", x,y
       print *

       x=1.05D0
       print "(1x,'x  =',F20.17)", x
       print "(1x,'x  =',Z16,' Hex')", x
       x=x-1
       print "(1x,'x-1=',F20.17)", x
       print *
       end

Przykład 5

W języku C stałe zmiennoprzecinkowe są domyślnie podwójne. Użyj "f", aby wskazać wartość zmiennoprzecinkowa, jak w "89.95f".

/* Compile options needed: none
*/ 

#include <stdio.h>

void main()
   {
      float floatvar;
      double doublevar;

/* Print double constant. */ 
      printf("89.95 = %f\n", 89.95);      // 89.95 = 89.950000

/* Printf float constant */ 
      printf("89.95 = %f\n", 89.95F);     // 89.95 = 89.949997

/*** Use double constant. ***/ 
      floatvar = 89.95;
      doublevar = 89.95;

printf("89.95 = %f\n", floatvar);   // 89.95 = 89.949997
      printf("89.95 = %lf\n", doublevar); // 89.95 = 89.950000

/*** Use float constant. ***/ 
      floatvar = 89.95f;
      doublevar = 89.95f;

printf("89.95 = %f\n", floatvar);   // 89.95 = 89.949997
      printf("89.95 = %lf\n", doublevar); // 89.95 = 89.949997
   }