Laboratorium 9: Praca nad zadaniem zaliczeniowym 2. Reprezentacja liczb

Ćwiczenie 1

Napisz program, który zmiennej typu Real nada wartość 0.1, a następnie pomnoży ją przez 21 i odejmie 2. W rezultacie na zmiennej powinno się znów znaleźć 0.1, co sprawdzimy wypisując jej wartość. By uniknąć trudności z interpretacją notacji wykładniczej, użyjemy modyfikatora : 40 : 15 operacji writeln:

program jednadziesiata1;
 
var x : Real;
 
begin
    x := 0.1;
    x := 21 * x - 2;
    writeln(x : 40 : 15)
end.

Po kompilacji i uruchomieniu programu dostaniemy wynik:

                       0.100000000000000

czyli dokładnie to, czego się spodziewaliśmy.

Prosimy o dopisanie do programu jeszcze jednej operacji writeln(), wypisującej wynik porównania wartości zmiennej x z 0.1:

program jednadziesiata2;
 
var x : Real;
 
begin
    x := 0.1;
    x := 21 * x - 2;
    writeln(x = 0.1);
    writeln(x : 40 : 15)
end.

Otrzymamy następujący wynik:

FALSE
                       0.100000000000000

Okazuje się więc, że wynik obliczenia nie jest równy wartości 0.1, choć jest jej tak bliski, że na 15 miejscach po kropce nie widać różnicy.

Zobaczmy, jakie są konsekwencje tej różnicy. Instrukcję aktualizującą wartość zmiennej x umieśćmy w pętli for wykonującej 30 obrotów (writeln() wypisujące wartość logiczną można już usunąć):

program jednadziesiata3;
 
var x : Real;
    i : Integer;
 
begin
    x := 0.1;
    for i := 1 to 30 do
        x := 21 * x - 2;
    writeln(x : 40 : 15)
end.

W wyniku dostaniemy coś, co jednej dziesiątej nie przypomina ani trochę:

 25760784001056300000000.000000000000000

By dokładnie zobaczyć, co się dzieje, przenieśmy writeln() do pętli:

program jednadziesiata4;
 
var x : Real;
    i : Integer;
 
begin
    x := 0.1;
    for i := 1 to 30 do begin
        x := 21 * x - 2;
        writeln(x : 40 : 15)
    end
end.

Dostaniemy wynik:

                       0.100000000000000
                       0.100000000000002
                       0.100000000000051
                       0.100000000001080
                       0.100000000022671
                       0.100000000476098
                       0.100000009998050
                       0.100000209959047
                       0.100004409139979
                       0.100092591939550
                       0.101944430730551
                       0.140833045341563
                       0.957493952172825
                      18.107372995629300
                     378.254832908216000
                    7941.351491072530000
                  166766.381312523000000
                 3502092.007562990000000
                73543930.158822700000000
              1544422531.335280000000000
             32432873156.040800000000000
            681090336274.857000000000000
          14302897061770.000000000000000
         300360838297168.000000000000000
        6307577604240520.000000000000000
      132459129689051000.000000000000000
     2781641723470070000.000000000000000
    58414476192871500000.000000000000000
  1226704000050300000000.000000000000000
 25760784001056300000000.000000000000000

Mamy więc wyjaśnienie zagadki. W każdym kolejnym kroku dostajemy błąd bezwględny 21 razy większy od poprzedniego. W rezultacie już po kilkunastu obrotach pętli na zmiennej x znajdzie się wartość zupełnie bezsensowna.

Ćwiczenie 2

Rozważmy następujący program:

program jedynka;
 
var x : array [1 .. 8] of Real;
    i, j : Integer;
 
begin
    for i := 1 to 8 do
        x[i] := 1.0;
    for j := 1 to 20 do begin
        x[1] := x[1] - 0.1; x[1] := x[1] - 0.8;
        x[2] := x[2] - 0.2; x[2] := x[2] - 0.7;
        x[3] := x[3] - 0.3; x[3] := x[3] - 0.6;
        x[4] := x[4] - 0.4; x[4] := x[4] - 0.5;
        x[5] := x[5] - 0.5; x[5] := x[5] - 0.4;
        x[6] := x[6] - 0.6; x[6] := x[6] - 0.3;
        x[7] := x[7] - 0.7; x[7] := x[7] - 0.2;
        x[8] := x[8] - 0.8; x[8] := x[8] - 0.1;
        for i := 1 to 8 do
            x[i] := 10 * x[i]
    end;
    for i := 1 to 8 do
        writeln(x[i] : 40 : 15)
end.

Łatwo zauważyć, że kolejne obroty "dużej" pętli for powinny pozostawić na każdej z ośmiu pozycji w tablicy x zastaną tam 1.

Uruchamiamy program. Wynikiem będzie:

                    2468.162276944790000
                    4935.324553889580000
                   -4933.324553889580000
                   -2466.162276944790000
                       1.000000000000000
                    2468.162276944790000
                   -1232.581138472400000
                       1.000000000000000

Widać, jak nieprzewidywalne mogą być wyniki obliczeń na danych w reprezentacji zmiennopozycyjnej. W zależności od doboru wartości odejmowanych od elementów tablicy, dostajemy sześć różnych wyników.

Powyższe ćwiczenia pokazują, że specyfika arytmetyki zmiennopozycyjnej ma wpływ na nasze programy nie tylko wtedy, gdy operujemy na wartościach bardzo małych, bardzo dużych, lub wykonujemy złożone obliczenia.

