Ć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:
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:
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.