Systemgenauigkeit, Maschienengenauigkeit, Zufallszahlen
Abspeichern von Zahlen
Prinzipiell werden Zahlen in der Regel im IEEE Standard gespeichert. Das bedeutet, eine Zahl besteht aus 4 Bytes bzw. 2 Words. Ein Byte besitzt 8 Bits und somit stehen zur Speicherung der Zahl 32 Bits zur Verfügung. Eine Zahl mit 32 Stellen hört sich schon mal nicht schlecht an, jedoch muss man bedenken, dass ein Bit nur Nullen und Einsen abspeichert. Das heißt, die größte Zahl, die so dargestellt werden könnte, wäre 10000…(bis 31 Nullen) im Binärsystem, was im Dezimalsystem 2147483648 entspricht – viel zu klein, um damit effektiv arbeiten zu können.
Aus diesem Grund wird im IEEE Standard im ersten Bit das Vorzeichen. Die nächsten 8 Bits sind der Exponent und die nächsten 23 die Mantisse. Da eine Erklärung, wie man eine Zahl in den IEEE Standard umwandelt, den Rahmen sprengen würde, betrachten wir nur den Sinn dahinter:
Dadurch, dass Zahlen nicht “fix”, sondern durch Abkürzungen einen Exponenten und die Berechnung des Exponenten mit einem BIAS gespeichert werden, ist es möglich, deutlich größere Zahlen zu speichern, je nachdem ob man sich auf einem 32 Bit oder 64 Bit System befindet, gibt es entsprechend noch einmal Unterschiede.
Maschienengenauigkeit
Wenn der Computer anfängt zu rechnen, werden jedoch immer Ungenauigkeiten auftreten, da er zwischen Zahlensystemen wechselt und dabei Informationen verloren gehen. Dabei redet man dann von der so genannten Maschienengenauigkeit. Diese hängt davon ab, ob es sich um ein 32-Bit oder 64-Bit System handelt und was für Datentypen – also unter anderem auch wie viel Byte man ihnen zuweist – beim Programmieren verwendet werden.
Führt man zum Beispiel folgendes C++ Programm aus:
#include <iostream>
int main ( )
{
float l_val = 10.f;
for ( int i = 0; i < 1000; i++ )
l_val += 0.1f;
std::cout << l_val;
return 0;
}
erhält man als Ausgabe 109,99 statt 110.
Bei größeren Zahlen würde die Ungenauigkeit steigen, besonders bei medizinischen oder kaufmännischen Anwendungen kann das fatale Folgen haben. Im Ernstfall kostet das ein Menschenleben oder fügt Millionenschäden zu. In dem Beispiel von oben wurde float als Datentyp genutzt. Benutzt man hingegen double, erhält man die korrekte Ausgabe von 110, da double mit deutlichem Abstand genauer als float ist, jedoch wird früher oder später auch double an seine Grenzen stoßen. Zu diesem Zweck gibt es je nach Programmiersprache “arbitrary precision” Bibliotheken, die es erlauben, fast unbegrenzt lange Zahlen zu definieren und mit ihnen zu rechnen. In Java kann man zum Beispiel aus den Standardlibraries Biginteger oder Bigdecimal einbinden.
Zufallszahlen
Ein Computer kann unmöglich einen echten Zufall simulieren – wie sollte er auch? Nichts im Computer ist zufällig, jeder Bit und Byte ist geplant und ausgeführt, höchstens menschliches Versagen kann unerwartete Fehler hervorrufen. Wie funktionieren also zum Beispiel Glücksspiel Programme, Verlosungen oder ähnliches?
Bei solchen Anwendungen geht es ja meist um bares Geld. Die Zahlen, die für Gewinn oder Verlust entscheidend sind, werden vom System generiert, können aber nicht wirklich zufällig sein. Wie vermeidet man also, dass solchen Zahlen vorhersehbar sind und jedes Online-Casino demnächst pleite geht?
Zu diesem Zweck gibt es diverse Algorithmen, die aufgrund von Daten, die zur Verfügung stehen, zum Beispiel das Betriebssystem o.ä., versuchen, eine Zufallssituation so realistisch wie möglich zu programmieren. Eine der effektivsten Engines in C++ für so genannte “Pseudozufallszahlen” ist mt19937.
Erstellen wir einfach mal einen Vergleich zu einer anderen Zufallsengine:
#include <iostream>
#include <algorithm>
#include <random>
#include <chrono>
#include <functional>
int main ( )
{
int all = 0;
for ( int b = 0; b < 100; b++ )
{
int i = rand ( ) % 100 + 1;
all += i;
}
std::cout << std::endl << all / 100 << std::endl;
system ( "PAUSE" );
return 0;
}
Dies gibt auf meinem System in 20/20 Versuchen als Schnitt der 100 Zahlen aus, das heißt, da steckt definitiv ein sehr kurz gefasstes System hinter und irgendetwas kann nicht stimmen, da sich die Zufallszahlen verändern müssten bei einer Stichprobe von nur 100.
Testen wir das gleiche mit einer deutlich genaueren Engine:
#include <iostream>
#include <algorithm>
#include <random>
#include <chrono>
#include <functional>
int main ( )
{
int all = 0;
for ( int b = 0; b < 100; b++ )
{
auto seed = std::chrono::high_resolution_clock::now ( ).time_since_epoch ( ).count ( );
std::mt19937 mt ( seed );
std::uniform_int_distribution<int> dist ( 1, 100 );
int i = dist ( mt );
all += i;
}
std::cout << std::endl << all / 100 << std::endl;
system ( "PAUSE" );
return 0;
}
Auch hier generieren wir eine Stichprobe von n=100 und erhalten als Ergebnisse:
- 32
- 64
- 33
- 14
- • 45
- • 54
- • 63
Der Schnitt ist also sehr zufällig, wie es auch sein sollte. Nun testen wir das ganze einmal mit einer Stichprobe von n = 1000000.
Bei echten Zufallszahlen würde sich das Ergebnis nun annähern, d.h. 1 müsste 10000 mal vertreten sein, genau so wie 2, 3 und so weiter, da jede Zahl mit einer 1% Chance auftritt. Des Weiteren müsste der Schnitt der Zahlen sich 50 stark annähern.
Folgende Ergebnisse traten auf:
- 50
- 50
- 50
- 50
- 50
Das Resultat sollte eindeutig sein. Zum Vergleich verringern wir die Stichmenge noch einmal auf 100000. Dadurch ist der Schnitt wie man anhand der folgenden Ergebnisse sehen kann nicht mehr exakt 50, was auch nachvollziehbar sein sollte.
- 51
- 47
- 49
- 50
- 50
- 52
Rückmeldungen