...die vielleicht nicht ganz uninteressant sind.
Also Klausur, die findet statt am 2.
September, 14 Uhr.
Die Dauer ist je nachdem, ob Sie jetzt Informatiker sind, also
Informatiker zwei Stunden, also von 14 Uhr bis 16 Uhr für die
Informationswerte.
Das findet auch in getrennten Hörsälen statt, weil dadurch die
Informationswerte früher fertig sind, die Rennerei können wir nicht
brauchen.
Also werden die Informationswerte in anderen Hörsälen die Prüfung
haben, wie die Informatiker.
Anmeldung erfolgt einfach durch Einwurf der Zulassungsbescheinigung in
den Briefkasten, den Sie sich erkennen, im Informatikgebäude, in den
Fassadenkarten, also dort, wo Sie auch die Übungsblätter hoffentlich
einwerfen.
Dort können Sie sich auch anmelden, indem Sie bis zum 28.
August, also relativ kurz vorher, Ihre Zulassungsbescheinigung dort
einwerfen.
Hilfsmittel zur Klausur sind keiner erlaubt.
Natürlich müßig zu sagen, dass Sie natürlich den Studentenausweis mit
in die Klausur bringen müssen.
Auf der Homepage werden wir die Hörsaalverteilung rechtzeitig bekannt
geben.
Soviel zur Verwaltung am Anfang.
Wir haben das letzte Mal begonnen, uns mit Speicherregimen im
Speziellen und erstmal mit Cashspeicher im Allgemeinen und mit
Cashspeicher im Speziellen zu beschaffen.
Ich möchte hier, um das nochmal ein bisschen zu motivieren und auch zu
wiederholen nochmal, ein paar weitere Informationen hinzufügen, die
jetzt belegen sollen, warum eigentlich der gerade interessantere
Speicherregime und insbesondere Cashspeicher eben so wichtig sind.
Wenn man sich mal die Technologietrends anguckt, für so verschiedene
Komponenten wie Logik, dynamische Speicher oder Festplatten, dann
sieht man hier, dass die Kapazität bei der Logik sich in drei Jahren
etwa verdoppelt, die Geschwindigkeit auch in drei Jahren verdoppelt.
Bei den dynamischen Speicherbausteinen sieht es hingegen so aus, die
Kapazität vervierfacht sich in drei Jahren, die Geschwindigkeit
verdoppelt sich aber nur alle zehn Jahre etwa.
Also hier ist doch eine gewaltige Schere.
Bei der Festplatte sieht es relativ ähnlich aus, also die Kapazität
vervierfacht sich etwa alle drei Jahre, während die Geschwindigkeit
sich auch nur alle zehn Jahre verdoppelt.
Wenn man speziell mal die dynamischen Speicher, die wir jetzt
betrachtet haben, nochmal anguckt über die Jahre, dann haben wir hier
so eine kleine Aufstellung, die dankenswerterweise der Herr Haas
vorher zusammengestellt hat, um das nochmal deutlicher zu machen.
Also wenn wir mal ins Jahr 1980 gehen, hatten wir eine Kapazität von
64 Kilobyte bei den dynamischen Speicherbausteinen, pro Baustein eine
Zykluszeit von 250 Nanosekunden.
Heute sind wir bei einem Gigabyte, aber so arg viel schneller sind wir
nicht geworden, also 80 Nanosekunden.
Wenn man das jetzt mal betrachtet, einfach mal einen Sprung hier nimmt
von 15 Jahren, nämlich von 1980 bis 1995, dann hat sich die Kapazität
vertausendfacht in diesen 15 Jahren.
Wenn man mal die Zykluszeit betrachtet, dann hat sie sich aber sogar
hier nur verdoppelt etwa.
Das heißt also, die dynamischen Speicherbausteine kommen in ihrer
Geschwindigkeitsentwicklung deutlich nicht mit der Logik oder den
Prozessoren mit, wie uns nochmal diese Grafik hier zeigt, die so ein
bisschen die Schere zwischen Prozessoren und Speicherperformance
zeigt.
Also das ist das Moorische Gesetz, dass wir eine Leistungsverdopplung
etwa alle 1,5 Jahre haben für die Leistungsentwicklung der Prozessoren
von 1980 bis 2000 in Faktoren, also 1980 wurde das 1 hier eskaliert,
dann hatten wir eben hier etwa im Jahr 1988 89 verzehnfachung, hier
95, 96 verhundertfachung, bei 2000 sind wir etwa bei der
Vertausendfachung, während die Speicher eben mit der Entwicklung
Geschwindigkeitsverdopplung alle 10 Jahre etwa dieser Kurve folgen.
Und Sie sehen diese Schere, dieser Abstand zwischen der
Geschwindigkeit von Prozessoren und der Geschwindigkeit von Speichern,
die wird immer größer.
Wenn man die mal noch evaluiert, dann sehen Sie, dass dieser
Performanceunterschied etwa um 50 Prozent pro Jahr wächst.
Das heißt, jedes Jahr kommen hier weitere 50 Prozent
Performanceunterschied dazu.
Und das ist eben einer der wesentlichen Triebfedern, warum eben so
viel Arbeit in Speicherhierarchien im Allgemeinen und Cache-Speicher
im Speziellen gesteckt werden.
Das ist einfach notwendig, weil ansonsten haben wir das Problem, wir
haben zwar wunderbar schnelle Prozessoren, nur bekommen die ihre Daten
nicht, weil eben die Speicher so langsam sind.
Deswegen eben Speicherhierarchie.
Das Ganze, was ich eben gezeigt habe, gilt ja für dynamische
Speicherbausteine.
Es ist ja nicht so, dass wir nicht Speicherbausteine hätten, die
schnell genug wären, um mit den Prozessoren mitzukommen.
Das sind statische Speicherbausteine.
Die haben aber den Nachteil, dass sie relativ viel Fläche benötigen,
dass sie relativ viel Strom benötigen und relativ teuer sind.
Das heißt, um es nochmal zum letzten Mal zu wiederholen, die Folie
hatten wir das letzte Mal aufgelegt, also ein einheitlicher Speicher
mit so kurzer Zugriffszeit, dass er mit einem Prozessor mitkommt, aber
auch eine große ausreichende Kapazität hat.
Die ist aus kurzen Gründen nicht möglich.
Deswegen müssen wir eben unsere verschiedenen Technologien, die wir
haben, geschickt schichten, um eben die großen langsamen wie auch die
kleinen schnellen Speicher einsetzen zu können und das Ganze so
geschickt schichten, dass es nach außen im Prinzip aussieht, als
hätten wir einen homogenen großen schnellen Speicher.
Das ist letztendlich die Grundidee, die hinter der Speicherhierarchie
steht.
Und hier haben wir eben nochmal die zwei Strategien.
Das eine, was wir jetzt gerade betrachten, der Cache-Speicher und das
andere, was dann in der nächsten Vorlesung begonnen wird, der
virtuelle Speicher.
Wie gesagt, die Idee, wir wollen versuchen, dass obwohl wir eben
keinen einheitlichen großen Speicher haben, der schnell ist, wollen
wir doch, dass es so aussieht, als hätten wir einen.
Das Ganze kann man im Prinzip dadurch erreichen, wenn bestimmte
Voraussetzungen erfüllt sind, nämlich wenn man eine gewisse Lokalität
im Programmverhalten hat.
Auf den Begriff Lokalität komme ich später noch ein bisschen genauer
zurück, was es bedeutet, wenn ich jetzt verschiedene Schichten habe,
dass ich rechtzeitig auch in der geeigneten Zeit in diesen Schichten
umlagere und dass ich eben diese Inhomogenität für den Benutzer
unsichtbar mache.
Hier also eben nochmal diese Hierarchie.
Auch das Bild hatten wir das letzte Mal in der Vorlesung schon.
Mit eben hier ganz unten die sehr großen Speicher, sehr langsam, mit
zunehmender Geschwindigkeit hier, abnehmender Kapazität und
zunehmenden Kosten.
Gut, und wir wollen uns jetzt mal auf der Ebene hier bewegen, das ist
die Ebene, die uns jetzt interessiert.
Das heißt, wir wollen uns mit Cache-Speichern auseinandersetzen.
Formulieren wir also nochmal das Kernproblem, was wir in der vorigen
Grafik eben gesehen haben, aus dieser Schere.
Das Problem ist, dass eben die Zykluszeit moderner Mikroprozessoren
erheblich kürzer ist, als die Zykluszeit preiswerter dynamischer
Speicherbausteine.
Da haben wir also zunächst mal zwei Möglichkeiten.
Also entweder wir nehmen wirklich die dynamischen Speicherbausteine,
dann müssen wir aber Wartezyklen einfügen und zwar nicht zu wenige,
also nicht nur einen Wartezyklus, sondern zwei Wartezyklus, sondern
wir reden hier im Bereich von 10 bis 100 Wartezyklen, die wir einfügen
müssen pro Speicherzugriff.
Oder wir benutzen eben keine dynamischen Speicherbausteine, wir
benutzen statische Speicherbausteine.
Die können wir schnell genug realisieren, dass der Prozessor ohne
Wartezyklen darauf zugreifen kann.
Allerdings sind die teuer genug, dass wir im Prinzip nur kleine
Speicher so realisieren können.
Die Lösung hier, um dieses Problem zu lösen, ist eben der sogenannte
Cache -Speicher.
Das heißt, wir setzen einfach jetzt zwischen den Prozessoren, ich habe
nachher noch einmal ein genaueres Bild, nur so als kleine Skizze, aber
aus der jetzigen Orientierung schon, wir haben also hier unseren
Prozessor und wir haben hier unseren großen, langsamen
Arbeitsspeicher.
Und im Prinzip setzen wir da dazwischen unseren Cache-Speicher.
Das ist also ein kleiner Speicher, der jetzt aber aus sehr schnellen
statischen Speicherbausteinen aufgebaut ist.
Das heißt, wir haben also unseren langsamen Arbeitsspeicher aus
dynamischen Speicherbausteinen, unseren schnellen Cache-Speicher aus
statischen Speicherbausteinen und unseren Prozessor.
Das heißt, allgemein unter dem Begriff Cache-Speicher versteht man
immer einen kleinen, schnellen Puffer-Speicher, der von einem
langsamen, größeren Speicher geschaltet wird, um die Zugriffszeit zu
verbessern.
Das muss nicht nur hier der Fall sein, also es gibt mehrere Typen von
Caches, es gibt ganz verschiedene Arten von Caches.
Zum Beispiel eben, was ich eben schon angedeutet habe, was wir jetzt
hier betrachten wollen, was unser Schwerpunkt ist, indem ich eben den
langsamen Hauptspeicher, die Zugriffszeit des langsamen Hauptspeichers
verbessere, indem ich eben so einen Cache davor schalte.
In dem Fall spricht man von einem sogenannten CPU-Cache oder auch
Befehls -Cache oder Daten-Cache, je nachdem, was ich genau da drin
ablege.
Es ist nicht die einzige Stelle, an der ich Cache-Speicher einsetzen
kann.
Cache -Speicher sind immer dann interessant, wenn ich eben sehr große
Geschwindigkeitsunterschiede habe.
Na gut, einen weiteren großen Geschwindigkeitsunterschied habe ich bei
Plattenspeicher, also wenn ich auf die Festplatte gehe, dann habe ich
so Zugriffszeiten im Bereich von 10 Millisekunden.
Und hier wäre es natürlich auch sinnvoll, einen schnellen Speicher
davor zu schalten, wobei wir hier nämlich von anderen Größenordnungen
sprechen.
Die Verhältnisse sind etwa gleich, aber die Größenordnungen sind
natürlich andere.
Bei einem CPU-Cache haben wir vielleicht einen Arbeitsspeicher von,
sagen wir mal, 1 Gigabyte maximal, so in der Größenordnung.
Und wir haben dann einen Cache-Speicher in der Größenordnung
vielleicht von einigen 100 Kilobyte bis 1 Megabyte, um mal so ein paar
Größenzahlen zu nennen.
Bei einem Platten-Cache hätten wir eben einen Plattenspeicher in der
Größe vielleicht von 100 Gigabyte und entsprechend einen Cache
-Speicher von in der Größenordnung ein halbes Gigabyte.
Das heißt, bei einem Platten-Cache würde man natürlich als Cache
-Speicher einen dynamischen Speicher einsetzen, der zwar für den
Prozessor im Vergleich zur Prozessorgeschwindigkeit natürlich sehr
langsam ist, aber im Vergleich zur Festplattengeschwindigkeit wiederum
extrem schnell.
Insofern kann ich jetzt hier für den Prozessor eigentlich einen
langsamen Speicher, durch das Cache-Speicher für die Platte einsetzen.
Aber das interessiert uns hier an der Stelle eigentlich eher weniger.
Wir wollen uns eigentlich hier im Wesentlichen auf diesen ersten Fall
konzentrieren.
Das heißt also, wie kann ich jetzt mit einem schnellen kleinen
Speicher, einem Cache-Speicher, die Zukunftszeit auf meinen
Arbeitsspeicher, der aus dynamischen langsamen Speicherbausteinen
besteht, verbessern.
Das ist die Fragestellung, mit der wir uns hier jetzt
auseinandersetzen wollen.
Gut, wir haben es hier jetzt schon definiert.
Wie sieht es jetzt ein bisschen genauer aus?
Ich habe hier ein Bild.
So kurz kommt, da kommt es auch schon.
Das heißt, wir haben hier einen sogenannten CPU-Cache-Speicher, der
ist zwischen dem Mikroprozessor-CPU und dem normalen Arbeitsspeicher
angesiedelt.
Ja gut, wir haben hier, betrachten wir mal die Verbindung, wir haben
den Mikroprozessor, der den normalen Arbeitsspeicher über den
Adressbus adressiert, der mit dem normalen Arbeitsspeicher Daten
austauscht.
Das wäre der normale Transfer, das heißt, wir legen hier eine Adresse
an und bekommen hier ein Datum zurück.
Da dazwischen legen wir jetzt den Cache-Speicher, indem wir jetzt auf
geschickte Art und Weise, wie wir das noch tun, müssen wir noch
betrachten, auf geschickte Art und Weise irgendwelche Teile dieses
Speichers hier einlagern, sodass dieser Mikroprozessor im Wesentlichen
nicht aus dem langsamen Speicher zugreifen muss, sondern nach
Möglichkeit eben aus diesem schnellen kleinen Cache-Speicher.
Dazu brauchen wir nämlich eine Steuerung.
Das ist ganz klar, wir müssen nach irgendwelchen Strategien dafür
sorgen, dass jetzt Inhalte von hier nach hier hinkommen, und zwar nach
der Strategie, die sinnvoll ist, also nach einer Art und Weise, die es
uns jetzt erlaubt, mit möglichst hoher Wahrscheinlichkeit hier uns aus
dem schnellen Cache bedienen zu können.
Das heißt, wir brauchen hier eine Steuerung.
Und diese Steuerung muss letztendlich dafür sorgen, dass hier Daten
zwischen dem Cache und dem normalen Speicher ein- und ausgelagert
werden.
Ich denke, das Einlagern ist klar.
Ich muss hier Daten hineinbringen, auslagern muss ich leider auch,
weil eben dieser Cache-Speicher nicht klein ist.
Das heißt, es werden immer wieder Fälle auftreten, wo ich hier
Elemente überschreiben muss.
Das heißt, wo ich dann hier Elemente aus dem Cache-Speicher wieder
entfernen muss und hier wieder den normalen Speicher auslagern.
Was ich erzielen möchte im Prinzip, ist, dass mein Prozessor hier im
Wesentlichen diesem Pfeil folgt.
Das heißt, seine Verarbeitung im Wesentlichen aus diesem Cache
durchführen kann und nach Möglichkeit nie oder ganz selten nur auf
diesen äußeren Speicher zugreifen können muss.
Das heißt, was wir hier haben wollen, ist eben ein schneller Puffer
-Speicher, in dem ich Kopien derjenigen Teile des Arbeitsspeichers
bereithalte, die mit größtmöglicher Wahrscheinlichkeit als nächstes
benutzt werden.
Das ist letztendlich die Zielstellung, die ich erreichen möchte.
Gut.
Ich möchte natürlich so schnell wie möglich zugreifen können.
Das bedeutet, dieser Cache-Speicher muss im Prinzip so schnell sein,
dass der Prozessor praktisch ohne Wartezyklen zugreifen kann.
Also praktisch so schnell wie auf die internen Register.
Hierfür gibt es zwei Möglichkeiten.
Die idealste Möglichkeit ist es natürlich, diesen Cache-Speicher
direkt mit auf den Prozessor-Chip zu integrieren.
Das ist die Technik, die auch heute verfolgt wird.
Damit habe ich die kürzesten Datenfahrten, kann eben die schnellsten
Technologien benutzen.
Ich bin eben sehr nah, kann also mit sehr hohen Taktfrequenzen
arbeiten.
Wenn ich das nicht habe, dann muss ich zumindest nach Möglichkeit
schnellste statische Speichertechnologie benutzen, die zur Verfügung
steht für einen Off-Chip-Cache.
Wobei doch deutlich die Geschwindigkeitsunterschiede zwischen On- und
Off -Chip-Cache durchaus in der Größenordnung Faktor 5 bis 10 etwa
liegen.
Also ein On-Chip-Cache ist ja um Faktor 5 bis Faktor 10 schneller als
ein Off-Chip-Cache.
Gut, Aufgabe der Steuerung, der Cache-Verwaltung ist es eben jetzt
dann dafür zu sorgen, dass nach Möglichkeit die am häufigsten
benötigten Daten sich im Cache befinden, sodass eben die CPU mit
möglichst großer Wahrscheinlichkeit das nächste Datum aus dem Cache
holen kann und nicht auf den langsamen Arbeitsspeicher warten muss.
Dazu haben wir eben eine Steuerung, die war vorhin schon mal in dem
Bild eingezeichnet.
Diese Steuerung hier.
Und da das Ganze nicht sehr schnell gehen muss, ist es klar, dass
diese Steuerung eine Hardware sein wird.
Das wird also kein Programm oder irgendwas sein, das wird eine
Hardware sein.
Das heißt, dieser Cache beinhaltet eine Steuerung, eine Hardware
-Steuerung, auch Cache-Controller genannt, die eben jetzt dafür sorgt,
dass genau die Daten nach Möglichkeit in den Cache kopiert werden, die
der Prozessor als nächstes benötigt.
Das ist letztendlich die Aufgabe.
Eine weitere Aufgabe ist es natürlich, andere Daten zu verdrängen.
Der Cache ist klein.
Wir haben da so einen ständigen Verdrängungswettbewerb im Cache.
Wenn ich ein neues Datum in den Cache einbringe, muss ich in der Regel
eins dafür rausschmeißen.
Auch das ist Aufgabe der Hardware-Steuerung, hier eine Strategie
bereitzustellen, um diesen Vorgang auch möglichst schnell zu
erledigen.
Man muss sich immer mit dem Hinterkopf hier behalten, dieses Ganze
muss schnell gehen, weil wenn die Cache-Steuerung langsam ist, dann
lohnt der Cache nicht.
Der Cache soll ja eben den Arbeitsspeicher, die Wartezeit vom
Arbeitsspeicher überbrücken.
Die Wartezeit auf den Arbeitsspeicher beträgt vielleicht 50
Nanosekunden.
Das heißt, diese Steuerung muss in der Lage sein, innerhalb von sehr
wenigen Nanosekunden mir mein Ergebnis zu liefern, weil sonst kann ich
auch gleich auf den Arbeitsspeicher warten.
Also hier wird eine relativ hohe Anforderung an diese Steuerung
gestellt.
Gut, warum funktioniert eigentlich ein Cache überhaupt?
Wieso kann ich mit einem relativ kleinen Speicher doch dafür sorgen,
dass ich mit hoher Wahrscheinlichkeit auf diesen kleinen Speicher
zugreifen kann und nicht einen großen Speicher benutzen muss?
Wir erreichen bei heutigen Caches, wenn man das mal untersucht,
Trefferquoten, die liegen in der Größenordnung von 80% und mehr.
Das heißt, in wirklich 80% der Fälle und mehr funktioniert das Ganze.
Das heißt, der Prozessor muss nicht auf den langsamen Arbeitsspeicher
zugreifen, sondern kann auf den schnellen Cache-Speicher zugreifen.
Das Schlüsselwort, das diesen Erfolg ermöglicht, ist die Lokalität
oder die Lokalitätseigenschaft von Programmen.
Lokalität heißt, dass ein Programm sich bevorzugt an einer bestimmten
Stelle bewegt oder mit größerer Häufigkeit an einer bestimmten Stelle
als an einer anderen.
Wenn Sie ein Programm hätten, bei dem alle Stellen des Programms mit
gleicher Wahrscheinlichkeit absolut gleich verteilt aufgerufen werden
würden, dann würde Ihnen ein Cache überhaupt nichts nützen.
Hätte überhaupt keinen Effekt.
Ein Cache funktioniert nur dadurch, dass wenn Sie jetzt mal Ihr
Programm betrachten, von Anfang bis zum Ende, dass Sie in der
Zugriffshäufigkeit im Prinzip irgendwelche Berge haben.
Das heißt, bestimmte Teile des Programms werden sehr viel häufiger
zugegriffen als andere Teile des Programms.
Und nur wenn man solche Lokalitäten hat, nur dann funktioniert ein
Cache, weil nämlich dann der Cache eben genau diese Teile einlagern
kann, während diese selten benötigten Teile ausgelagert werden.
Wenn diese Funktion hier eine Gleichverteilung wäre, würde wie gesagt
der Cache überhaupt gar nichts bringen.
Gut, glücklicherweise haben Waisenprogramme eine relativ hohe
Lokalität auf.
Und zwar kann ich hier nochmal unterscheiden in eine sogenannte
zeitliche und örtliche Lokalität.
Zeitliche Lokalität heißt hier aber einfach, dass die Information, die
ich schon früher einmal angesprochen habe, mit hoher
Wahrscheinlichkeit ich auch in Zukunft wieder benötige.
Das passiert eben sehr häufig im Schleifen.
Wenn ich im Schleifen durchlaufe oder in Tabellen verarbeite, dann
brauche ich sehr häufig immer wieder mal dieselbe Information.
Gerade beim Schleichendurchlauf greife ich zum Beispiel immer wieder
auf dieselben Befehle zu.
Gut, und dann die örtliche Lokalität, die einfach sagt, dass ein
zukunftsreicher Zugriff mit großer Wahrscheinlichkeit in der Nähe des
bisherigen Zugriffs liegt.
Geht einfach daraus hervor, dass ein Programm normalerweise relativ
sequenziell abläuft.
Sprungbefehle sind nicht so häufig.
So normale Programme haben eben im Durchschnitt Untersuchungen, die
sagen, jeder siebte bis achte Befehl im normalen Programm ist ein
Sprungbefehl.
Das heißt, ich habe immer sechs aufeinanderfolgende Befehle, bevor der
erste Sprungbefehl kommt.
Hieraus ergibt sich eine gewisse örtliche Lokalität.
Also die Vermutung, dass wenn ich eine bestimmte Instruktion ausführe,
dass ich die nachfolgende Adresse dann ebenfalls benötige, ist sehr
zutreffend.
Die trifft also etwa in 60 Prozent der Fälle grob gesagt zu.
Das heißt also, diese beiden Eigenschaften, zeitliche Lokalität und
örtliche Lokalität, bewirken überhaupt erstmal, dass ein Cache
funktioniert.
Ausgrund von diesen Eigenschaften kann ich eben hier bestimmte Teile
meines Programms bevorzugt in den schnellen Speicher einladern, wenn
ich große Teile hier überhaupt nicht brauche.
Ich meine, man kann es auch plausibel machen.
Jeder, der schon mal angefangen hat, ein größeres Programm zu
entwickeln, dem ist vielleicht aufgefallen, dass so Sachen wie
Fehlerbehandlungen in einem modernen heutigen Programm unter Umständen
60 bis 70 Prozent des Programmcodes ausmachen.
Eigentlich nur 30 Prozent des Programmcodes auf die eigentliche
Funktion des Programms bestimmt sind.
Aber diese Fehlerbehandlung, die wird eben nur dann aufgerufen, wenn
ein Fehler auftritt.
Das heißt, wenn Sie als Benutzer das Programm richtig bearbeiten, oder
wenn es ein Steuerprogramm irgendwo ist, wenn die Umgebung korrekt
funktioniert, dann werden diese Teile sogar nie aufgerufen.
Das ist also hier eine starke Lokalitätseigenschaft, dass der
eigentliche Funktionscode sehr viel häufiger aufgerufen wird als der
Fehlerbehandlungscode.
Gut, schauen wir uns jetzt mal die Funktionsweise eines Caches ein
bisschen genauer an.
Hier müssen wir jetzt unterscheiden, haben wir Lese- oder haben wir
Schreibzugriffe.
Da gibt es ganz unterschiedliche Arten, wie der Cache funktioniert.
Die Lesezugriffe sind etwas einfacher zu handhaben, deswegen fangen
wir damit an.
Ich meine, die Vorgehensweise ist relativ logisch.
Wenn der Prozessor ein Datum holen möchte, dann geht der, wenn ich
einen Cache habe, zunächst her und prüft, habe ich das Datum im Cache
oder habe ich es nicht im Cache.
Das ist logisch und offensichtlich.
Wenn ja, wenn ich das Datum im Cache habe, dann habe ich etwas Grün
eingezeichnet, ist ein Treffer oder auch auf Neudeutsch ein Cache-Hit.
Das heißt, ich kann das Datum dem Cache entnehmen und ich muss nicht
warten.
Wenn nein, wenn also mein Datum nicht im Cache steht, dann habe ich
einen sogenannten Cache-Miss.
Das heißt, mein Datum steht nicht im Cache.
Ich muss auf jeden Fall auf den Arbeitsspeicher zugreifen und dann ist
es aber sinnvoll, eben aus Gründen der Lokalität, wenn ich dieses
Datum, auf das ich jetzt zugreife, auch gleichzeitig in den Cache
kopiere.
Das heißt, im Fall eines Cache-Misses gehe ich her, füge meine
Wartezyklen ein, lese mein Datum aus dem Arbeitsspeicher und kopiere
es gleichzeitig in den Cache.
Einfach in der Hoffnung, dass ich dieses Datum, das ich hier eben
brauche, vielleicht gerade in der Zukunft nochmal brauche.
Und diese Hoffnung ist nicht unberechtigt.
Das heißt, hier ist es ganz normal als Bild dargestellt.
Im Fall eines Cache-Hits kann ich eben direkt meine Adresse an den
Cache legen, bekomme mein Datum.
Im Fall eines Cache-Misses lege ich meine Adresse hier an den
Arbeitsspeicher, füge meine Wartezyklen ein, greife auf mein Datum zu
und lege es aber gleichzeitig auch hier in den Cache, sodass es für
nächste Zugriffe eben zur Verfügung steht.
Unter Umständen führt dieses Einlagern hier jetzt zu einem Verdrängen.
Ich muss es ja irgendwo in den Cache einlagern.
Das heißt, es kann durchaus sein, dass ich hier ein Gegenstand
verdrängen muss.
Das heißt, weiterhin wird damit unter Umständen eine Verdrängung
verbunden sein, die auch Aufgabe der Cache-Steuerung ist.
Aber darauf kommen wir später noch detaillierter.
Hier jetzt zunächst nochmal ein paar Begriffe, relativ einfach
eigentlich zu erklären.
Zunächst mal eine Trefferrate, von der ich schon gesprochen habe, oder
eine Hitrate, die ist sehr einfach definiert, indem ich einfach die
Anzahl der Treffer durch die Anzahl der Gesamtzugriffe dividiere.
Und dann bekomme ich eben Trefferraten entweder in Prozent angegeben,
80 % würde bedeuten eben 8 Treffer auf 10 Zugriffe im Schnitt, oder
eben als Gleitkommazahl oder als rationale Zahl 0,8, wie Sie wollen.
Ich kann jetzt hergehen und zumindest annäherungsweise mit dieser
Trefferrate, eine mittlere Zugriffszeit berechnen, die ich an dem
Gesamtsystem habe.
Also ich habe hier mein System, Cache-Speicher plus Arbeitsspeicher.
Wenn ich das Ganze als Gesamtsystem sehe, kann ich hier eine mittlere
Zugriffszeit angeben, und zwar nach der unteren Formel, die sich
relativ einfach berechnet.
Ich nehme zum Einfachen einmal meine Trefferquote, meine Hitrate, und
multipliziere die mit der Zugriffszeit des Caches, mit der schnellen
Zugriffszeit des Caches.
Dann nehme ich meine Missrate oder meine Fehlerrate, die sich einfach
durch 1 minus der Hitrate berechnet, logischerweise, multipliziere die
mit der Zugriffszeit meines langsamen Arbeitsspeichers und addiere
beide zusammen, und im Ergebnis bekomme ich relativ gut die mittlere
Zugriffszeit meines Gesamtsystems.
Das heißt also Zugriffsrate mal Zugriffszeit im Falle eines Cache
-Treffers plus Missrate mal Zugriffszeit im Falle eines
Arbeitsspeicherzugriffs.
Das Ganze summiert ergibt eben etwa meine mittlere Zugriffszeit.
Und es ist relativ offensichtlich, wenn Sie eben hier zum Beispiel so
Zahlen einsetzen wie hier, 85%, das heißt eine Hitrate von 0,85 und
Sie haben entsprechend dadurch eine Missrate hier von 0,15.
Und wenn Sie hier, sagen wir mal, 70 Nanosekunden stehen haben und
hier aber nur eine Nanosekunde stehen haben, Sie können sich
überlassen, Ihnen das auszurechnen, aber das Ganze ist doch deutlich
näher an einer Nanosekunde als es an den 70 Nanosekunden ist.
Also Sie bekommen dadurch durch diesen Cache sehr, sehr gute
Zugriffszeiten.
Gut, die Lesezugriffe sind wie gesagt recht einfach.
Die Schreibzugriffe sind etwas komplizierter.
Wobei auch wir hier den einfachen Fall haben, dass wir beim Schreiben
einen Miss haben, das heißt, wenn wir ein Datum schreiben, wir sollen
ein Datum schreiben, das nicht im Cache ist, das ist sehr einfach.
In diesem Fall haben wir einen Fehler, das heißt wir schreiben das
Datum in den Arbeitsspeicher, wir ändern unser Datum im
Arbeitsspeicher und hier hängt es davon ab, es wäre auch hier sehr
vernünftig zu sagen, okay, wir nehmen dieses Datum auch gleichzeitig
in unseren Cache auf.
Genau wie vorhin beim Lesen, ein Datum auf das ich zugreife, ich setze
auf die Lokalitätseigenschaft, ein Datum auf das ich zugreife, brauche
ich vielleicht in absehbarer Zukunft nochmal.
Also wie beim Lesen, ich das gelesene Datum bei einem Cache Miss in
den Cache übernommen habe, mache ich relativ sinnvollerweise beim
Schreiben dasselbe, wenn ich ein Datum in meinen Arbeitsspeicher
schreibe, das ich nicht im Cache hatte, dann schreibe ich gleich das
neue Datum eben auch in meinen Cache.
Das ist, denke ich, einfach und durchsichtig.
Etwas komplizierter wird es bei einem Cache Hit.
Das heißt, ich habe einen Treffer, ich möchte ein Datum verändern, ich
habe dieses Datum, das ich verändern möchte, bereits in meinem Cache
stehen.
Was mache ich?
Und hier kann ich jetzt verschiedene Cache-Organisationsformen
unterscheiden, die Ihnen vielleicht schon mal aufgefallen sind, wenn
Sie bei Ihrem PC zu Hause bei den Einstellungen des Chipsatzes mal
durch die einzelnen Einstellungen durchgeblättert haben, dann werden
Ihnen vielleicht einige Einstellungen zum Cache aufgefallen sein und
vielleicht auch die ein oder andere Einstellung von den
Organisationsformen, die ich jetzt mit Ihnen besprechen möchte.
Die einfachste Organisationsform, wie ich im Fall eines
Schreibzugriffs bei einem Treffer vorgehen kann, ist das sogenannte
Durchschreibverfahren oder auch die Write-Through-Policy.
Das ist eine Option, die Sie meistens irgendwo bei Ihrem Chipsatz im
PC wählen können.
Da gibt es da irgendwo einen Punkt Durchschreibverfahren.
Durchschreibverfahren funktioniert so, auch dann, wenn ich einen Cache
-Treffer habe, gehe ich her und ich verändere mein Datum immer
gleichzeitig im Cache wie auch im Speicher.
Das heißt also, ich habe hier irgendwo hier ein Datum, ich habe das
Datum natürlich auch hier und wenn ich jetzt hier einen neuen Wert
eintrage, nehmen wir den neuen Wert einfach mal X, dann trage ich
diesen neuen Wert gleichzeitig auch hier ein.
Das heißt, das Durchschreibverfahren geht so vor, dass es bei einem
Cache -Treffer beim Schreiben das Datum immer sowohl im Cache wie auch
gleichzeitig im Arbeitsspeicher verändert.
Ich denke, das ist eine einfache und sehr schöne Strategie, deren Vor-
und Nachteile auch sehr schnell aufgezählt sind.
Der Vorteil ist nämlich der, dass ich dadurch natürlich immer eine
garantierte Konsistenz zwischen Cache und Arbeitsspeicher habe.
Das heißt, es taucht hier niemals der Fall auf, dass durch so einen
Cache -Treffer im Cache etwas anderes steht als im Arbeitsspeicher,
für die gleiche Information.
Das heißt, wenn ich es hier ändere, ändere ich es gleichzeitig auch
hier.
Ich habe also immer hier ein X und auch hier ein X stehen.
Also niemals der Fall, dass hier ein X und vielleicht hier ein Y
steht, wie wir das bei anderen Verfahren, die wir jetzt noch
kennenlernen werden, durchaus passieren kann.
Das heißt, wir haben immer garantierte Konsistenz, immer das Gleiche
im Arbeitsspeicher wie im Cache.
Allerdings haben wir natürlich den Nachteil, dass für das Schreiben
der Cache praktisch unwirksam ist, weil ich beim Schreiben immer in
den Arbeitsspeicher auch schreibe, muss ich beim Schreiben also immer
die langsame Zykluszeit des Arbeitsspeichers benutzen.
Ich habe also beim Schreiben keinen direkten Vorteil durch meinen
Cache.
Ich habe einen indirekten Vorteil dadurch, dass der Wert, den ich
schreibe, natürlich auch in meinem Cache steht, sodass ich beim
nächsten Lesezugriff ich dann vielleicht nicht auf den Arbeitsspeicher
zugreifen muss, aber wenn ich nur den reinen Schreibvorgang betrachte,
habe ich keinerlei Vorteil, weil ich eben immer, dadurch, dass ich
auch in den Arbeitsspeicher schreibe, immer die langsame
Arbeitsspeicherzugriffszeit benutzen muss.
Gut, das ist natürlich unschön.
Jetzt kann man sich Gedanken machen, wie kann ich das Ganze
verbessern?
Wie kann ich das besser oder schneller machen?
Und eine Variante ist dann das sogenannte gepufferte-durchschreib
-Verfahren oder das Buffered-Write-Through-Policy.
Das ist im Prinzip eine Variante.
Was man macht, zeige ich vielleicht einfach mal an dem vorhergehenden
Bild.
Man geht her und fügt im Prinzip hier an dieser Stelle einen kleinen
Pufferspeicher plus eine kleine Hardware-Steuerung ein.
Dieser Pufferspeicher ist in der Lage, einige wenige Daten aufzunehmen
und schreibt Zugriffe zu Puffern.
Die Idee ist jetzt folgende.
Wenn ich hier einen Cache-Treffer habe, schreibe ich das Datum, ändere
ich mein neues Datum natürlich im Cache, ich ändere es auch
gleichzeitig im Arbeitsspeicher, aber ich mache diese Änderung im
Arbeitsspeicher nicht unter Kontrolle des Mikroprozesses, sondern ich
schreibe dieses zu ändernde Datum hier in diesen kleinen
Pufferspeicher.
Und hier habe ich jetzt eine kleine Hardware-Steuerung, die jetzt
autonom dafür sorgt, dass dieses Datum hier automatisch in den
Arbeitsspeicher abgelegt wird.
Das heißt, der Prozessor muss sich da nicht mehr darum kümmern, der
schreibt das Datum hier rein, mit der gleichen schnellen
Geschwindigkeit übergibt er das Datum an diesen Pufferspeicher und
wendet sich dann der nächsten Aufgabe zu, während dann diese kleine
Hardware -Steuerung in diesen Pufferspeicher dafür zu sorgen hat, dass
jetzt dieses Datum hier mit der langsamen Zugriffszeit in den
Arbeitsspeicher übertragen wird.
Damit kann ich zumindest eine ganze Reihe oder eine bestimmte Anzahl
von Zugriffen abfangen.
Natürlich irgendwann, wenn ich ständig Schreibtreffer habe, ist dieser
Pufferspeicher voll, dann funktioniert es natürlich nicht mehr.
Solange dieser Pufferspeicher aber eben noch Kapazität hat, kann ich
dann im Prinzip ein Durchschreibverfahren benutzen und aber trotzdem
mit der schnellen Cache-Zugriffszeit arbeiten und operieren.
Das ist als dieses gepufferte Durchschreibverfahren oder das Buffered
Write -Through-Policy, die eben diesen Nachteil mildert, indem ich
diesen Pufferspeicher mit dieser kleinen Hardware-Steuerung hier
einfüge.
Gut, und als dritte Variante, die Sie dann meistens eben auch bei
Ihrem Chipsatz im PC als Einstellung finden, ist das sogenannte
Rückschreibverfahren oder die Write-Back-Policy.
Hierbei gehe ich jetzt grundsätzlich anders vor.
Hier mache ich Folgendes, wenn ich ein Datum verändern möchte, das ich
an meinem Cache stehen habe, dann gehe ich her und ändere dieses Datum
nur in meinem Cache und ändere es nicht in meinem Arbeitsspeicher.
Damit kriege ich natürlich jetzt eine Diskrepanz, dann steht in meinem
Cache ein neueres Datum, als es in meinem Arbeitsspeicher steht, das
muss ich irgendwie kennzeichnen.
Dazu füge ich irgendwie ein spezielles Bit ein, ein sogenanntes
Altered Bit oder Modified Bit oder auch Dirty Bit, es gibt
verschiedene Namen dafür, die einfach kennzeichnen.
Das heißt, dieses Datum wurde im Cache geändert, wurde aber noch nicht
in den Arbeitsspeicher zurückgeschrieben und ich schreibe es nur dann
zurück, wenn jetzt genau dieses Datum verdrängt wird.
Das heißt, wenn also jetzt ein anderes Datum kommt, das in die Stelle
dieses geänderten Datums einnehmen muss, erst dann schreibe ich dieses
Datum zurück.
Das ist die Idee des Rückschreibverfahrens.
Das heißt also, ich habe hier einen Wert in meinem Arbeitsspeicher
stehen, sagen wir nehmen den Y, dann habe ich denselben Wert hier in
meinem Cache, hier ändere ich ihn, das heißt ich trage hier ein X ein
und ich trage diese Änderung nicht im Arbeitsspeicher nach, sondern
ich mache hier eine kleine Marke, ich mache hier einen Fleck dran,
eine Fahnenzeige an, okay, dieser Wert wurde verändert, er wurde aber
nicht im Arbeitsspeicher nachgetragen, das heißt hier haben wir noch
den alten Wert stehen, hier haben wir schon den neuen Wert stehen.
Das ist also das Abspeichern und das Rückschreiben hier, also hier das
Rückschreiben, wo ich dann hier wirklich sage, ich nehme das X hier
und trage es auch wirklich hier ein.
Dieses Rückschreiben passiert erst, wenn ich aus irgendeinem Grund
diesen Wert hier aus meinem Cache verdrängen muss, z.B.
weil es eben ein anderer Wert kommt, der genau diese Stelle einnehmen
soll, aus welchen Gründen auch immer, die werden wir nachher noch
kennenlernen, welche Gründe das sein können.
Erst dann ändere ich meinen Wert und schreibe ihn in den Speicher
zurück.
Gut, wie ist dieses Verfahren zu bewerten im Vergleich zum
Durchschreibverfahren?
Der Vorteil liegt auf der Hand.
Natürlich, wenn ich einen Cache-Treffer habe, kann ich jetzt auch
Schreibzugriffe mit der schnellen Cache-Zykluszeit abwickeln, dadurch,
dass ich eben nicht mehr immer meinen Arbeitsspeicher aktualisiere,
kann ich eben immer beim Schreibzugriff jetzt auch mit der schnellen
Cache -Zykluszeit arbeiten, wenn ich einen Treffer habe.
Der Nachteil besteht aber nun, dass ich unter Umständen
Konsistenzprobleme kriege zwischen dem Cache und meinem
Arbeitsspeicher, weil nämlich jetzt unterschiedlich alte Versionen
meiner Daten im Cache und im Arbeitsspeicher vorhanden sind.
Machen wir hier mal ein Beispiel, vielleicht anstelle hier dieses
aufwendigen Textes, vielleicht mal eine kleine Grafik.
Fügen wir hier mal eine Seite ein, nehmen wir an, wir haben hier
unseren Arbeitsspeicher,
wir haben unseren Cache-Speicher und
wir haben unseren Mikroprozessor.
So, solange das die einzigen Protagonisten in unserem Szenario hier
sind, haben wir keine Probleme, weil dann können wir im Prinzip
beliebig hier eine neuere Version halten als hier, das ist überhaupt
kein Problem, solange immer nur dann gewährleistet ist, dass wenn ich
hier einen Wert verdränge, dass ich ihn dann zurückschreibe,
funktioniert das völlig.
Haben wir überhaupt keine Schwierigkeiten mit.
Problematisch wird das Ganze, wenn wir jetzt hier noch irgendeinen
zweiten Protagonisten reinkriegen.
Das heißt, nur mal als Beispiel, wir haben hier einen zweiten
Mikroprozessor.
Ob mit oder ohne Cache ist relativ egal.
Nehmen wir da einfach an, er hätte keinen Cache.
Jetzt gibt es Probleme.
Und zwar können jetzt hier zweierlei verschiedene Probleme auftauchen.
Problemvariante Nummer 1,
wir haben hier ein Datum stehen, wir haben dieses Datum auch hier
stehen, wir haben das gecached und mittlerweile haben wir diesen Wert
hier bereits verändert.
Das heißt, hier haben wir bereits ein X draus gemacht.
Das heißt, hier im Cache steht ein neuerer Wert, also hier im
Arbeitsspeicher steht.
Was passiert, wenn jetzt dieser zweite Prozessor auf den
Arbeitsspeicher zugreifen möchte, weil er kann natürlich auf gar
keinen Fall auf diesen Cache zugreifen, davon weiß er nichts von dem
Cache, der ist lokal.
Also das hier bildet eine Einheit.
Also das hier ist eine Einheit, da kann man nicht reingucken.
Das heißt, dieser Mikroprozessor wird anstelle des aktuellen Datums X
hier das veraltete Datum Y vorfinden und wird mit dem veralteten Datum
arbeiten.
Das wäre also ein klassischer Fall von einem Konsistenzproblem.
Es ist aber nicht das einzige Konsistenzproblem, das auftauchen kann.
Machen wir mal ein anderes Beispiel.
Andere Farbe.
Nehmen wir an, wir haben hier...
...einen zweiten Wert.
Auf den haben wir bisher nur Lesen zugegriffen, da steht also ein A
drin zum Beispiel und wir haben hier Lesen bereits mit unserem ersten
Mikroprozessor zugegriffen.
Das heißt, der hat diesen Wert A...
...hier in seinen Cache-Speicher übernommen.
Bisher hat noch keiner geschrieben.
Das heißt, wir haben den Wert A hier und wir haben den identischen
Wert im Cache-Speicher.
So, jetzt geht dieser Mikroprozessor, der untere Mikroprozessor, her
und verändert diesen Wert und schreibt hier an dieser Stelle...
...schreibt dafür jetzt ein B hier rein.
So, jetzt haben wir genau den umgekehrten Fall.
Jetzt haben wir nämlich das neuere Datum im Arbeitsspeicher.
Das neuere Datum B steht im Arbeitsspeicher, während dieser
Mikroprozessor hier sein veraltetes Datum A noch im Cache hat.
Das heißt, jetzt haben wir gerade den umgekehrten Fall.
Dieser Prozessor arbeitet mit dem veralteten Datum, während der hier
das neue Datum hat.
Sie sehen also, durch dieses Verfahren können zweierlei Probleme
auftauchen.
Einmal, wir haben ein veraltetes Datum im Arbeitsspeicher stehen und
ein neues Datum im Cache.
Oder wir haben umgekehrt, wir haben ein veraltetes Datum im Cache und
ein neues Datum im Arbeitsspeicher.
Das sind die klassischen Konsistenzprobleme, die eben mit dem
Durchschreibverfahren zusammen auftauchen.
Und diese erfordern eben eine relativ aufwendige Steuerung.
Es gibt eine ganze Reihe von Verfahren, wie man das in den Griff
kriegen kann.
Das ist aber jenseits dieser Vorlesung.
Das werden Sie zum Beispiel in der rechten Strukturvorlesung
kennenlernen, wie man das machen kann.
Nur so als kleine Andeutung, was man dort an der Stelle macht, ist
meistens so eine Art Busschnüffeln oder auch Bussnooping.
Das heißt, um diesen Problem in den Griff zu kriegen, bedeutet das,
der Mikroprozessor hier muss immer überwachen, was der tut und
umgekehrt.
Das heißt, wenn dieser Prozessor nicht aktiv am Bus ist, schnüffelt
er, was dieser Prozessor tut.
Wenn der Prozessor nicht aktiv am System ist, schnüffelt er, was
dieser Prozessor tut.
Und auf diese Art und Weise versuchen eben beide mitzubekommen, wann
solche Inkonsistenten entstehen und dann einzugreifen, um diese
Inkonsistenten zu vermeiden.
Das ist Busschnüffeln oder Bussnooping, wie es auch in heute allen
modernen Mikroprozessoren in der Regel durchgeführt wird, um eben
solche Probleme in den Griff zu kriegen.
Aber hier würde ich dann auf die rechten Strukturvorlesung, die Sie im
Hauptdiplom haben, verweisen, wo Sie hier verschiedene Verfahren näher
kennenlernen werden.
Gut.
Schauen wir uns das mal ein bisschen genauer an, nachdem wir jetzt
gesehen haben, wie das Lesen und wie das Schreiben im Prinzip
funktionieren, wie ein Cache-Speicher jetzt genauer aufgebaut ist.
Und eigentlich ist ein Cache-Speicher nicht nur ein Speicher, ein
Cache -Speicher sind eigentlich immer zwei Speicher.
Denn ich muss ja nicht nur ein Datum aus meinem Arbeitsspeicher
speichern, sondern ich muss mir auch merken, wo war denn das Datum in
meinem Arbeitsspeicher gelegen.
Ich muss ja irgendwo jetzt, wenn der Prozessor auf eine Adresse im
Arbeitsspeicher zugreifen möchte, muss ich ja prüfen, habe ich dieses
Datum im Cache.
Das heißt, ich muss jetzt gucken, habe ich ein Datum unter der Adresse
in meinen Cache abgelegt.
Das bedeutet, ein Cache-Speicher besteht eigentlich immer aus zwei
Speichern.
Nämlich den eigentlichen Daten, in dem die Daten abgelegt sind und
zusätzlich einem Adressspeicher, in dem für jedes Datum die Adresse
steht, wo dieses Datum im Hauptspeicher zu finden ist.
Und hier habe ich eben einen Komparator, einen Vergleicher.
Und es ist die Aufgabe dieses Komparators eben festzustellen, wenn
hier, hier haben wir nochmal das große Bild.
Also das können Sie sich im Prinzip als Vergrößerung betrachten von
einem von den Bildern, die wir vorher hatten.
Von diesem Bild hier.
Im Prinzip ist dieser Bereich hier rausvergrößert mit dem Adressbus
oben, dem Cache und dem Datenbus unten.
Genau das müssen Sie sich als Vergrößerung vorstellen.
Hier oben der Adressbus, hier unten der Datenbus.
Und immer dann, wenn der Mikroprozessor hier eine Adresse am Adressbus
hat, muss in diesem Komparator verglichen werden, habe ich die gleiche
Adresse hier in meinem Adressspeicher.
Wenn ja, wenn ich diese Adresse in meinem Adressspeicher habe, dann
habe ich wohl einen Cache-Hit, dann habe ich das Datum hier.
Und dann kann ich anstelle über den langsamen Arbeitsspeicher arbeiten
zu müssen, kann ich dieses Datum hier aus meinem Datumspeicher
entnehmen.
Das heißt, ein Cache-Zugriff funktioniert immer in zwei Schritten.
Zum Ersten muss eben die Adresse über einen Komparator mit allen
Adressen im Adressspeicher verglichen werden.
Es muss festgestellt werden, ist die Adresse hier vorhanden.
Wenn die Adresse vorhanden ist, muss der zugehörige Eintrag im
Datenspeicher aufgesucht werden.
Und dann kann ich mein Datum an den Datenbus legen.
Gut.
Die Frage ist jetzt noch, wie sind meine Daten organisiert?
Das heißt, wie ist hier mein Datenspeicher hier organisiert?
Na gut, wie ihr einfach seht, ist die Idee natürlich zu sagen, ich
organisiere meinen Datenspeicher genauso wie meinen Arbeitsspeicher.
Das heißt, wenn mein Arbeitsspeicher zum Beispiel 32 Bit organisiert
ist, dann organisiere ich diesen Datenspeicher auch 32 Bit.
Das heißt, jeder Eintrag hier im Cache enthält 32 Bit.
Das hat man auch bei älteren Mikroprozessoren so gemacht.
Das hat aber den Nachteil, dass der Verwaltungsaufwand relativ stark
ansteigt.
Weil im Prinzip muss ich mir für jedes Datum, das ich hier
eigenständig einlagere, eine Adresse merken.
Und diese Adresse muss ich auch vergleichen.
Und als kleinen Vorgriff, es ist ja nicht ganz so trivial, diese
Adresse zu vergleichen, weil ich habe nicht viel Zeit.
Ich muss das sehr schnell tun.
Ich muss also extrem schnell feststellen, ist meine Adresse hier in
dem Adressspeicher vorhanden oder nicht.
Das heißt, es ist sicher einsichtig, je weniger Adressen hier abgelegt
sind, desto einfacher wäre das Ganze.
Ja gut, und die Grundidee, was man hier durchführen kann, ist ganz
einfach.
Ich kann dann sagen, okay, ich mache meine Daten hier einfach breiter.
Das heißt, ich speichere nicht ein einzelnes Wort hier ab, sondern ich
nehme hier einen ganzen Block, eine sogenannte Cache-Line.
Das heißt, ich speichere ja mehr als einfach die Breite meines
normalen Arbeitsspeichers ab, sondern vielleicht zweimal die Breite,
dreimal die Breite, viermal die Breite und so weiter.
Wenn ich nämlich hier breitere Daten abspeichere, brauche ich hier
entsprechend weniger Adressen.
Und ich merke mir einfach hier immer nur die Anfangsadresse meines
ganzen Datenblocks, den ich ablege.
Das heißt, ich lege in meinen Datenspeicher nicht ein einzelnes Datum
ab, sondern immer einen ganzen Datenblock und speichere hier dann die
Anfangsadresse meines Datenblocks.
Das ist sicher auch sinnvoll im Zusammenhang der Lokalität, die wir ja
vorhin betrachtet haben.
Und dann eben mit der Wahrscheinlichkeit, dass ich, wenn ich ein Datum
brauche, dass ich das nachfolgende Datum mit sehr hoher
Wahrscheinlichkeit auch benötige, ist es auch von dem Rahmen her
sinnvoll zu sagen, okay, ich speichere nicht ein einzelnes Datum im
Cache, sondern immer gleich einen größeren Block.
Das heißt, heute kann ich sagen, jeder Dateneintrag in meinem Cache
besteht aus einem ganzen Block, auch einer sogenannten Cache-Line.
Die sind üblicherweise bis zu 64 Byte groß.
Das heißt dann nicht 64 Bits, sondern ganze 64 Byte, also sehr breit.
Man hofft eben dadurch auch, die Lokalität zu verbessern und im
Adressspeicher lege ich eben, wie gesagt, nur noch die Basisadresse
jedes Blocks ab.
Gut, mein Komparator hat eben dann die Aufgabe, festzustellen, ist
diese Adresse, die gerade auf meinem Adressbus liegt, ist die auch in
meinem Cache-Speicher vorhanden?
Wenn ja, habe ich ja meinen Cache-Hit, wenn nein, habe ich meinen
Cache -Miss.
Und eben da der Cache-Speicher schnell sein soll, muss dieses sehr
schnell gehen.
Man kann im Prinzip sagen, das muss in einem Taktzyklus gehen.
Das heißt, es muss möglich sein, in meinem Cache-Speicher innerhalb
eines einzelnen Taktzykluses festzustellen, ist meine Adresse in
meinem Adressspeicher vorhanden oder ist sie nicht vorhanden.
Das stellt einige Anforderungen an den Hardwareaufbau dieses
Vergleichers.
Daran lassen sich auch verschiedene Cache-Typen noch unterscheiden.
Das möchte ich dann später auch noch ein bisschen genauer betrachten.
Hier möchte ich jetzt nochmal ein kleines bisschen zunächst mal
zusammenfassen, was wir so bisher schon gesehen haben.
Also zunächst mal jede Cache-Seile, die enthält im Prinzip ein Par.
Das Par besteht aus einem Adress und einem Dateneintrag.
Zusätzlich kommen natürlich noch ein paar Status-Bits hinzu.
Zum Beispiel ein Status-Bit, das haben wir vorhin gesehen, für das
Durchschreibverfahren, wie eben dieses Dirty-Bit, dieses schmutzige
Bit, das eben anzeigt, dieser Eintrag wurde im Cache geändert, aber er
wurde noch nicht im Arbeitsspeicher geändert.
Ein anderes Status-Bit ist üblicherweise ein sogenanntes Valid-Bit.
Sie müssen ja davon ausgehen, am Anfang, irgendwann mal ist der Cache
leer.
Das heißt, die ganzen Cache-Seilen enthalten noch gar keine Einträge.
Das muss ich auch irgendwie kennzeichnen.
Ich muss kennzeichnen, dieser Eintrag ist leer.
Dazu habe ich in der Regel ein weiteres Bit, das mir eben anzeigt, ist
diese Seile im Cache leer oder ist sie gefüllt.
Das ist üblicherweise das Valid-Bit.
Gut, in dem Datenbereich speichere ich dann eben heute nicht
üblicherweise ein einziges Datum ab, sondern ich speichere immer einen
ganzen Datenblock ab, der eben eine zusammengehörige Reihe von
Speicherplätzen darstellt.
Ich nehme also immer einen ganzen Block aus meinem Hauptspeicher und
übertrage ihn in eine Cache-Seile.
Dazu lege ich dann als Adresse eben die Startadresse dieses Blocks ab.
Und diese hat auch noch einen Namen.
In diesem Adressspeicher spricht man auch noch von einem Adressetikett
oder auch von einem Tag oder einem Cache-Tag oder auch von einem
Index, je nachdem, ob Sie das ganzdeutsch oder mehr neudeutsch haben
wollen.
Das heißt also, diese Adresse, diese Startadresse meines Blocks, den
ich in meinem Adressspeicher im Cache abgelegt habe, nimmt man eben
auch noch, ja, üblicherweise im englischsprachigen Raum ein Cache-Tag,
im deutschsprachigen Raum spricht man entweder auch von einem Cache
-Tag oder man spricht von einem Adressetikett.
Gut, das heißt, dieses Adressetikett bestimmt eben die Startadresse
des aktuellen Blocks im Hauptspeicher und die Status-Bits sagen eben,
wie schon gesagt, aus, habe ich gültige Daten im Cache oder habe ich
keine gültigen Daten.
Gut, wenn wir jetzt die Arbeitsweise des Caches unter der Prämisse
nochmal ein bisschen genauer beleuchten, dann ist die Aufgabe des
Cache -Speichers, im Prinzip kann ich zweierlei Komponenten oder
Bestandteile isolieren, nämlich die Cache-Steuerung muss im Prinzip
prüfen, wenn ich jetzt zugreife, ob zum ersten die Speicheradresse
oder der zu der Speicheradresse gehörende Dateninhalt, ob der in
meinem Cache steht.
Das heißt, ich muss meinen Adressvergleich durchführen.
Ist die Adresse, auf die ich zugreife, auch in meinem Cache vorhanden?
Das ist die Bedingung 1.
Und zum zweiten muss ich noch testen, sind diese Daten, die dort
stehen, auch gültig oder sind durch irgendeinen Grund ungültig
geworden.
Das ist üblicherweise die Bedingung 2.
Das heißt also, ich muss mal schauen, ob dieses Gültigkeits-Bit
gesetzt ist.
Der einfachste Fall ergibt sich natürlich am Anfang, der Cache ist
völlig leer.
Jetzt ist es aber so, dass ein leerer Speicher natürlich nicht mit
Nullen gefüllt ist, sondern bei sowohl dynamischen wie statischen
Speichern ist der Inhalt in der Regel recht zufällig nach dem
Einschalten.
Es ist relativ einfach, wenn Sie sich mal einen statischen Speicher
vorstellen.
Statische Speicher wurden durch Flip-Flops realisiert.
Flip-Flops haben zwei stabile Zustände.
Beide werden gleich gerne eingenommen.
Wenn ich jetzt die Spannung einschalte, ohne das Flip-Flop irgendwie
zu beeinflussen, dann ist es relativ zufällig, welcher Zustand
eingenommen wird.
Das hängt von bestimmten Technologie-Parametern ab.
Verarbeitungstoleranzen, ob jetzt vielleicht irgendwo ein bestimmtes
Gate an einem Transistor ein bisschen besser ausgebildet ist an einem
anderen.
Das heißt aber, ich kann nicht vorhersagen, zumindest wenn ich nicht
genaue, sehr detaillierte Technologie-Untersuchungen mache, welchen
Zustand so ein Flip-Flop einnehmen wird.
Und schon gar nicht, wenn ich jetzt einen Speicher habe, der aus
vielen hundert oder tausend oder hunderttausend von diesen Flip-Flops
besteht, kann ich nicht vorhersagen, welchen Zustand er annehmen wird.
Das heißt, dieser Adressspeicher wird am Anfang einen völlig beliebig
zufälligen Wert haben.
Und da könnten durchaus zufälligerweise Adressen stehen, auf die ich
gerade zugreife.
Das heißt, ich könnte durchaus die Bedingungen 1 erfüllt haben.
Ich möchte auf die Adresse ABC zugreifen.
Und eben durch den Schmutz-Effekt beim Einschalten steht diese Adresse
ABC irgendwo in meinem Adressspeicher.
Dann hätte ich im Prinzip die Bedingungen 1 erfüllt.
Aber natürlich ist das kein gültiger Eintrag.
Dazu habe ich eben meinen Valid-Bit, das eben diesen Eintrag als
ungültig, noch nicht geschrieben, kennzeichnet.
Und damit wäre die Bedingung 2 verletzt.
Und Sie pflichten mir sicher bei, dass es einfacher ist.
Ich habe so eine Reihe von Valid-Bits, die ich am Anfang mal
zurücksetzen muss.
Einzelne Bits pro Cache-Block.
Also ich müsste den ganzen Adressspeicher zurücksetzen.
Das macht sicher viel weniger Aufwand, eben ein paar wenige Bits
zurückzusetzen als den ganzen Adressspeicher.
Das heißt, ich habe immer bei einem, die Aufgabe der Cache-Steuerung
steht im Prinzip immer da drin, die Bedingungen 1 zu prüfen.
Habe ich also meine Adresse, die Bedingungen 2 zu prüfen, ist das
Gültigkeits -Bit gesetzt.
Und diese Prüfung führt eben zu einem Cache-Treffer oder zu einem
Cache -Miss.
Der Cache-Miss ist genau dann gegeben, wenn eben eine der beiden
Bedingungen nicht erfüllt ist.
Gut, betrachten wir das mal jetzt ein bisschen genauer.
Betrachten wir mal den Lesezugriff.
Also wir haben einen Fehlzugriff.
Eine der beiden Bedingungen ist nicht erfüllt.
Was machen wir dann?
Wie eben vorhin schon gesagt, wir lesen das Datum aus dem
Hauptspeicher und laden das Datum in den Cache-Speicher.
Das ist der erste Schritt, den wir tun.
Der zweite Schritt, wir haben jetzt ja einen gültigen Eintrag im Cache
-Speicher.
Das heißt, wir setzen für diesen Eintrag dieses Gültig-Bit, dieses
Valid -Bit oder V-Bit oder V-Bit, wie Sie es immer auch nennen mögen.
Wir kennzeichnen den Eintrag damit als gültig.
Und schließlich machen wir als drittes Speicher die Adressinformation
in unserem Adressspeicher.
Indem wir einfach dort ablegen, unter welcher Adresse ist dieses Datum
im Hauptspeicher zu finden.
Das heißt, wir haben drei Schritte letztendlich im Cache zu tun.
Erster Schritt, übertrage das Datum an sich oder den zugehörigen
Datenblock.
Wir übertragen immer einen ganzen Datenblock vom Arbeitsspeicher in
den Cache-Speicher.
Schritt zwei, setze für diesen Datenblock das Gültig-Bit.
Dieser Eintrag ist gültig.
Schritt drei, übertrage die Startadresse des Datenblocks in den
Adressspeicher des Caches.
Um eben festzulegen, aha, wir haben genau unter dieser Adresse das
Datum in unserem Cache.
Gut, gleiche Prämisse, Schreibzugriffe.
Hier geht es im Prinzip umgekehrt.
Wobei wir hier uns noch überlegen können, ob wenn wir ein Datum im
Cache schreiben, ob wir das Datum entsprechend auch aktualisieren oder
nicht aktualisieren.
Das haben wir vorhin gesehen, das war entsprechend die Rückschreib-
oder Durchschreibphilosophie.
Beim Cache-Treffer habe ich immer dann, wenn beide Bedingungen, die
ich vorhin betrachtet habe, erfüllt sind, also Bedingung 1 sowohl als
auch Bedingung 2, dann kann ich eben meinen Zugriff auf den Cache
-Speicher erfüllen oder durchführen.
Gut, wir haben also gesehen, wir müssen jetzt zwei Bedingungen prüfen.
Wir müssen Adressbedingungen überprüfen, wir müssen die Bit
-Bedingungen überprüfen.
Diese Bit-Bedingungen überprüfen ist sicher ziemlich eine einfache
Angelegenheit normalerweise.
Schwieriger wird es, die Adressbedingungen zu überprüfen.
Oh, haben wir noch eine Verdrängungsstrategie, habe ich vergessen,
eine Folie dazwischen.
Okay, schieben wir diese Folie ein.
Also ein Problem ist auch natürlich noch ein anderes Problem.
Wenn ich jetzt ein Datum in den Cache einlagere, muss ich unter
Umständen, sobald der Cache voll ist, ein anderes dafür rausschmeißen.
Das bestimmt die sogenannte Verdrängungsstrategie.
Das heißt, nach einem Cachemiss muss ich eine neue Adresse finden, wo
ich mein jetzt zu lesendes Datum einlagere.
Dazu muss ich eine Speicherportion überschreiben, ich muss
herausfinden, wo ich das tun kann.
Gut, diese Verdrängungsstrategie ist, wie wir jetzt gleich noch sehen
werden, nicht bei allen Cachetypen sinnvoll, sondern nur bei ein paar
Cachetypen.
Es gibt nämlich bestimmte Cachetypen, da lassen wir überhaupt keine
Wahl, wo ich was hinschreibe.
Dort muss ich mir keine großen Gedanken über Verdrängung machen.
Andere Cachetypen lassen wir die Wahl und bei denen muss ich mir
überlegen, wo ich es hinschreibe.
Na gut, und hier kann ich mir natürlich jetzt alle möglichen
Verdrängungsstrategien ausdenken.
Allerdings oberste Priorität ist Geschwindigkeit, das muss schnell
gehen.
Deswegen wird man eine relativ einfache Verdrängungsstrategie wählen.
Also was man üblicherweise benutzt, ist sowas wie Least Reason Deused,
also das am längsten nicht benutzte Speicherportion.
Die wird ersetzt, wobei man hier oft sogar neben dem echten Least
Reason Deused einen sogenannten Pseudo-Least Reason Deused-Algorithmus
benutzt.
Ein Pseudo-Least Reason Deused ist ein Algorithmus, der im Prinzip
anhand von wenigen Bits, zwei, drei Bits, hier ein Urteil abgibt,
welches der schon am längsten nicht mehr benötigte Speicherblock ist.
Das zwar nicht mit absoluter Präzision bestimmen kann, aber mit
relativ hoher Wahrscheinlichkeit, der aber viel schneller ist, als
wenn ich es wirklich bestimme.
Das Problem ist ja, wenn ich einen echten Least Reason Deused
-Algorithmus benutze, dann muss ich im Prinzip für jeden Eintrag einen
Zähler mitführen, in dem ich zähle, wie oft wird auf diesen Eintrag
zugegriffen.
Und dann muss ich im Prinzip meine Zähler vergleichen.
Das heißt, ich habe einen relativ aufwendigen Vergleich und einen
relativ aufwendigen Zähler, den ich pro Eintrag brauche.
Diese Pseudo-Least Reason Deused-Algorithmen benutzen hier nur wenige
Bits anstelle vollwertiger Zähler und schaffen es relativ
näherungsweise ein Verhalten wie LAU zu bekommen, aber bei weniger
Aufwand und höherer Geschwindigkeit.
Gut, jetzt kommen wir zu dem, was ich vorhin eigentlich schon sagen
wollte.
Dieser Adressvergleich ist eben jetzt eine der entscheidenden
Tätigkeiten, die ich in meinem Cache-Speicher durchführen muss.
Weil ich muss diesen Adressvergleich in einem einzelnen Taktzyklus
durchführen.
Ich muss also in einem Taktzyklus wissen, ist eine Adresse in meinem
Adressspeicher, ist sie nicht drin?
Und je nachdem, wie dieser Adressvergleich durchgeführt wird, lassen
sich jetzt verschiedene Cache-Typen unterscheiden.
Und zwar unterscheiden wir in der Regel einen sogenannten Voll
-Assoziativen Cache, einen Direct-Map Cache und einen NW-Set
-Associative Cache oder auch einen NW-Gesatz-Assoziativen Cache.
Für Direct-Map Cache kenne ich jetzt keine deutsche Übersetzung.
Kennen Sie eine deutsche Übersetzung für Direct-Map Cache?
Ach, direkt abgebildeter Cache, das habe ich noch selten gehört.
Aber den Satz Assoziativen Cache, den hört man schon mal öfters.
Gut, wir wollen jetzt einfach mal diese drei Techniken betrachten.
Und zwar genau in der Reihenfolge, wie sie hier stehen.
Wir fangen mit dem Voll-Assoziativen an.
Weil er vom Verständnis der einfachste und vom Aufwand der schlimmste
eigentlich ist.
Bei einem Voll-Assoziativen Cache habe ich immer das Problem, ich habe
hier meinen großen, großen Arbeitsspeicher.
Zum Beispiel hier, der kann 1024 Blöcke fassen.
Und ich habe meinen kleinen Cache, der leider nur 64 Blöcke fassen
kann.
Ich muss irgendwie eine Abbildung machen.
Ich muss irgendwie hier Daten reinschreiben.
Und ich muss dann feststellen, habe ich ein Datum hier oder nicht.
Dieses Tag hier ist immer die Adresse, dieses Adressetikett.
Das heißt hier, wenn ich zum Beispiel diesen Block 1023 hier
reinschreibe, irgendwo an irgendeiner von diesen Stellen, dann müsste
ich mir hier als Tag 1023 notieren.
Das heißt dann zum Beispiel, ich nehme hier,
ich schreibe das hier rein, dann müsste ich hier in dieses Tag
notieren, ich habe 1023.
Das ist schwer hier mit klein zu schreiben.
Machen wir es größer.
Ich würde also hier in dieses Tag 1023 reinschreiben.
Nur als kleiner Tipp an der Stelle, also an der Stelle würde ich auch
ziemlich gut aufpassen, weil diese Aufgaben kommen sehr, sehr gerne in
der Klausur.
Solche Cache-Aufgaben.
Das sind beliebte Klausuraufgaben der TE2.
Wie mache ich jetzt hier diese Abbildung?
Ich kann es nämlich genauso machen, wie ich es hier gemacht habe,
nämlich assoziativ, indem ich einfach sage, ich mache es
inhaltbezogen.
Also wenn ich den Block 1023 hier ablege, dann schreibe ich hier die
Adresse oder das Tag 1023 rein.
Damit habe ich eine vollassistative Abbildung des ganzen Bereiches.
Das heißt, ich mache wirklich einfach über den Inhalt.
Wenn ich also wissen möchte, ist dieser Block 1023 in meinem Cache
vorhanden, dann muss ich im Prinzip jedes einzelne Tag abprüfen, steht
in jedem einzelnen Tag der 1023 drin oder nicht.
Da ich jetzt hierfür nicht beliebig viel Zeit habe, kann ich es nicht
nacheinander machen, sondern ich muss es parallel tun.
Das heißt, ich kann nicht nacheinander prüfen, steht es da drin, steht
es da drin, steht es da drin, die Wahrscheinlichkeit habe ich nicht.
Ich muss in einem einzelnen Tagzyklus feststellen, steht irgendwo in
einem dieser Tags zum Beispiel der Wert 1023 und zu wissen, ob ich
diesen Block hier in meinem Cache habe.
Und das bedeutet, ich muss im Prinzip für jede Cache-Line einen
Vergleicher spendieren.
Ich muss also für jede Cache-Zeile, für jedes Tag einen separaten
Vergleicher spendieren, um eben in einem Tagzyklus parallel
feststellen zu können, ist mein Tag vorhanden oder ist es nicht
vorhanden.
Das heißt, der Aufbau sieht im Prinzip so aus.
Machen wir mal ein Beispiel Cache, Kapazität 256 Byte, ein Cache
-Block, bestehende aus vier Wörtern, sprich insgesamt 16 Byte, also
eher etwas Kleines.
Das heißt, wir haben dann insgesamt bei 256 Byte 16 Cache-Lines,
jeweils 16 Byte.
Hier oben haben wir unsere Adresse, das ist einmal eine 32-Bit
-Adresse.
Na gut, die unteren vier Bits sind relativ einfach zu wählen.
Die unteren zwei Bits, die wählen das Byte im Wort aus.
Im Prinzip gehen wir von 32 Bit hier auch aus.
Das heißt, wir haben also vier Bytes in jedem Wort.
Mit den unteren beiden Bits wähle ich hier innerhalb jeder Worte, die
ich hier habe, das entsprechende Byte aus.
Mit den nächsten beiden Bits wähle ich innerhalb der Zeile das Wort
aus.
Das heißt, ich habe hier vier 32-Bit-Worte.
Hier eins, zwei, drei, vier, die wähle ich mit diesen zwei aus.
Innerhalb eines 32-Bit-Worts habe ich wieder vier Bytes.
Also hier habe ich, wenn wir es nochmal klein teilen, habe ich hier
eben meine vier Bytes stehen.
Habe ich hier meine vier Bytes.
Und die wähle ich mit diesen unteren zwei Zeilen aus.
Das ist also relativ einfach.
Das heißt, mit den unteren vier Bits meiner Adresse, die habe ich
entsprechend schon verwertet, bleiben mir jetzt hier die oberen 28
Bits.
Und diese 28 Bits sind jetzt mein Tag.
Die lege ich hier in meinen Adressspeicher ab.
Also das hier ist der Adressspeicher meines Caches.
Hier lege ich diese Tags ab.
Dann habe ich hier zum Beispiel meine zwei Bits, mein Valid und mein
Dirty Bit.
Das anzeigt, ist hier überhaupt schon was Gültiges drin.
Und ich habe hier drin, ist der Eintrag verdrängt oder nicht.
Gut, und dann muss ich im Prinzip für jede einzelne Cache-Line jetzt
hier einen Vergleicher spendieren, der mir sozusagen sagt, ist das,
was hier drin ist, identisch mit dem, was ich hier suche.
Das heißt, wenn ich eine Adresse anlege hier, an diesen Cache, um
festzustellen, habe ich mein Datum im Cache oder nicht, wird diese
Adresse parallel in diesem Fall an alle 16 Vergleicher angelegt.
Alle diese 16 Vergleicher vergleichen parallel die Adresse, die ich
hier angelegt habe, die oberen 28 Bits der Adresse, mit den 28 Bits,
die hier im Tag stehen.
Und wenn ich den Wert habe, dann wird einer dieser Vergleicher hier
schreien.
Der wird also melden, ich bin da.
Wenn ich keinen Treffer habe, wird keiner der Vergleicher melden, ich
bin hier.
Das heißt, die Ergebnisse, all diese Vergleiche würden jetzt oder
verknüpft werden und würden hier am Ausgang einen Bit liefern, nämlich
einen Tag Hit, eine 1, wenn einer der Vergleicher hier ein positives
Resultat geliefert hat, oder eine 0, wenn keiner der Vergleicher ein
positives Resultat geliefert hat.
Das heißt, hier raus kriege ich dann einen Tag Hit.
Des Weiteren muss ich jetzt die entsprechende Zeile anwählen.
Das heißt, derjenige Vergleicher, der mir gesagt hat, hier, ich habe
das Datum, in dieser Zeile muss ich mir jetzt hier noch das Valid Bit
anschauen.
Ich muss feststellen, ist der Eintrag auch gültig, habe ich hier ein
Valid.
Das Ganze muss ich unverknüpfen, also das ist die Bedingung 1.
Adressvergleich, Bedingung 2, Gültigkeit.
Und die Unverknüpfung liefert mir hier schließlich letztendlich das
Ergebnis Cash Hit oder Cash Miss mit gleichzeitiger Adressierung der
entsprechenden Zeile über denjenigen Vergleicher, der hier geschrien
hat.
Also, wenn wir zum Beispiel hier unseren Treffer gehabt hätten,
dann hätte dieser Vergleicher hier, hätte die 1 geliefert, alle
anderen hätten die 0 geliefert.
Dann hätten wir hier das entsprechende Valid Bit noch überprüft.
Wenn also auch dieses Valid Bit hier zum Beispiel eben eine 1 drin
stehen gehabt hätte, also Valid, dann hätten wir hier eine 1 bekommen,
dann hätten wir hier die Cash Line 1 gehabt und dann hätten wir auf
diese Zeile hier zugreifen können und hätten unseren Eintrag gefunden.
Damit haben wir also unser Problem gelöst.
Wir haben in einem Taktzyklus festgestellt, es ist vorhanden, ja,
nein.
Und wenn ja, wir haben den entsprechenden Block, die entsprechende
Cash Line adressiert und können sie hier auslesen.
Das heißt, wenn wir den volladaptiven Cash hier nochmal
zusammenfassen, heißt das, ich brauche einen vollparallelen Vergleich
aller Adressen, die im Adressspeicher vorhanden sind in einem einzigen
Taktzyklus.
Das ist die Bedingung.
Der Vorteil ist natürlich klar, ich kann mit dieser Strategie ein
Datum an beliebiger Stelle im Cash ablegen.
Ich bin nicht gebunden irgendwo, ich kann ein Datum, wenn ich habe, an
jeder beliebigen freien Stelle in meinem Cash ablegen.
Das heißt, ich habe eine optimale Cash Ausnutzung und kann auch bei
der Verdrängung völlig frei wählen, welches Datum verdränge ich aus
dem Cash.
Wir werden eben nachher andere Strategien kennenlernen, wo das nicht
mehr der Fall ist.
Aber hier kann ich das machen.
Das heißt, ich bin also sicher hier sehr glücklich dran.
Auf der anderen Seite sind auch einige Nachteile mit verbunden.
Der erste und Hauptnachteil ist natürlich der immense Hardwareaufwand.
Das heißt, ich brauche für jede Cashzeile einen Vergleicher.
Das heißt, ich kann volladaptive Cashs im Prinzip nur für sehr kleine
Cashs machen.
Also so 128, 256 Cashlines kann man machen, aber darüber hinaus wird
es einfach zu aufwendig.
Und ich habe natürlich ein weiteres Problem.
Dadurch, dass ich eben völlig frei eine Verdrängungsstrategie wählen
kann, dass ich total flexibel bin, wo schreibe ich was rein und wo
verdränge ich was, brauche ich dafür eine Steuerung.
Das heißt, ich muss beim volladaptiven Cash zusätzlich eine irgendwie
geartete Steuerung vorsehen, die jetzt bestimmt, in welche Zeile
übertrage ich denn ein neues Datum und was schmeiße ich dafür raus.
Das heißt, also hier brauche ich entsprechend mehr Hardware.
Entsprechend das krasse Gegenstück zum volladaptiven Cash ist der
Direct Map Cash.
Beim Direct Map Cash geht man jetzt vor und hat überhaupt keinen
assoziativen Vergleich, sondern man macht eine wirklich, man wählt
eine feste Zuordnung.
Das heißt, man legt für jeden Block fest, in welchen Platz, in welchen
Teil des Cashs er kommen kann.
Das heißt, zum Beispiel hier, der Block 0 kann, wenn er im Cash steht,
nur hier stehen.
Der Block 64 kann, wenn er im Cash steht, nur hier stehen.
Der Block 128 kann, wenn er im Cash steht, nur hier stehen.
Der Block 1023 kann, wenn er im Cash steht, nur hier stehen.
Das ist die Grundidee des Direct Map Cash.
Das heißt, man reserviere für jeden Block im Hauptspeicher oder
Arbeitsspeicher einen festen Block im Cash.
Das heißt, ich habe überhaupt keine Wahlfreiheit mehr, wenn ich einen
Block einlagere, wenn ich den Block 64 in den Cash einlagere, kann ich
ihn nur hier einlagern, nirgends anders.
Oder wenn ich den Block 1023 einlagere, kann ich ihn nur hier
einlagern.
Das hat auch einen Vorteil, wenn ich das so mache.
Wenn ich einen bestimmten Block nur an einer bestimmten Stelle
einlagern kann, dann muss ich auch, wenn ich nach dem Block suche, nur
an dieser einen bestimmten Stelle suchen, ob er denn da ist oder
nicht.
Das heißt, mein Vergleichsaufwand wird erheblich geringer.
Meine Schlüsselfrage ist jetzt, wie mache ich das?
Wie lege ich fest oder wie mache ich überhaupt die Zuordnung?
Wie gewährleichte ich, dass ein bestimmter Block immer nur an eine
bestimmte Adresse kommen kann?
Oder an eine bestimmte Stelle im Cash kommen kann?
Na gut, das kann ich relativ einfach machen.
Das sieht man vielleicht besser auf dem nächsten Bild.
In dem ich meine Adresse in drei Teile aufteile.
Ich habe nach wie vor meine Wortadresse, mit der ich jetzt innerhalb
meiner Cashline, wie vorhin, eben entsprechend Bits, Bytes und Worte
auswähle, also diese vier von vorhin.
Beim volladjektiven Cash hatte ich dann hier, neben der Wortadresse,
den Tagteil.
Hier mache ich jetzt voll und das hier teile ich jetzt nochmal auf in
einen Indexteil, in einen Tagteil.
Das heißt, ich schneide mir einfach einen gewissen Teil hier raus aus
meiner Adresse, bestimmte einzelnen Bits, benutze die als Index,
benutze hier einen Decoder und lege mit diesen Bits meine Adresse
fest, in welche Cashline kommen denn die Einträge, die hier
dazugehören.
Um das mal als Beispiel zu machen, nehmen wir mal an, wir hätten 4-Bit
-Wortadresse, sagen wir mal 3-Bit-Index und dann hier Tagadresse, die
kennst du eigentlich wieder mal mit 0.
Das ist mein Index hier.
Dann würde das bedeuten, dass diese 3 Bits letztendlich bestimmen, an
welche Cashline ein bestimmter Blockeintrag kommt.
Das heißt also, dieser Wert hier und dieser Wert, diese beiden
Adressen würden in den selben Cashplatz gemappt werden, weil nämlich
diese 3 Bits, die wir als Index genommen haben, identisch sind.
Das heißt, bei der Strategie wird folgendes gemacht, alle Adressen,
die den gleichen Indexteil haben, werden in die gleiche Cashline
gemappt.
Das ist die Grundidee hinter dem Diary-Map-Cash.
Das heißt also, der Indexteil einer Adresse legt fest, in welche Zeile
kann so ein Eintrag kommen.
Und nur in diese Zeile kann er kommen.
Das heißt also hier, 3 mal die 0 würde bedeuten, einzige Möglichkeit
hier ist der unterste Eintrag.
Wenn hier 3 mal die 1 stünden würde, einzige Möglichkeit ist der
oberste Eintrag.
Das heißt also, ein bestimmter Teil der Adresse, ein Indexteil, legt
fest, wohin kommt der entsprechende Cash-Eintrag, indem man einfach
diesen Indexteil in einem Decoder, über einen einfachen Decoder,
entsprechende Zeile auswählt.
Natürlich muss ich dann auch noch das Restige Tag dazu speichern, weil
ich muss ja nur feststellen, an der Stelle steht jetzt wirklich der
Wert da oder steht er nicht da.
Das heißt also, ich muss natürlich die restlichen Bits auch noch
vergleichen.
Ja, nur zum Beispiel, wenn ich jetzt hier nach meiner Zeile, was weiß
ich, nach der Adresse, ich mache mal ein drittes Beispiel, ich suche
nach der Adresse, 010000000, ja, 010000, dann hat die zwar hier den
gleichen Indexteil, das heißt also, sie würde entsprechend auch auf
diese unterste Cash-Line zeigen, in der ja was drin steht, aber sie
hat hier vorne nicht den Wert, den der oder der hat.
Das heißt, wir hätten keinen Cash-Hit, obwohl natürlich in dem Eintrag
was steht, steht hier eine andere Cash-Zeile drin.
Das heißt, ich muss also neben dem Index, muss ich auch immer noch den
Tag -Teil vorne mit abspeichern, hier, um eben festzustellen, habe ich
jetzt wirklich einen Treffer oder steht in der gleichen Zeile einfach
nur ein anderer, oder ein anderer der vielen möglichen Blogs, weil in
jedem dieser Blogs können jetzt sehr, sehr viele Wörter aus dem
Hauptspeicher stehen.
Ich denke, das kann man noch ein bisschen deutlicher machen, wenn man
einfach das analoge Bild von vorhin her nimmt und jetzt mal auf die
Direct Cash-Zeile beweisen.
Hier habe ich noch mal meinen Adressspeicher, ich habe meinen
Datenspeicher, ich habe meine Adresse, die untersten 4 Bit sind nach
wie vor jetzt zur Wort- und zur Byte-Auswahl.
hier insgesamt 16 Cache-Lines.
Na gut, wenn ich 16 Cache-Lines habe, dann brauche ich 4 Bit als
Index, um die anwählen zu können.
Das heißt, ich nehme hier die nächsten 4 Bits als Index und dann
bleiben wir insgesamt noch 24 Bit als Tag.
Sie erinnern sich, beim Full-ASCII im Cache hatten wir 4 Bit
Wortauswahl und die vollen 28 Bit als Tag.
Hier haben wir 4 Bit Wortauswahl, 4 Bit Index und 24 Bit
vergleichendes Tag.
Das heißt, wenn ich jetzt ein Wort, hier eine bestimmte Adresse habe,
bestimmen diese 4 Bits eben die Bits von 3 bis 7 in diesem Fall, in
welche Zeile wird es geschrieben und die oberen 24 Bit werden dann in
den Tag-Speicher abgelegt.
Machen wir also mal ein Beispiel.
Nehmen wir an, wir haben hier die Adresse 101 und so weiter, und hier
Index 001 und hier 0000, dann würde dieser Index 001 bedeuten, hier
wir wählen die Zeile 1 aus, das heißt wir würden diesen Eintrag hier
in die Zeile 1 schreiben, würden den vorderen Teil der Adresse eben
diese 01 und so weiter bis 11, also diese 24 Bit würden wir hier als
Tag ablegen.
Würden wir nochmal als Value kennzeichnen hier.
Gut, wenn wir jetzt wissen wollen, wir haben eine andere Adresse,
wollen wissen, ob wir hier einen Cache-Hit haben, dann gehen wir her,
nehmen den Index, wählen das entsprechende Index-Feld aus und
vergleichen das Tag-Feld mit dem Tag-Feld da oben.
Das heißt also, wenn wir jetzt hier irgendwo die Adresse als Adresse,
eine andere Farbe machen, z.B.
hier die Adresse 100 und so weiter, hier heißt es identisch 110010000
haben, dann würden wir hier einfach hergehen, auch hier haben wir den
Index 001, das heißt wir haben in diesem Fall immer, entschuldigen
Sie, das ist ein bisschen zu wenig, muss ein 4 sein, Entschuldigung,
0001 und hier haben wir auch nochmal ein 4.
rein.
Ja, passt rein.
Würden also im Prinzip die gleiche Zeile nehmen, würden hier das Tag
-Feld hernehmen, würden vergleichen, ist das hier das gleiche Tag
-Feld, in diesem Fall wäre es nicht das gleiche Tag-Feld, das würde
bedeuten, wir haben einen Cache-Miss.
Wenn wir hier das gleiche Tag-Feld hätten, dann hätten wir hier einen
Hit und wüssten, wir hätten einen Treffer.
Und Sie sehen, durch diese Technik müssen wir jetzt im Prinzip nur
noch in einer einzigen Cache-Line vergleichen, das heißt wir brauchen
einen einzigen Vergleicher, nicht mehr.
Das macht also den Cache deutlich unaufwendiger, das heißt wir haben
einen extrem geringen Hardware-Aufwand für die Adressierung, da wir
nur einen Vergleicher für alle Tags brauchen.
Wir haben eigentlich den Nachteil, dass wir eine ständige Konkurrenz
der Blöcke haben, das heißt also zwei Blöcke, die das gleiche Index
-Feld haben, werden immer in die gleiche Cache-Line eingetragen.
Und wenn der ganze Cache leer ist, ist völlig egal, wenn nur ein
einziger Eintrag in diesem Cache steht und ein neuer Eintrag kommt,
der den gleichen Index hat, wie dieser einzige Eintrag, der schon im
Cache steht, dann wird der verdrängt, auch wenn der ganze sonstige
Cache noch leer ist.
Das heißt, wir haben hier, was ich vorhin erwähnt habe, wir brauchen
hier keine Verdrängungsstrategie, es gibt hier keine
Verdrängungsstrategie notwendig, weil ich keine Auswahl habe.
Jeder Block wird immer genau in eine Line geschrieben.
Das heißt, wir können die Merkmale des DirectMap-Caches wie folgt
zusammenfassen.
Wir haben eine sehr einfache Hardware-Realisierung, das heißt nur
einen Vergleicher, nur einen Tag-Speicher.
Der Zugriff ist sehr schnell, weil ich das Tag-Feldbank parallel mit
dem Block schon mal lesen kann.
Ich brauche keine Verdrängungsstrategie oder keinen
Verdrängungsalgorithmus, weil einfach die Strategie lässt keine
Alternativen zu.
Wir haben aber den Nachteil, dass eben, wenn es selbst an anderer
Stelle im Cache noch platzfrei ist, wegen der direkten Zuordnung
verdrängt wird.
Und das kann im schlimmsten Fall zu sogenannten Seitenflattern oder
Trashing führen, wenn ich eben unglücklicherweise immer auf Adressen
zugreife, hintereinander, die immer das gleiche Indexfeld haben.
Wenn mir das ungeschickterweise passiert, dann werden die immer
gegenseitig verdrängt, das heißt, mein Cache funktioniert überhaupt
nicht.
Er wird sich ständig ein- und auslagern.
Das heißt also, hier hat der DirectMapCache sicher seine Schwächen,
die sich insbesondere dann bemerkbar machen, wenn der DirectMapCache
sehr klein ist.
Es gibt Untersuchungen, die eben zeigen, je größer mein Cache wird,
desto geringer wird der Unterschied zwischen voll-assertivem und
DirectMapCache.
Also bei kleinen Caches ist der Vollassertive sicher im DirectMapCache
ausdrücklich überlegen.
Je größer ich meinen Cache mache, wird einfach durch die größere
Auswahl, die ich habe, kommt der DirectMapCache näher an den
Vollassertiven Cacher ran, wobei der Vollassertive Cache ab einer
gewissen Größe einfach nicht mehr rentabel ist.
Das heißt, man kann jetzt noch eine Fragestellung, kann ich denn auch
irgendwie einen Kompromiss machen zwischen beiden, zwischen
vollassertivem und DirectMapCached?
Und das kann man und der Kompromiss heißt Satzassertiver Cache.
Die Idee ist folgende, ich nehme einfach einen DirectMapCache her,
lasse aber jetzt für jeden Eintrag nicht nur ein Feld zur Auswahl,
sondern ein paar Felder.
Also im einfachsten Fall, wie hier angedeutet, wäre das ein 2-Weg
Satzassertiver Cache, weil ich hier für jeden Block nicht über einen
DirectMapCache einen Eintrag reserviere, sondern zwei.
Ich organisiere meinen Cache also in sogenannte Sets, in Sätze, und
jeder Satz kann in diesem Fall zwei Blöcke aufnehmen und ich ordne
jetzt einen Block im Hautspeicher, nicht einen Block im Cache zu,
sondern ich ordne ihm einen Satz zu.
Das heißt, der Block 0 kann nur hier oder hier sein, der Block 32 kann
nur hier oder hier sein, der Block 64 kann nur hier oder hier sein,
der Block 1023 kann nur hier oder hier sein.
Das heißt, ich erhöhe einfach die Auswahlmöglichkeit, indem ich jetzt
nicht über einen DirectMapCache einen Speicherblock, immer genau einen
Eintrag im Cache zuordne, sondern ich lasse ihm die Wahl zwischen
mehreren, zum Beispiel hier zwei.
Das heißt, wir fassen einfach mehrere Cachezeilen zu Sätzen oder Sets
zusammen und kriegen somit im Prinzip einen Kompromiss zwischen
Vollassertiv und DirectMapped.
Entsprechend hier, das ist das Bild.
Ich habe also jetzt zum Beispiel bei einem Zweiwegsassertiven Cache
jetzt nur noch einen Index von drei Bits, weil ich habe nämlich jetzt
immer noch 16 Cache-Lines.
Ich habe die Cache-Lines aber immer zu Sätzen A2-Lines
zusammengefasst.
Das heißt, insgesamt habe ich acht Sätze in meinem Cache noch stehen.
Ich habe also jetzt hier acht solche Sätze stehen.
Und das Indexfeld muss jetzt nicht mehr nur auf ein bestimmtes Feld
auswählen, sondern es muss im Prinzip nur noch einen Satz auswählen.
Das heißt, es muss nur noch zwischen einem von acht Sätzen auswählen.
Das heißt, ich komme hier mit drei Bit-Index aus, habe dann ein Bit
mehr Tag und kann jetzt immer dann, wenn ich einen Wert hier in den
Cache schreibe, wählt mir der Index einen Satz aus und jetzt habe ich
dann die freie Wahl, an welches Feld im Satz ich ihn schreibe.
Das heißt, ich bringe hier mehr Wahl.
In dem Fall hätte ich jetzt zwei Möglichkeiten.
Ich könnte auch vier Möglichkeiten, acht Möglichkeiten machen.
Und man sieht hier, der Kompromiss ist deswegen, wenn ich hier pro
Satz nur eine einzige Line zur Verfügung stellen würde, dann hätte ich
den Direct Map Cache.
Wenn ich hingegen sage, ich setze nur einen einzigen Satz ein, dann
habe ich den vollassertiven Cache.
Und über diese Gruppenbildung kann ich im Prinzip den beliebigen
Kompromiss machen.
Je größer ich meinen Satz mache, desto näher komme ich an den
vollassertiven Cache und desto weiter weg komme ich vom Direct Map
Cache.
Das heißt, man kann im Prinzip als anderes Bild sich auch vorstellen,
dass ein N-Weg Satzassertiver Cache einfach eine Parallelschaltung aus
N Direct Map Caches ist.
Also zum Beispiel bei hier, bei zwei, hätte ich einfach zwei Direct
Map Caches parallel geschaltet und ich habe hinten eine Auswahllogik,
die mir sagt, entweder nach links oder nach rechts.
Ich habe die freie Auswahl.
Entsprechend bei einem 4-Weg Satzassertiven Cache hätte ich dann 4
solche Direct Map Caches parallel, 8 Direct Map Caches parallel und so
weiter.
Dadurch kriege ich eben eine verbesserte Trefferquote, weil ich eben
jetzt die Auswahlmöglichkeit habe.
Und bei einem N-Weg Satzassertiven Cache kann ich immerhin zwischen N
Einträgen auswählen, wo ich einen Eintrag einbringen möchte.
Das heißt, ich bin nicht mehr so extrem festgelegt, sondern habe etwas
mehr Flexibilität.
Ich brauche vor allen Dingen jetzt auch eine Verdrängungsstrategie.
Ich muss also dann wissen, welchen, wenn ich die Auswahl habe,
verdränge ich.
Und hier kann ich einfach sagen, zyklisch, also irgendwas FIFO, ich
gehe einfach zyklisch vor.
Wenn ich mal einen 4-Weg habe, dann mache ich 1, 2, 3, 4, 1, 2, 3, 4,
1, 2, 3, 4.
Ich zähle einfach weiter.
Ich kann einen Zufallsgenerator benutzen.
Bringt gar nicht so schlechte Erfolge, wenn ich zufällig ersetze.
Kriegt relativ gute Ergebnisse.
Oder ich kann auch hier Least Reason to Use oder irgendwas benutzen.
Das heißt, was ich jetzt hier brauche, ist im Prinzip, ich habe Sätze
und in jedem Satz habe ich N Cashlines.
Und das heißt, ich brauche zum Vergleich auch N Vergleicher.
Weil jetzt mir der Index mir im Prinzip einen Satz auswählt.
Dieser Satz enthält N Lines bei einem N-Weg Satz, bei einem Cash und
den muss ich parallel in einem Taxo vergleichen.
Das heißt, ich brauche N Vergleicher.
Das bedeutet, bei einem 2-Weg Satz, bei einem Cash, brauche ich also 2
Vergleicher.
Bei einem 4-Weg Satz, bei einem Cash, bräuchte ich 4 Vergleicher.
Insofern ist es also ein Kompromiss.
Der Direct Map Cash benötigt einen Vergleicher.
Der 2-Weg Satz als Assessiven Cash benötigt 2 Vergleicher.
Der 4-Weg benötigt 4 Vergleicher.
Je mehr, je größer ich meinen Satz mache, desto mehr Vergleicher
brauche ich.
Bis ich schließlich alle Cashlines in einen einzigen Satz
untergebracht habe, dann habe ich den voll Assessiven Cash.
Ich brauche für jede Line einen Vergleicher.
Hier eben nochmal ein Beispiel der Organisationsform.
Wenn ich den Direct Map Cash habe, dann habe ich im Prinzip überhaupt
keine Wahl.
Jeder Block ist an eine Stelle gebunden.
Das heißt, ich muss nur an einer einzigen Stelle suchen.
Wenn ich einen Satz Assessiven Cash habe, dann muss ich an so vielen
Stellen suchen, wie groß mein Satz ist.
Also zum Beispiel hier bei 2, einem Satz der Größe 2, kann mein Datum
an zwei verschiedenen Stellen stehen.
Beim voll Assessiven Cash kann mein Datum an jeder beliebigen Stelle
im Cash stehen.
Ich muss also überall suchen.
Das sind letztendlich nochmal die Unterschiede in der Organisation.
Gut, hier noch ein paar Begriffe zum Schluss.
Also wir haben ja schon von Blocken gesprochen.
In einem Cash spricht man auch von einem sogenannten Blockrahmen.
Ein Blockrahmen ist im Prinzip einfach jetzt eine Menge von
Speicherplätzen, die jetzt in der Lage sind, einen Block aufzunehmen,
ein Adressetikett und die zugehörigen Status-Bits.
Die Blocklänge oder auch die Länge der Cashline gibt eben die Anzahl
oder die Größe des Datums an, die ich in einem Block abspeichern kann.
Üblicherweise 16, 32, 64 Bytes.
Gut, und diesen Blockrahmen kann ich jetzt eben noch in Sätze
unterteilen, in sogenannte Sets, wie wir das eben gesehen haben beim
Satz Assessiven Cash.
Und hier kriegen wir jetzt eben nochmal in mathematischen Formeln
ausgedrückt den Zusammenhang, den ich versucht habe vorhin verbal zu
erklären, dass eben der Satz Assessive Cash im Prinzip ein Kompromiss
ist, den ich jetzt mit dem Wählen von N in beide Richtungen drücken
kann, weil dieses N nennt man auch noch die Assessivität, also die
Anzahl der Blocks oder Blockrahmen pro Satz, die bezeichnet man als
Assessivität N.
Und die Gesamtkapazität kann ich natürlich dann berechnen, indem ich N
mit S multipliziere, wobei S die Anzahl der Sätze ist, also Anzahl der
Sätze mal Anzahl der Blöcke pro Satz gibt die Gesamtkapazität.
Das ist, denke ich mal, relativ offensichtlich.
Und ich kann jetzt einfach anhand von diesen beiden Parametern S und
N, also Anzahl der Blöcke pro Satz oder Assessivität und Anzahl der
Sätze, kann ich die bei drei Cash-Typen nochmal unterscheiden.
Bei einem vollassessiven Cash bedeutet das nämlich, dass der Cash
besteht aus einem einzigen Satz und dieser Satz enthält alle Blöcke,
das heißt das S ist gleich 1 und das N ist gleich C in dem Fall.
Das heißt ein einziger Satz dieser Satz enthält alle Blöcke, die
komplette Kapazität des Speichers.
Ich brauche entsprechend auch N gleich C Vergleicher.
Der direkt abgebildete Cash ist genau das Umgekehrte.
Hier habe ich im Prinzip S Sätze und jeder Satz enthält nur einen
einzigen Block.
Die Assessivität des Cashes ist 1, ich habe keine Auswahl.
Jeder einzelne Satz enthält nur einen einzigen Block, dafür brauche
ich auch nur einen einzigen Vergleicher.
Na gut, in der N-Fach-Satz-Assessivität ist im Prinzip der Kompromiss
dazwischen.
Jeder andere Wert für S und N ist eben Satz-Assoziativ.
Das heißt damit kann ich eben zum Beispiel wählen N gleich 2, S gleich
C halber wird der 2-Weg-Satz-Assessiv-Cash, N gleich 4, S gleich C
viertel wird dann der 4-Weg-Satz-Assessiv-Cash und so weiter.
Wobei einfach der Hardware-Aufwand steigt mit zunehmendem N, weil N
eben auch die direkte Anzahl der Vergleicher ist, die ich benötige.
Die Flexibilität steigt aber auch gleichzeitig mit zunehmendem N, weil
je mehr N ich habe, desto höher die Assessivität ist, desto mehr
Möglichkeiten habe ich beim Einlagern und beim entsprechenden
Verdrängen.
Gut, das soll es zum Thema Cash gewesen sein.
Am nächsten Donnerstag findet eine Übung zu dem Thema statt.
Das war's für heute, danke.