DIGITAL
Die einfachste Repräsentation einer (nichtnegativen) Ganzzahl ist die Binärdarstellung, mit der Sie alle bereits vertraut sein sollten. Das ist auch die Darstellung, die in modernen Prozessoren verwendet wird. Für manche Anwendungen ist es nötig, direkt mit der Binärdarstellung einer Zahl zu arbeiten. In anderen Fällen kann ein Arbeiten mit der Binärdarstellung Ihren Code signifikant beschleunigen. Zum Arbeiten mit der Binärdarstellung bietet C++ die Operatoren ~
, &
, |
, <<
, >>
, ^
, die Sie in der Vorlesung oder im Tutorium kennenlernen werden.
Aufgabenteil A:
Aufgabenteil B:
Im Folgenden wollen wir Operationen auf einzelnen Bits eines uint64_t
implementieren. Wir nummerieren die Bits so, dass das niederwertigste (least significant bit / „rechteste“) Bit den Index 0 bekommt und das höchstwertigste (most significant bit / „linkeste“) den Index 63.
extract
, die eine Zahl \(z\) vom Typ uint64_t
und eine Zahl \(n\) vom Typ int32_t
nimmt und genau dann true
zurückgibt, wenn das n-te Bit in \(z\) gesetzt ist.set
, die eine Zahl \(z\) vom Typ uint64_t
und eine Zahl \(n\) vom Typ int32_t
nimmt und das n-te Bit in \(z\) auf 1 setzt.clear
, die eine Zahl \(z\) vom Typ uint64_t
und eine Zahl \(n\) vom Typ int32_t
nimmt und das n-te Bit in \(z\) auf 0 setzt.0 <= n < 64
sinnvoll. Modifizieren Sie die Funktionen so, dass sie die Stellen rückwärts durchlaufen, wenn -64 <= n < 0
ist (analog zur Listenindizierung in Python).print
, die eine Zahl \(z\) vom Typ uint64_t
nimmt und die Binärdarstellung auf der Konsole ausgibt.upper
, die eine Zahl \(z\) vom Typ uint64_t
nimmt und die Zahl zurückgibt, die durch die oberen ("höherwertigen") 32 Bit von z dargestellt wird.lower
, die eine Zahl \(z\) vom Typ uint64_t
nimmt und die Zahl zurückgibt, die durch die unteren ("niederwertigen") 32 Bit von z dargestellt wird.Aufgabenteil C:
Eine andere Repräsentation einer Ganzzahl ist „Binary Coded Decimal“ (BCD). Hierbei werden vier aufeinanderfolgende Binärstellen einer Zahl als Dezimalstelle interpretiert, die nur die Werte 0 bis 9 annehmen darf. Ein uint64_t
kann auf diese Weise 16 Dezimalstellen darstellen.
uint64_t
darstellen, wenn die Zahl binär kodiert wird?0
, 1337
, 65536
, 5318008
und 3141592653589793
in BCD. Schreiben Sie die Zahlen mithilfe von Hexadezimalzahlliteralen als C++-Ausdruck.uint64_t
mit Zahlen in BCD-Darstellung nimmt und die Summe ebenfalls als BCD-Zahl zurückgibt. Der Übertrag der letzten Stelle darf ignoriert werden (das ist konsistent mit dem Verhalten in C++).Aufgabenteil A:
Aus Python kennen Sie noch den Datentyp int
für Ganzzahlen. int
s in Python können beliebig viele Stellen haben, was das Rechnen in Python sehr intuitiv macht. Maschinennahe Sprachen wie C++ verwenden stattdessen eine beschränkte Repräsentation für numerische Datentypen. Das bewirkt, dass man den Speicheraufwand von numerischen Daten sehr genau vorhersagen kann, hat aber auch zur Folge, dass sich die meisten Zahlen nicht unmittelbar darstellen lassen.
sizeof(T)
kann man herausfinden, wie viele Bytes ein Wert vom Datentyp T belegt. uint64_t
belegt?bool
zu speichern? Wieviele Bits sind es laut sizeof(bool)
?static_cast<T>(x)
verwenden, um den Wert x explizit in einen Wert vom Typ T umzuwandeln. Das funktioniert natürlich nicht immer verlustfrei.Finden Sie für die folgenden Konversionen jeweils eine Zahl, die im Eingabedatentyp problemlos repräsentiert werden kann, aber nicht im Zieldatentyp. Testen Sie mithilfe Ihres Compilers für Ihre gewählte Zahl, welches Ergebnis der Cast liefert. Falls keine solche Zahl existiert, begründen sie kurz, warum alle Zahlen verlustfrei übernommen werden können. Geben Sie außerdem Ihre Compilerversion an.
uint32_t
zu uint64_t
int32_t
zu uint64_t
int32_t
zu uint32_t
int64_t
zu uint32_t
uint32_t
zu int32_t
int32_t
zu double
double
zu int32_t
float
zu int32_t
Aufgabenteil B:
Betrachten Sie die folgende Schleife, die einen einfachen Countdown implementiert:
for (uint32_t i = 10; i >= 0; --i) {
std::cout << i << std::endl;
}
Kompilieren Sie den Code. Verifizieren Sie, dass die Schleife endlos läuft. Erklären Sie, wieso das passiert. Schreiben Sie den Code so um, dass er korrekt funktioniert. Versuchen Sie, eine möglichst nachvollziehbare Lösung zu finden.
Aufgabenteil C:
Auch Fließkommazahlen sind durch beschränkten Speicherplatz eingeschränkt, aber auf eine andere Weise. Die folgende Aufgabe basiert auf der Annahme, dass ihr Compiler Fließkommazahlen nach dem Standard IEEE-754 implementiert, aber alle größeren Compiler tun das eigentlich. Wir wollen jetzt zeigen, dass Fließkommazahlen, die nah an der 0 liegen, mehr Abstufungen zulassen als solche, die weiter von der 0 entfernt sind. Wir können dazu alle verschiedenen Binärdarstellungen von float
s aufzählen und die Differenz zur jeweils nächsten Fließkommazahl betrachen:
int main() {
// ignore this line
static_assert(sizeof(uint32_t) == sizeof(float), "");
uint32_t lower = 0x00000000u;
uint32_t upper = 0xFFFFFFFFu;
// enumerate all 2**32-1 possible states
for(uint32_t i = lower; i < upper; ++i) {
// reinterpret storage location as float
float i_as_float = *reinterpret_cast<float*>(&i);
uint32_t i_plus_one = i + 1;
float i_plus_one_as_float = *reinterpret_cast<float*>(&i_plus_one);
double difference = i_plus_one_as_float - i_as_float;
std::cout << std::setprecision(20) << "Decimal: " << i << " | Float: " << i_as_float << " | Difference to next float: " << difference << std::endl;
}
}
lower
und upper
, bei denen man das oben beschriebene Problem gut sehen kann, d.h. für ein Paar sollen die Abstände zwischen mehreren aufeinanderfolgenden Zahlen möglichst groß und für ein anderes möglichst klein sein. Erklären Sie kurz, wie sie die Werte gefunden haben.float
den Exponenten der Fließkommazahl. Finden Sie ein uint32_t
x, sodass die float
s, die durch x und (x + 1) kodiert werden, unterschiedliche Exponenten haben.