Podkreślamy, że zademonstrowane tu problemy nie są związane z konkretnym językiem programowania, ale z reprezentacją liczb. Po przetłumaczeniu przykładów z Pascala na C++ czy Javę zobaczylibyśmy to samo zjawisko.

Ćwiczenie 3

Posłużymy się programem pomocniczym, demonstrującym reprezentację binarną liczb. Oto treść (reprezentacja.pas):

program reprezentacja;
 
const
    NAJWIEKSZY_ROZMIAR = 256;
    LICZBA_TYPOW = 14;
    NAZWA : array [1 .. LICZBA_TYPOW] of String = (
        'byte',          'cardinal',       'double',          'extended',
        'int64',         'integer',        'longint',         'longword',
        'qword',         'real',           'shortint',        'single',
        'smallint',      'word');
    ROZMIAR : array [1 .. LICZBA_TYPOW] of Integer = (
        SizeOf(Byte),     SizeOf(Cardinal), SizeOf(Double),   SizeOf(Extended),
        SizeOf(Int64),    SizeOf(Integer),  SizeOf(LongInt),  SizeOf(Longword),
        SizeOf(QWord),    SizeOf(Real),     SizeOf(ShortInt), SizeOf(Single),
        SizeOf(SmallInt), SizeOf(Word));
 
var wartosc : record case Integer of
        1  : (byte          : Byte);
        2  : (cardinal      : Cardinal);
        3  : (double        : Double);
        4  : (extended      : Extended);
        5  : (int64         : Int64);
        6  : (integer       : Integer);
        7  : (longint       : LongInt);
        8  : (longword      : LongWord);
        9  : (qword         : QWord);
        10 : (real          : Real);
        11 : (shortint      : ShortInt);
        12 : (single        : Single);
        13 : (smallint      : SmallInt);
        14 : (word          : Word);
        15 : (bajtyWartosci : array [1 .. NAJWIEKSZY_ROZMIAR] of Byte)
    end;
    i, j, l, s, p : Integer;
    x             : Byte;
    argument      : String;
 
begin
    if paramCount <> 1 then
        writeln('Program wywolujemy z jednym argumentem - nazwa typu')
    else begin
        argument := lowerCase(paramStr(1));
        l := 1;
        p := LICZBA_TYPOW;
        while l < p do begin
            s := (l + p) div 2;
            if argument <= NAZWA[s] then
                p := s
            else
                l := s + 1
        end;
        if argument <> NAZWA[l] then
            writeln('Nieprawidlowa nazwa typu: "', paramStr(1), '"')
        else begin
            case l of
                1  : readln(wartosc.byte);
                2  : readln(wartosc.cardinal);
                3  : readln(wartosc.double);
                4  : readln(wartosc.extended);
                5  : readln(wartosc.int64);
                6  : readln(wartosc.integer);
                7  : readln(wartosc.longint);
                8  : readln(wartosc.longword);
                9  : readln(wartosc.qword);
                10 : readln(wartosc.real);
                11 : readln(wartosc.shortint);
                12 : readln(wartosc.single);
                13 : readln(wartosc.smallint);
                14 : readln(wartosc.word);
            end;
            for i := 1 to ROZMIAR[l] do begin
                x := wartosc.bajtyWartosci[i];
                for j := 1 to 8 do begin
                    write(x div 128);
                    x := 2 * (x mod 128)
                end;
                write(' ')
            end;
            writeln
        end
    end
end.

Program ten wywołujemy z jednym argumentem - nazwą wbudowanego typu, a następnie podajemy na standardowym wejściu tekstowy zapis wartości tego typu. Program wypisuje reprezentację tej wartości w postaci ciągu bajtów, z których każdy rozpisany jest na poszczególne bity. Bity są uporządkowane w kolejności od najbardziej znaczących, a bajty w takiej, jaką mają w pamięci.

Program rozpoznaje czternaście typów, w tym dziesięć całkowitych:

  • Byte
  • Cardinal
  • Int64
  • Integer
  • LongInt
  • LongWord
  • QWord
  • ShortInt
  • SmallInt
  • Word

i cztery rzeczywiste:

  • Double
  • Extended
  • Real
  • Single

By dostać się do poszczególnych bajtów reprezentacji wartości liczbowych, program korzysta z rekordu z wariantami bez pola znacznikowego.

Zacznijmy od ustalenia, czy komputer, na którym pracujemy, jest "małym indianinem" czy "dużym indianinem" a więc czy bajty wartości liczbowych są uporządkowane w pamięci w kolejności od najmniej znaczących czy od najbardziej znaczących.

Następnie zbadajmy reprezentację wartości typów Byte, Word i Integer.

Rozpoznanie sposobu reprezentacji wartości typu Real jest trudniejsze. Mamy tu do czynienia z implementacją standardu IEEE 754:

  • wartość typu Real jest zapisana na 64 bitach
  • najbardziej znaczący bit najbardziej znaczącego (czyli ostatniego) bajtu to bit znaku
  • 11 kolejnych bitów zawiera cechę, ale nie w U2, tylko jako liczbę nieujemną powstałą po dodaniu do prawdziwej wartości liczby 1023 - np. cecha o wartości 1 jest reprezentowana przez 1024
  • pozostałe 52 bity to mantysa znormalizowana do przedziału [1, 2), przy czym najbardziej znaczący jej bit, ten przed kropką binarną, jest ukryty.