kommen zur letzten Vorlesung, die ich dieses Semester übernehme.
Nächste Woche ist Andreas dann wieder da. Und wir waren dabei uns,
nachdem wir hoffentlich jetzt einen Eindruck haben,
wie tatsächlich Programme im Prinzip auf dem Rechner ausgeführt werden,
waren wir dabei, uns eine Sache anzuschauen,
die in allen aktuellen Prozessoren tatsächlich verbaut ist,
die sogenannte Befehlspipeline.
Und die Idee ist halt, dass wir die Ausführungszeit beschleunigen.
Dieses hier war die Stelle, an der ich aufgehört habe,
aber ich will doch nochmal ein paar Folien zurückgehen,
um die Motivation wieder zu zeigen.
Und auch nochmal darauf hinzuweisen, aus dem letzten Teil
der Vorlesung kommt traditionell selten irgendwie was in die Klausur.
Diese Sache hier hat es aber schon zweimal in die Klausur
geschafft, mit mehr oder weniger katastrophalen Resultaten.
Das ist aber essentiell wichtig, von daher will ich
das nochmal kurz erläutern. Hier geht es darum,
dass wir halt ein Schaltnetz haben und anschließend ein Schaltwerk,
ein Register und wir wollen hier irgendwas berechnen und wir wollen das möglicherweise beschleunigen.
Gut, die Komplexität hier, die kriegen wir natürlich nicht weg,
wenn wir das vereinfachen könnten, dann hätten wir das lange getan.
Im Beispiel haben wir jetzt hier halt hier ein Schaltnetz,
das irgendwas berechnet, sagen wir mal eine Addition und
es dauert 300 Pikosekunden
Und dann wird das in ein Register hineingespeichert und die Durchlaufzeit
für das Register, die Schallzeit ist halt 20 Pikosekunden.
Dann kann man sich leicht überlegen, dass vom Zeitpunkt,
wo die Eingabedaten hier anliegen, bis zu dem Zeitpunkt,
wo wir die Daten hinten im Register fertig gespeichert haben,
dauert es halt einmal hier durch und dann durch das Register durch.
Also die Summe davon, 320 Pikosekunden. Wenn man sich
jetzt überlegt, wie oft man das pro Sekunde machen kann,
dann ist das halt der Durchsatz.
Es roupert eins durch die Zeit, die da vergeht.
In diesem Fall dann halt eins durch 320 mal 10
minus 12 Sekunden. Und das wären dann drei Milliarden Operationen
pro Sekunde
Nicht schlecht, aber es geht besser. Und die Idee war halt,
dass wir die gesamte Logik aufteilen in mehrere einzelne Teile,
sogenannte Stufen. Und in diesem Fall ist das jetzt hier
gleichmäßig verteilt worden. Wir brauchen jetzt allerdings zusätzliche Register.
Und deswegen ist die Durchlaufzeit für die ersten Daten länger geworden,
weil die Logik ist im Wesentlichen gleich geblieben,
aber die Register sind dazu gekommen und deswegen dauert es jetzt 360 Pico-Sekunden.
Der Durchsatz dieser Scheidung ist aber jetzt nicht mehr eins durch 360 Pico-Sekunden,
sondern eins durch die längste Stufe, die wir hier haben.
Also dann eins durch 120 Pico-Sekunden und das bedeutet,
dass wir jetzt in der Größenordnung 8 Milliarden Operationen pro Sekunde ausführen können.
Wenn wir keine zusätzlichen Abhängigkeiten haben, das werden wir uns gleich anschauen.
Das geht natürlich im allgemeinen Fall nicht so schön,
sondern diese Aufteilung wird nicht ganz gleichmäßig sein.
Und deswegen dann hier nochmal dieser Hinweis auf diese Schaltung hier,
der 300-Pico-Sekunden-Block ist jetzt in drei Teile zerlegt worden,
aber einer ist ein bisschen länger als die anderen.
Und deswegen ergibt sich jetzt hier in den Gesamtverzögerung von 510 Pico-Sekunden
hier. Und der Durchsatz 5,88 Milliarden Operationen pro Sekunde
Hier nochmal die Frage, ist jedermann klar,
warum das Delay jetzt 510 Pico-Sekunden ist und nicht 360?
Nicht deine einzige Meldung. Naja gut,
also die Register hängen jetzt hier alle an dem gleichen Takt.
Wir müssen den Takt also so langsam machen,
dass die langsamste Stufe gerade noch mitkommt, dass es dann halt diese hier.
Und deswegen haben wir halt für die langsamste Stufe 150 plus 20 Pico-Sekunden 170.
Und diese Stufen müssen entsprechend, obwohl sie schneller rechnen könnten,
genauso langsam getaktet werden.
Deswegen also dreimal 170 Pico-Sekunden
Falls irgendeine Frage zu Pipeline kommt, dann vermute ich diese hier.
Deswegen empfehle ich sich das nochmal anzuschauen. Gut, und
die Idee war jetzt eben, dass wir das tatsächlich nutzen.
Wir haben jetzt unser Schaltnetz ersetzt durch die einzelnen Phasen des von Neumann-Befehlszyklus,
also Befehl holen, Operanten holen, berechnen,
möglicherweise den Speicher zugreifen und dann die Resultate wieder in den Register zurückschreiben.
Und statt das,
wie man das bei den ersten Prozessoren gemacht hat,
alles nacheinander zu tun und dann den nächsten Befehl zu starten an dieser Stelle,
werden wir jetzt halt versuchen,
das ineinander zu verschachteln,
indem wir halt Pipeline bauen
Und in dem Moment, wo die erste Instruktion hier ihre
Instruktion geholt hat und im zweiten Tag dann ihre Operanten holt,
können wir im zweiten Takt eben schon den zweiten Befehl starten.
Im dritten Takt berechnet die erste Befehl seine Operation.
Der zweite holt seinen Operanten und der dritte wird gestartet.
Das war die Idee. Und die klassische Pipeline,
die man auch relativ einfach verstehen kann, das ist dann halt diese.
Das ist eine fünfstufige Pipeline,
wo halt jetzt letztlich fünf Takte vergehen bis eine erst,
bis die erste Operation hier durchgelaufen ist.
Und der Witz an der Sache war,
dass der Hardwareaufwand für den Prozessor
sich kaum vergrößert hat
All die teuren Funktionseinheiten, also insbesondere die Speicher, die Aluis und die Register.
Und was wir sonst noch so an Funktionseinheiten haben,
hier den Adira für die Fortschreitung des Befehlssteelers, das brauchen wir sowieso.
Das Einzige, was jetzt dazugekommen sind,
sind an dieser Stelle vier Register,
die halt die Zwischenergebnisse der einzelnen Stufen speichern und entsprechend verwalten.
Das war halt die Idee. Also wir können im Prinzip,
wenn wir das so bauen,
im Prinzip bis zu etwa die fünffache Performance dadurch gewinnen,
dass jetzt halt der gesamte Befehlszyklus in fünf Stufen zerlegt ist
und wir im Prinzip immer fünf Instruktionen gleichzeitig in Bearbeitung haben
So, die Frage ist natürlich,
funktioniert das jetzt auch einfach, wenn wir das so bauen?
Also aus Sicht der Hardware natürlich schon, aber wie wirkt
sich das auf die Software aus? Ein paar Begriffe,
einzelne Stufe der Pipeline nennt man dann eben auch Stage,
vom Englischen her und den Pipeline Psychle oder Machine Cycle ist immer dann,
wenn halt eine Instruktion eine Stufe in der Pipeline weiterkommt,
ist Rupot oder Durchsatz die Anzahl der Instruktionen, die abgeschlossen werden.
Also im Idealfall eine Instruktion pro Takt und dann entsprechend ein höherer Takt,
als wir bisher hatten,
weil wir halt nicht mehr die gesamte Operation auf einmal
berechnen,
sondern nur einen Tiger baut
Und die Latenz ist dann eben die Zeit, die eine Instruktion benötigt,
um komplett durch den Prozessor durchzulaufen, also bearbeitet zu werden.
Kurze Zusammenfassung, welche Vor- und Nachteile wir erwarten. Zunächst mal,
wir haben die komplizierte Gesamtoperation von Neumann-Befehlszyklus in kleinere Blöcke aufgeteilt.
Wir werden also einen höheren Takt erwarten und im Idealfall
werden wir pro Takt einen neuen gefehlt starten können,
einen Gefehl abschließen können.
Das heißt, wir haben einen höheren Durchsatz und insgesamt eben
bessere Performance des Programms,
weil halt mehr Instruktionen pro Zeit
abgearbeitet werden
Hardwareaufwand hält sich bisher noch in Grenzen und so wie wir das bisher gemacht haben,
sieht eigentlich der Prozessor, der Programmierer sieht gar nicht,
ob der Prozessor eine Pipeline nutzt oder nicht.
Das werden wir dann gleich sehen, dass man das gegebenenfalls
doch sieht oder dass eben die Hardware entsprechend kompliziert wird. Was auch klar ist,
die Ausführungszeit für einen einzelnen Befehl wird nicht verbessert, sondern wird ein bisschen verschlechtert,
weil wir halt die zusätzlichen Register dazwischen haben und der Takt,
mit dem wir das Ganze betreiben können,
ist halt limitiert durch die langsamste Pipeline-Stufe.
Wenn wir dann also so eine unausgewogene Situation haben,
wie eben 50, 150 100,
dann ist halt die 150er die langsamste Stufe,
die maßgebliche für den Takt.
Und wir werden halt versuchen müssen, die einzelnen Teiloperationen möglichst
gleich schnell zu bekommen. Und natürlich das Ganze funktioniert nur dann,
wenn wir in jedem Takt auch wirklich eine Instruktion nachfüllen können.
Ansonsten bleiben Funktionseinheiten unausgelastet und wir gewinnen eigentlich nichts. So,
und das kann man jetzt auch noch mal ganz kurz mathematisch
sich anschauen, was man im allgemeinen Fall erwarten kann.
Wenn wir zum Beispiel jetzt K-Pipeline-Stufen haben, in dem Beispiel
noch den Folien, ist K eben meistens fünf.
Und eine Anzahl von In- und Struktionen
berechnen wollen
Gut. Ohne die Pipeline läuft das dann im N-Mahl-Krahl-Taktzyklen ab,
weil wir halt einfach eine Instruktion da reinschieben,
ganz durchlaufen lassen und erst dann die nächste Instruktion starten.
Mit der Pipeline reduziert sich das, weil wir halt Endinstruktionen starten,
den ersten Takt brauchen wir sowieso und dann haben wir noch
karminus 1 Stufen der Pipeline übrig, die dann jeweils benutzt werden.
Das sieht also schon mal ganz spannend aus.
Wir können also ein Speed-up, eine Beschleunigung erwarten,
um den Faktor N-Mahl-K durch K plus N.
Die 1 können wir natürlich im Grenzen wird,
dass die Anzahl der Instruktionen groß ist, dann schon mal weglassen.
Dann kürzt sich das K raus. Und nee,
dass der NSM kürzt sich raus, sorry.
Und dann haben wir halt einen erwarteten Geschwindigkeitszuwachs unseres Programms,
um den Faktor K. mehr oder weniger.
Und damit das sich lohnt, müssen wir halt zwei Sachen haben.
Erstens, dann muss die Pipeline eben möglichst viele Stufen haben.
Und zum zweiten gilt dieser Grenzwert hier, nur dann,
wenn das N wirklich gegen unendlich geht.
Das heißt, wenn wir wirklich lange Sequenzen von Instruktionen haben
In der Praxis wird man das nicht erreichen. Und was
hier in dieser Formel auch vernachlässigt wurde, ist natürlich die Durchlaufzeit durch die zusätzlichen Register.
Wir hatten bereits gesehen, dass wenn man ganz,
ganz kleine Schaltnätze baut und dann immer ganz viele Register dazwischen schaltet,
dass das dann letztlich dann durch die Durchlaufzeit,
durch die Register limitiert wird, also diese Formel ist nur eine Näherung,
sie berücksichtigt halt den Overhead für die Register gar nicht.
So, das kann man dann auch plotten,
also in dem Beispiel jetzt hier für zehn Pipeline-Stufen und dann die Anzahl der Befehle,
die da durchlaufen, von unten nach oben der Speedup,
also Verbesserungsfaktor und Ja, das weiß ich,
nehmen wir mal an, wir haben jeweils 100 Befehle,
die da durchlaufen können, dann erreichen wir nicht ganz zehn,
sondern in diesem Fall immerhin schon mal ein Faktor 9.
Das ist also schon mal ganz ordentlich. Eine kleine Übersicht,
wie sich das dann historisch entwickelt hat bei verschiedenen Prozessoren,
von verschiedenen Herstellern, der 386er,
seit der Prozessor,
den wir für die Assembler-Aufgaben immer genommen haben,
der 32 mit Inkelbefehlsalz,
der war halt noch ohne Pipeline und
relativ magere Taktfrequenz von 33 Meterherz
Der Kens Jung war dann so der erste Intel-Prozessor mit einer Pipeline,
die mit dieser hier,
die wir in den Folien haben, vergleichbar ist.
Der hat dann fünf Pipeline-Stufen, der kam natürlich ein paar
Jahre später und die Taktfrequenz war dann schon deutlich höher.
Also hier erstmal ein Faktor 10 in der Taktfrequenz und das
resultiert halt zum einen aus der Verwendung der Pipeline und zum anderen aus Verbesserung der Herstellungstechnik zwischen diesen,
das waren letztlich drei Generationen von Prozessoren und zusätzlich dann eben einen Gewinn dadurch,
dass wir fünf Pipeline-Stufen haben,
statt eben ohne Pipeline
Dann hat sich das so ein bisschen weiterentwickelt. Also derzeit
sind wir hier, was weiß ich, bei den Core-I-Prozessoren,
die Taktrate im Bereich 3 bis 5 Gigahertz ist immer noch aktuell.
Da hat sich technologisch ein Limit ergeben. Das ist ja
derzeit ökonomisch nicht verbessert werden kann. Und die Zahl
der Pipeline-Stufen ist halt immer weiter gewachsen. Also das,
was wir uns hier in den Folien angucken,
ist nur die allererste Einführung, weil das so ein Design ist,
was man noch einigermaßen plausibel verstehen kann.
Die tatsächlichen Prozessoren heute,
die sind halt weitaus komplizierter und wir werden gleich sehen,
dass halt eine zunehmende Anzahl der Pipeline-Stufen tatsächlich da
einige Sachen berücksichtigt werden müssen
Bisschen unschön sind. Gut, kurze Zusammenfassung,
wenn man Pipeline einbauen will, was ist dann aus Sicht
des Prozessorentwerfers, des Architekten gut, was ist schlecht?
Ja, das wird jetzt niemanden verwundern. Die originalen ursprünglichen Risk-Prozessoren
sind halt speziell für die Verwendung von Befehlspipelines entwickelt worden.
Dann liegt es nahe, dass die halt auch gute Eigenschaften haben.
Eine Sache ist halt die Frage der Befehlskodierung und wenn alle Befehle gleich lang sind,
dann kann man halt immer einen Befehl aus dem Speicher holen, bequem in diese Pipeline hineinschieben
Und je weniger Komplexität in diesen verschiedenen Befehlsformaten drin ist,
umso besser. Und wir werden auch gleich sehen,
dass Speicherzugriffe halt ein Flaschenhals sind, also eine Load-Store-Architektur,
wo die meisten Operanten aus Registern kommen,
ist halt ebenfalls gut für das Pipelining.
Hier nochmal die Zusammenfassung der fünf Befehlsformate bei dem MIPS-Prozessor,
die halt ziemlich regulär sind, alle 32 Bit lang.
Alle haben hier oben den Obcode und feste Einteilung in diese
verschiedenen Felder, um die Register zu adressieren.
Wenn man das halt nicht macht,
sondern so eine Komplex-Instruction-Set-Architektur hat, ist das halt schon mal
schlecht für Pipeline
Die nächste Sache, die schlecht ist, sind die sogenannten Hazards oder Konflikte.
Und das ist also jetzt die gleiche Vokabel Hazard,
wie wir das schon bei den Schaltnetzen und den Laufzeiten gesehen haben.
Aber jetzt eine andere Verwendung. Das bezieht sich jetzt also
auf Konflikte, die da auftreten können.
Und wir unterscheiden drei Stück, die wir uns gleich anschauen.
Strukturkonflikte, mal gleichzeitig mit Zugriff auf eine Ressource.
Da haben wir jetzt halt mehrere Befehle gleichzeitig in Erarbeitung sind,
können auch mehrere Befehle gleichzeitig irgendwas machen wollen.
Datenkonflikte, wenn halt Befehle in den Bearbeitungen sind und
die Ergebnisse von diesen Befehlen noch nicht fertig sind,
aber schon benötigt werden
Zum letzten, die Steuerkonflikte, Control Conflicts, Control Heasards,
insbesondere eben bei Sprunggefährden. Und dieses hier ein ganz,
ganz schlecht, das wird im Moment noch nicht ganz klar,
weil wir halt Interrads und System Calls und sowas
in der Vorlesung noch gar nicht besprochen haben.
So, das ist jetzt das gleiche Schema, aber so ein bisschen vereinfacht,
also jetzt runtergebrochen auf die absolut minimalen Funktionseinheiten in unserem Prozessor.
Und jede Zeile entspricht jetzt einem Befehl.
Das ist jetzt der MIPS-Assembler, das heißt,
hier ist ein Befehl,
der sagt Load Word
Wir adressieren den Speicher mit dem Inhalt des Registers 1,
ADN20 da drauf und packen das in das Register 10. So,
wenn das gestartet ist,
im ersten Takt wird halt dieser Befehl hier aus dem Speicher geholt,
anschließend dekodiert, in der wird dann seine Operanten holen,
im nächsten Takt dann Berechnung durchführen und hier schließlich den Wert,
also hier den Speicherzugriff machen und dann hier den Wert in das Register zurückschreiben.
Der wird also im ersten Takt gestartet,
im zweiten Takt holt der seine Operanten aus den Registern und während das passiert,
können wir schon mal den nächsten Befehl hier aus dem Speicher holen und was da jetzt drin steht,
als Beispiel ist eine Subprojektion Register 2,
Minus Register 3, nach Register 11.
Das ist also die MIPS 3 Operantenkonvention hier. Hier ist
das Zielregister halt vorne. Gut, der nächste Befehl ist
deine Addition. Register 3 plus Register 4, nach Register 12.
Dann holen wir nochmal Wert aus dem Speicher und Svenes Register
13. Wir nehmen wieder das Register 1 als Badesadresse und jetzt addieren wir
mal 24 drauf statt oben 20 und zum Schluss nochmal eine Additionsbefehl.
So, und das würde jetzt also brav funktionieren. Wir sehen schon,
dass wir, was weiß ich hier, das Register 1 lesen,
hier nochmal das Register 1 lesen und ansonsten haben wir halt hier die Zielregister aufsteigend, durchnummeriert.
Da sind also keinerlei Konflikte. Das würde also jetzt eine Situation sein,
die diesem N mal K durch N plus K entspricht.
Das heißt, wir haben im Wesentlichen eine Beschleunigung durch Umbann Faktor 5,
weil wir halt in jedem Takt eine neue Instruktion starten können.
Das ist also der Fall, wo es gut geht.
Und in den Folien sind jetzt Teilen die Fälle,
wo es zu Problemen führt, in Rot markiert,
da das hier nicht der Fall ist, ist,
dass so weiter alles in Ordnung.
Eine Sache, die vielleicht schon mal auffällt, ist,
dass jetzt hier die Speicherzugriffe so unter den Teppich gekehrt worden sind.
Hier steht nämlich schon mal IM drin und da DM
Steht natürlich für Instruction Memory und Data Memory, damit das funktioniert,
müssen wir halt hier einen Befehl aus dem Speicher holen.
Hier ein Befehl aus dem Speicher holen, hier ein Befehl aus dem Speicher holen.
Hier laden wir den Inhalt von dieser Speicherstelle in das Register 10.
Das heißt, hier mein Speicherzugriff und gleichzeitig möchten wir aber hier ein Buffet holen.
Da wir also tatsächlich schon die Situation vorhanden,
dass wir einen Konflikt haben,
das umgeht man bei den meisten Pipeline-Prozessoren dadurch,
dass wir halt einen Teil des Speichers separat für die Instruktion reservieren.
Im 11 Dateiformat wird das halt dieser Dieses Textsegment,
was halt hier in den Instruction-Speicher geladen wird und die Datenoperationen folgen aus dem Rest der Speichers.
Das werden wir dann in der letzten Woche der Vorlesung sehen,
wenn Cashes drankommen, wie man das dann tatsächlich realisieren kann.
Dass wir halt hier an dieser Stelle eine Speicher in Zugriff hier
haben für eine Datenoperation und einen parallel gleichzeitig Speicherzugriff für den Befehl.
Aber ansonsten funktioniert das in dieser Situation ganz gut.
Das wird aber nicht immer so sein.
Das hier kommt sehr, sehr, sehr schnell vor,
dass nämlich mehrere Stufen gleichzeitig auf eine Ressource zugreifen wollen
Und das klassische, ein klassisches Beispiel ist gleichzeitig ein Zugriff auf
den Speicher. Das haben wir jetzt gerade schon gesehen.
Die sogenannte Harvard-Architektur hat halt nicht nur einen Speicher,
sondern separate Speicher für Befehle und für die Daten.
Und damit wird halt hier dieser Strukturkonflikt schon vermieden.
Es geht aber auch schlimmer so. Also das war jetzt hier dieser Strukturkonflikt,
wo wir halt aus dem, wenn wir nur einen Speicher hätten,
dann würde halt genau diese Situation passieren.
Deswegen ist das jetzt hier rot hervorgehoben,
diese Instruktion läuft los
Jetzt hier einen Wert aus dem Speicher lesen und gleichzeitig
drei Tage später möchte die halt einen Befehl laden.
Das lösen wir dadurch,
dass wir halt den Speicher in zwei separate Speicher aufteilen,
mit separaten Bussen, separaten Adresswerken und dann würde das hier funktionieren.
Die einzige Sache, bei der man dann Schwierigkeiten kriegt,
ist Selbstmodifizieren der Code und das wird dann nächste Woche klar werden,
wie man das lösen kann.
So, die nächste Sache ist jetzt noch wesentlich häufiger und
da kann man nicht so leicht irgendwas tun,
sogenannten Data-Hersatz oder eben Datenkonflikte und der klassische Fall
ist dieses hier
eine Instruktion
Braucht die Ergebnisse einer vorhergehenden Instruktion. Das ist aber noch nicht fertig.
Da gibt es auch eine Abhilfe, das ist sogenannte Forwarding,
aber jetzt gucken wir uns erstmal an, was da überhaupt passiert.
Ich gehe dann gleich zu den anderen Folien zurück.
Damit man mal sieht, was als Beispiel passiert,
wir haben jetzt hier eine Subduktion, wir wollen Register 1,
Minus Register 3 und das Ergebnis soll nach Register
2. Das ist also soweit nicht weiter verwerflich und der nächste Befehl,
den der Compiler uns jetzt gegeben hat, möchte das Ergebnis,
was wir hier gerade berechnen, dann jetzt aber schon benutzen.
Der möchte also jetzt ein logisches Und ausführen mitweise
zwischen Register 5 und Register 2 und das Resultat soll in
das Register
Gut, und der nächste Befehl ist jetzt halt so gemacht,
dass er auf das Register 2 lesen möchte und der macht
ein logisches oder in das Register 13.
Und der nächste Befehl in Adiadregister 2 mit sich selbst und packt das Resultat nach
14. Und dann möchten wir einen Wert speichern,
Register 2, Inhalt plus 100 und das Ganze.
Denn der Inhalt von Register 15 soll halt in den Speicher gespeichert werden.
So, zunächst mal sieht das so aus,
als ob wir das so machen können.
Wir können also hier jetzt die erste Instruktion starten,
wird dekodiert, die holt sich jetzt hier ihre Operanten,
das heißt Register 1 und 3,
dann wird die Superduktion ausführen
Dann ist hier das Resultat der Subprojektionen bekannt. Speicherzugriff haben wir
nicht, deswegen ist das hier ausgegraut oder weiß gelassen.
Das heißt, jetzt würden hier das Resultat dieser Subtraktionen,
würde jetzt hier in das Pipeline-Register weitergegeben werden und so wie die Pipeline gestaltet ist,
würde jetzt erst am fünften Takt das Ergebnis der Subtraktionen hier zurückgeschrieben werden.
Also das ist jetzt halt so durch das Pipeline,
wir starten die Instruktionen und erst im fünften Takt
ist das Ergebnis wirklich fertig und in das Register abgespeichert.
Das ist natürlich ein bisschen spät, weil die zweite Instruktion,
dieses logische und wird jetzt hier am zweiten Takt gestartet und
im dritten möchte es seine Operanten holen, also den Inhalt von Riggister 5.
Das ist okay. Und den Inhalt von Riggster 2. Register 2
ist aber im Moment noch in Bearbeitung und steht noch nicht zur Verfügung.
Da müssten wir also jetzt irgendwie die Fehlersituation erkennen,
was man als Lösung macht, ist das sogenannte Forwarding,
durch einen glücklichen Zufall ist das Resultat der Subtraktion ja jetzt
hier aus der Alu schon gerade rechtzeitig fertig geworden.
Das heißt, das Resultat ist noch nicht im Register drin.
Wir können es also nicht aus dem Rigster holen
Wir können aber, wenn wir diese Situation erkennen,
mit zusätzlicher Hardware, dafür sorgen,
dass das Resultat aus diesem Register zu Alu gegeben wird,
statt aus der Registerbank.
Eine ähnliche Situation tritt jetzt hier auf. Der dritte Befehl
möchte das Register 2 auch lesen. Das wird aber erst
an dieser Stelle geschrieben. Der wird also hier gestartet,
dekodiert und hier möchte er seine Operanten haben.
Die Operanten sind aber immer noch nicht in dem Register drin,
weil sie erst hier geschrieben werden.
Das heißt, auch hier müssen wir das erkennen und dann
entsprechend die Daten jetzt hier in diesem Pipeline-Register drin sind,
nach hier
weiter vorordnen
Wir können natürlich nicht die Daten von hier nehmen,
weil die zu diesem Zeitpunkt bereits von diesem Befehl überschrieben worden sind.
Das heißt, wir müssen jetzt hier einen Bus,
einen Datenpfad einbauen, der es erlaubt,
die Daten von hier zu Alu weiterzugeben und einen separaten,
der es erlaubt, die Daten von hier zu Alu weiterzugeben.
So, jetzt wird es ein bisschen knifflig,
in dieser Befehl möchte Register 2 addieren, mit sich selbst.
Und da hängt es jetzt von der tatsächlichen Hardware-Realisierung ab, was da passiert.
Also jetzt haben wir hier die Situation, dass die Subtraktion,
der erste Befehl tatsächlich im Takt zur ersten Hälfte des Takts 5,
Zyklus 5, die Daten hier reinschreibt,
sodass sie gerade rechtzeitig zum Auslesen in der zweiten Hälfte bereitstehen.
Wenn man das anders implementiert,
müssten wir tatsächlich nochmal so ein Forwarding bauen,
was von hier, die Daten auch hierhin liefern kann.
Und der nächste Befehl allerdings, das ist dann der hier,
der möchte das Register 2 auch lesen.
Der hat aber kein Problem, weil zu dem Zeitpunkt,
wo er seine Operanten liest,
sind die Werte von dem ersten Befehl tatsächlich schon in
die Register reingeschrieben worden.
Ja klar
Ja. Nee. Ja, genau. Ja gut,
der liest das ja nur. Also die Situation,
wenn der jetzt hier das auch wieder in Register 2
reinschreiben würde, ist nochmal ein bisschen komplizierter.
Ja. Nee, nee, also der hier schreibt das
und die folgenden vier
lesen das
Genau, dann wird es ja nochmal um einiges schwieriger.
Dann haben wir halt tatsächlich die Situation, die ich
dann gleichzeige, da kann man dann halt nichts machen.
Das ist jetzt erstmal das Konzept und jetzt gehe ich ein
paar Folien zurück, was man da nämlich bauen kann.
Also das ist die Situation, mit der wir gestartet sind. Die ursprüngliche Hardware mit der Alu und dem Speicher und den Restern und diesem Pipeline-Registern. Und was wir jetzt brauchen,
um dieses Forwarding bauen zu können,
sind halt ein paar zusätzliche Multiplexe und halt so eine Steuerung,
die uns sagt, dass wir aus den,
also nachdem die Alu was berechnet hat,
sind die Resultate halt hier in diesem Pipeline-Register zwischengespeichert und können in den Speicher geschrieben werden können,
hier in die nächste Stufe weitergegeben werden und damit wir sie halt einen Tag später schon verwenden können,
gibt es jetzt einen zusätzlichen Pfad, der von hier,
hier reinläuft, in diese Multiplexe,
sodass wir die entweder für den ersten oder den zweiten Operanten nehmen können und diese Vorwarding-Unique,
die kriegt jetzt halt die Befehlsfelder aus der Stufe,
die hier gelesen werden will und die Daten,
die jetzt hier in den Pipeline-Registern drin sind und kann entscheiden,
ob ein Vorwarding gemacht werden muss oder nicht
Den Hardwareentwurf für dieses Ding sparen wir uns jetzt.
Wer das nachlesen möchte, in dem Bryant-Buch ist halt tatsächlich skizziert,
wie man die ursprüngliche Hardware entwirft und wie man dann tatsächlich
dieses Pipelining in den Prozessor da anflanschen kann.
Da sieht man dann also auch die schönen Logikgleichungen,
die jetzt hier rausfallen. Aber das Konzept ist, glaube
ich, plausibel. Wir haben die Alu-Ergebnisse hier berechnet.
In diesem Register liegen sie vor und wir können sie jetzt
gerade rechtzeitig hier über diesem Pfad weitergeben.
Und für die nächste Situation einen Tag später sind die Zwischenergebnisse
hier in dem Register drin und auch von hier können wir
sie halt über diese Kontrolleinheit und diesen Datenpfad dann entsprechend hier an die Adu weitergeben.
So, die nächste Sache ist dann halt dieses hier.
Wir haben wieder einen Ladebefehl, der den Speicher zugreifen möchte
an der Adresse, Inhalt von Register 1 plus 20.
Das läuft also hier so durch,
dann macht es hier seinen Speicherzugriff und im fünften Takt
wird das Resultat in den Speicher reingeschrieben
Jetzt haben wir halt hier einen Unbefehl,
der das Register 2 mit dem Register 5 verbunden will,
dass Register 2 wird aber hier jetzt durch diesen Ladebefehl erst in diesem Takt geschrieben.
So, lesen möchte dieser Befehl,
das Ding halt in seinem zweiten Takt,
also hier insgesamt in dem dritten,
da möchte der Unbefehl halt das Register 5 lesen,
okay,
das geht und das Register 2,
das Register 2 ist aber im Moment ungültig,
weil es halt von diesem
hier modifiziert wird
Gut, jetzt haben wir natürlich zu diesem Zeitpunkt keine echte Möglichkeit,
die Daten, die wir erst zu diesem Zeitpunkt zur Verfügung haben,
mit so einer kleinen Zeitmaschine einen Takt in die Vergangenheit,
also einen Takt zurück hierher zu transportieren.
Deswegen ist das hier gestrichelt. Das geht schlichtweg nicht,
weil wir die Daten erst zu diesem Zeitpunkt zur Verfügung haben.
Und da müssen wir uns also jetzt gleich irgendwas überlegen.
Dieser Befehl hier wird jetzt im dritten Takt gestartet und möchte im
vierten Takt ebenfalls das Register 2 und das Register 6 lesen.
Register 6, kein Problem, das steht zur Verfügung.
Register 2 ist halt auch problematisch. So,
das nennen die Alu möchte hier also eine Unteroperation, ne,
sorry, eine Oder-Operation durchführen hier und braucht ihre Resultate.
Das kann jetzt funktionieren, wenn wir ein Forwarding machen,
von dem Wert, den wir hier aus dem Speicher gelesen haben, hierhin.
Und dann haben wir jetzt hier wieder in diese Situation mit den halb,
halb getakteten Registern. Die Register sind halt viel schneller als
die Alu oder Speicherzugriff, deswegen kann man das machen.
Also der wird hier in der ersten Hälfte da reingeschrieben und kann
in der zweiten Hälfte des Takts 5 bereits benutzt werden.
Also dieses hier würde funktionieren, dieses hier würde auch funktionieren
Und an dieser Stelle haben wir aber ein echtes Problem.
So, das geht also nicht. Da gibt es also
zwei Möglichkeiten, das zu beheben. Die erste ist nicht für
den Assembler-Programmierer normalerweise nicht praktikabel, aber für den Compiler schon.
Wenn der Compiler halt seine Hardware kennt,
dann weiß der Compiler, dass diese Kombination notwort,
nach Register 2 und einen Tag später Register 2 benutzen, nicht gültig ist.
Und die einfachste Möglichkeit, das dann zu lösen, ist,
dass wir halt die Hardware einfach lassen und stattdessen sagen,
dass wir das in der Software reparieren, wenn diese Situation vorkommt,
dann müssen wir halt hier an dieser Stelle eine Operation einfügen.
Das heißt, wir packen jetzt hier einen Befehl in die Pipeline rein,
der wird gestartet und läuft durch und macht gar nichts.
Letztlich sorgt das eben dafür, dass wir einen Takt frei haben.
Das ist für die Performance nicht so gut,
aber das sorgt dann dafür,
dass hier dieser Unbefehl jetzt tatsächlich funktionieren kann,
weil der möchte halt hier einen Wert aus dem Register lesen.
Das ist noch nicht fertig,
aber wir können jetzt über diese Vorwarding-Einheit den Wert hier von
der Stufe 4 zur Alu weiterleiten und damit funktioniert es dann
So, und dadurch,
dass wir das jetzt einen Takt verzögert haben,
haben sich jetzt auch die Situation bei diesem Uder und dem Erdbefehl danach entspannt.
Der war bisher nur dadurch möglich, dass wir halt Forwarding gemacht haben.
Das geht jetzt hier mit diesem halben, halben Takt für die Briggister problemlos.
Und der letzte Befehl hat jetzt gar keine Schwierigkeiten mehr,
weil die Werte sind hier geschrieben worden und stehen dann halt hier rechtzeitig zur Verfügung.
Okay, wir haben jetzt also die Komplexität ausgelagert in den Compiler,
damit das sauber funktioniert auf der gegebenen Hardware.
Da müssen wir halt einen Compiler haben,
der all solche Situationen zuverlässig erkennt
Und dann halt gegebenenfalls solche Nops einbaut, damit halt
die Konflikte vermieden werden. Die zweite Möglichkeit ist,
dass man die Komplexität in die Hardware einbaut. Das heißt,
dann können wir das gegebenenfalls sogar noch wieder mit Assembler programmieren,
wo man halt wirklich es nicht im Griff behalten kann,
nicht im Blick behalten kann, welche Operationen gerade in welchem Zustand sind.
Wir können also jetzt an dieser Stelle die Hardware so aufpimpen,
dass sie erkennt,
dass hier ein Konflikt vorliegt und entsprechend den Rest
der Pipeline anhält
Eigentlich müsste ich hier jetzt auch noch so ein blauer Kringel sein,
weil hier müsste er eigentlich im dritten Takt der hier schon gestartet werden,
aber dadurch, dass jetzt hier halt erkannt wird,
wir haben den Wert von Register 2 noch nicht,
der ist noch nicht gültig.
Deswegen wird das jetzt hier angehalten und einen Tag später kann es weiterlaufen und
dann hier mit dem Vorwarding dieser Befehl dann ausgeführt werden.
Das ist also die zweite Möglichkeit, dass die Hardware-Designer alle
möglichen Konfliktsituationen erkennen, beschreiben und in ihre Logikgleichung einbauen müssen.
An dieser Stelle wird dann halt die Pipeline hardwaretechnisch angehalten
Und mit dieser Lösung wird sie halt softwaretechnisch einfach mit nur Operations gefüllt.
In beiden Fällen ist das Resultat letztlich das Gleiche.
Wir verlieren einen Takt, also einen Tag lang wird die Pipeline
nicht mit den nützlichen Befehlen gefüllt und liefert kein Resultat,
das ist irgendeine Verzögerung, aber dafür ist die Aufführung jetzt korrekt.
Gut, die nächste Sache sind die sogenannten Control-Hesards oder Steuerkonflikte.
Und das konnte sehr, sehr häufig vor,
nämlich bei jedem Sprungbefehl und bei jedem Winterprogramm oder Funktionsaufruf und bei jedem Return.
Wer sich die Folien schon mal angeschaut hat oder das Buch,
der weiß bereits, was jetzt passiert. Das Problem ist jetzt,
dass wenn wir einen Sprung haben, dass die Programmausführung nicht mehr Instruktion 1,
2, 3, 4, 5 macht, sondern irgendwo hinspringt.
Und in dem Moment, wo wir dann springen müssen,
das weiß ich, wenn wir von 1, 2,
3 nach 15, 16, 17, 18 springen,
dann müssen wir also nicht 1, 2, 3,
4, 5, 6 irgendwo in die Pipeline einfüllen,
sondern das,
was auf das Sprungziel füllt.
Das klingt jetzt noch nicht so schlimm,
aber das Sprungziel wird erst berechnet, während der Befehl,
der Sprungbefehl in der Pipeline drin ist
Das heißt, wir müssen entweder deinen Nops in die Pipeline
einfüllen nach jedem Sprungbefehl oder wir müssen dafür sorgen, dass das,
was wir potenziell schon mal in die Pipeline reingefüllt haben,
dann wieder alles gecancelt wird und verworfen wird.
Das wird natürlich, je mehr Forwarding und je mehr Zwischenergebnisse
wir da da haben, immer komplizierter.
Und Quintessenz ist halt Sprungbefehle und für Pipelines sind Gift.
Diese Idee mit N mal K durch N,
Funktion N mal K durch N plus K funktioniert nur für
so Lineare-Sequenzen von Befehlen,
wenn wir Sprünge haben
Dann muss die Pipeline immer komplett ausgeleert werden. Und das ist halt
nicht schön. Und jedenfalls in einem Aspekt der Performance. So,
auch da gibt es jetzt wieder die Möglichkeit,
dass wir entweder das auf den Compiler verlagern oder dass die Hardware das macht und die andere Sache ist,
dass halt, dass wir versuchen, Software,
softwaretechnisch Instruktionen so umzusortieren, dass wir erst den Sprungbefehl haben,
da noch ein paar Instruktionen danach kommen,
die wir noch ausführen können und erst dann den Sprung machen.
Das galt eine Zeit lang als gute Idee und deswegen haben halt,
was weiß ich,
die Leute bei Maps und Spark
das in ihren Befehlsatz eingeführt
Das hat aber nur so lange funktioniert, solange sie diese fünfstufige Pipeline haben.
Jetzt, wo sie, was weiß ich,
20 Pipeline-Stufen haben, bedeutet das, ja,
es ist mir nicht mehr so ein oder zwei Instruktionen nach dem Sprung bauen müssten, sondern 15 oder sowas.
Und das macht den Rechner jetzt viel komplizierter, als er sein müsste.
Und das Letzte werde ich aus Zeitgründen einfach überspringen,
das ist eine Technik, wo man halt in Software versucht,
entweder in Hardware oder im Software in der Befehlskodierung,
dem Prozessor zu sagen, ja,
der Sprung wird vermutlich gemacht oder nicht und entsprechend dann die Instruktionen,
die dann anschließend danach kommen, in die Pipeline eingefüllt werden.
So, das ist ziemlich kompliziert, was da auf dem Text steht,
aber wenn man sich das am Beispiel anschaut, dann wird es wahrscheinlich sofort klar.
Wir haben jetzt hier Branche Equel,
der vergleicht also eine Register 1 und Register 3 miteinander und wenn die beide gleich sind,
dann machen wir einen Sprung und der Sprung soll jetzt passieren,
ja, das ist jetzt halt hier ein Offset von 28.
Das heißt, wenn der Sprung nicht genommen wird,
dann laufen wir hier weiter und wenn der Sprung gemacht wird,
dann springen wir halt 28 Instruktionen weiter nach hinten und würden
diese hier ausführen wollen
So, das ist also jetzt ein Begegnersprung. Zur Laufzeit
ist dem Prozessor nicht bekannt, was passieren wird.
Der wird also jetzt hier erstmal aus dem Speicher geholt,
dann dekodiert, dann holt er sich hier seine Operanten.
Das heißt, Register 1 und Register 3. Hier werden die voneinander abgezogen,
damit wir entscheiden können, ob die gleich sind. Das Resultat des Vergleichses,
also zu diesem Zeitpunkt erst bekannt und letztlich,
weil der Speicher zu ihr keinen Speicherzugriff passiert,
nach der reinen Lehre ist erst hier am Ende des vierten Takts bekannt,
ob der Sprung gemacht werden soll oder nicht
Zu dem Zeitpunkt hier, haben wir aber jetzt schon diesen nachfolgenden Befehl,
also hier ein logisches und, logisches Uder und deine Addition,
auch schon in die Pipeline eingefüllt.
Die sind also jetzt hier schon unterwegs und wenn sich jetzt herausstellt,
dass wir den Sprung machen,
dann hätten wir diese hier also gar nicht erst starten dürfen,
sondern wir müssen dann also hierhin springen und diesen Befehl dann aus dem Speicher holen.
Bei der fünfstufigen Pipeline bedeutet das dann halt,
also dass wir diese hier wegschmeißen müssen,
weil die Hardware muss das also jetzt markieren,
dass die Resultate niemals nicht in den Speicher oder in
irgendwelche Register
reingeschrieben werden
Weil sonst dir das Resultat der Programmausführung halt falsch ist.
Das muss also jetzt hier gecancelt, verworfen werden und entschlüssen,
hier dann halt der nächste Befehl in die Pipeline eingefüllt werden.
Gut, dagegen kann man nichts machen. Bei der fünfstufigen Pipeline
verlieren wir halt an dieser Stelle drei Instruktionen. Wenn man sich anschaut,
dass jetzt aktuelle Prozessoren in der Größenordnung 20 oder so Stufen haben,
dann besteht halt bei jedem Sprung, den wir im Programm einkodieren,
das Problem, dass wir bis zu, ja,
was weiß ich, größenordnungsmäßig 20 Instruktionen jeweils verwerfen müssen,
die bereits partiell in Bearbeitung sind.
Und das ist halt schlecht
Andererseits gibt es keine bekannte Technik, um solche Sprunggefehle zu vermeiden.
Dagegen kann man also tatsächlich nichts tun. Gut, die anderen Dinge hier,
die werde ich jetzt einfach mal weglassen. Zusammenfassung Pipeline,
das Grundkonzept ist relativ klar, wie so ein Fließband in der Fabrik,
einzelne Stufen, die jeweils eine kleine Sache machen.
Dadurch, also im Fall des Prozesses halt den gesamten Befehlszyklus aufteilen in einzelne kleine Häppchen.
Dadurch werden die Schaltnetze einfacher und schneller. Das heißt,
wir können das Ding höher takten und da wir in jedem Takt
idealerweise dann halt einen neuen Befehl starten können, erhöht sich der Durchsatz.
Das heißt, das ist für die Performance gut.
Die klassische Pipeline hat halt diese fünf Stufen,
das ist einigermaßen plausibel, was da passiert, Befehle holen,
Operanten holen und ein bisschen dekodieren, dann ausführen,
dann gegebenenfalls Sprecherzugriffe machen und dann die Werte in die Register zurückschreiben.
Das war früher mal fast alle aktuellen Prozessoren sind halt
wesentlich komplizierter, aber das Konzept bleibt halt gleich.
Dann muss man sich eben überlegen,
dass diese Befehle nicht voneinander unabhängig sind,
sondern dass es da halt
Konflikte geben kann
Strukturkonflikte sind relativ leicht zu lösen. Einfach dadurch,
dass wir Hardware mehrfach bereitstellen. Diese Datenkonflikte kann man
zum Teil durch dieses Forwarding lösen, zum Teil nicht.
Da müssen wir halt solche Nops in die Pipeline einfüllen oder die Pipeline anhalten.
Und Sprungbefehle sind tatsächlich ein ernstes Problem. Bei Code,
der wenig Sprünge hat oder Schleifen,
die halt mehrfach gleichmäßig durchlaufen werden, ist das nicht so schlimm.
Aber irgendwie so kontrolldominierter Code, der ganz,
ganz viele Entscheidungen treffen muss,
der läuft halt auf solchen Pipeline-Prozessoren
entsprechend langsam
Gut, und Pipelining ist zunächst mal konzeptionell komplett unabhängig von der Befehlsarchitektur,
historisch war es halt so, dass die einen relativ regulären,
einfachen Architekturen, diese Risk-Architekturen, die ersten waren, wo halt Pipelining auf einen Chip passte.
Deswegen hatten die da eine Zeit lang Performance-Vorteile,
mittlerweile passen mehrere ganze Prozessoren auf einen Chip und da hat sich das dann halt wieder relativiert,
damit man mit so einem Pipeline programmieren kann,
muss man halt entweder die CPU-Pipeline sehr gut kennen,
wenn man Assembler programmieren will oder man muss sich auf seine Tools verlassen,
Das heißt, die Compiler müssen dann wissen,
für welchen Prozessor tatsächlich kompiliert werden muss und zum Beispiel eben solche No Operations einbauen.
Wäre das weiter vertiefen möchte, sind hier nochmal die Literaturangaben.
Das ist natürlich dieselben, die bei den meisten Sachen
und bei den meisten anderen Kapiteln auch.
Dieses war das Einsteigerbuch von Patterson und Hennessey,
entweder auf Englisch oder auf Deutsch und das ist sozusagen die Bibel.
Das ist dann, also das Buch,
wo das wirklich quantitativ analysiert wird,
abhängig von den Häufigkeiten der einzelnen Befehle und so weiter
Weiter dann wirklich analysiert wird, wie man sowas entwerfen kann.
Und in dem Brian O'Halleran wird tatsächlich auch die Hardware für sowas dann durchentworfen.
Da sind also dann auch zum Beispiel für diese Vorwarding-Blöcke
es erläutert, wie man dann die Logikgleichung dafür aufbaut.
Das soll jetzt zu dem Thema Pipelining reichen. Es ist halt eine Möglichkeit,
um die Performance zu verbessern und weil man mit relativ geringem Zusatzaufwand,
sagen wir mal, vier Zusatzregistern fünffache Performance kriegen kann,
ist das halt auch in allen Rechnern,
die man derzeit so kaufen kann,
implementiert,
außer in den ganz,
ganz langsamen Mikro-Controllern
Damit soll das Thema aber auch schon beendet werden.
Und jetzt wollen wir uns so ein bisschen was Interessanteres angucken oder
mindestens ebenso Interessantes, nämlich eine kurze Einführung in einen Parallelrechner.
Und das letzte Kapitel wird dann eben im Wesentlichen
ein Großteil der nächsten Woche ausmachen, den Speicher erreichen.
Klar ist, wer schon mal mit Rechnern gespielt hat oder sowas,
man möchte immer mehr Performance haben.
Und auf der anderen Seite ist halt die Performance eines
einzelnen Prozesses durch den Technologie und die Architektur begrenzt,
was man sich überlegen kann, ist,
dass man halt Programme auf mehrere Prozessoren aufteilen möchte
Und da gibt es jetzt eine ganze Vielfalt von Sachen,
die man tun kann, kann sich zum einen entscheiden,
wie viele Prozessoren man dann benutzen möchte und wie die aussehen sollen.
Und dann wird die Kommunikation zwischen verschiedenen Prozessoren halt
ein interessantes, neues Problem. Wie kommen zum Beispiel,
kommt ein Prozessor an die Daten eines anderen Prozessors heran?
Und Programmierung in Software und Tools, das ist ein aktuelles Forschungsthema,
also diese Sachen hier sind noch nicht endgültig gelöst.
Es gibt zwar eine ganze Menge Sachen,
die sich etabliert haben, aber da ist noch viel
im Fluss
So, jetzt haben wir schon fast ein ganzes Semester,
Rechner, Architektur, Rechnerstruktur gemacht.
Noch nicht einmal definiert, was Performance eigentlich ist.
Das wird also jetzt mal kurz nachgeholt. Da gibt es unterschiedliche Ansätze dazu.
Für uns als Einzelanwender mit so einem PC oder Smartphone oder sowas
ist dieses hier häufig die interessante Sache. Das sogenannte,
die sogenannte Antwortzeit oder Response Time oder Execution Time führen ein Programm.
Ward Clock Time bezieht sich eben auf die Wanduhr,
die Zeit, die tatsächlich vergeht,
bis ein Programm zu Ende durchberechnet hat
Also die Gesamtzeit und Performance können wir dann letztlich definieren,
als eine Sache, die eins durch die Ausführungszeit ist.
Das heißt, je schneller unser Programm fertig ist,
desto besser ist die gefühlte Performance.
Diese Art und Weise haben wir halt ein quantitatives Maß dafür,
wie schnell irgendwas funktioniert. Beispiel dafür wäre Monter Unix,
wenn man in der Shell irgendwie ein Time aufruft und dann ein Programm ausführt,
dann kriegt man, nachdem das Programm fertig berechnet hat,
halt so eine Ausgabe wie hier.
Und wenn man das dann aufschlüsselt, bedeutet das,
es hat insgesamt 9 Minuten, 57 Sekunden und 65 Millisekunden gedauert,
um das Programm auszuführen. Davon waren 507 Sekunden,
also hier fast zehn Minuten und hier fast 600 Sekunden.
Also fast die volle Zeit war für das Benutzerprogramm reserviert und
ein kleiner Teil in diesem Fall halt 0,15 Sekunden für das System,
Betriebssystem, Kontextwechsel, IO-Operation und sowas.
Und das Programm hat tatsächlich 99,9 Prozent unserer CPU
während der Programmausführung des Zeit insgesamt für sich selbst gehabt
Und der Windows weiß ich nicht, wie man es machen kann,
aber der Unix auf der Kommandozeile kann man halt time vor irgendein anderes,
vor irgendeinem anderen Befehl schreiben und dann kriegt man halt hier diese Ausgabe.
Für einen Google oder Twitter oder einen Betreiber eines Rechenzentrums
ist das möglicherweise gar nicht so interessant,
ob der einzelne Benutzer da ein gutes,
eine gute User Experience hat und das besonders schnell geht,
sondern für ein Rechenzentrum ist dieses hier vielleicht wichtiger,
wie viele Anfragen,
wie viele Programme haben wir
pro Zeit Einheit bearbeitet
Unabhängig davon, ob das halt für den Einzelnen Benutzer möglichst schnell ist.
Es ist vielleicht wichtiger, dass wir, was weiß ich,
1.000 Flugbuchungen oder 1.000 oder eine Million Suchanfragen pro Minute durchführen können.
Das ist dann also eine Sache, die eher so eine mittlere Performance misst,
während dieses halt hier die Performance für ein einzelnes konkretes Programm ist.
Und wenn man dann zwei Systeme miteinander vergleicht,
dann haben wir ja wieder diesen magischen Faktor Speedup.
Der misst einfach die Performance des einen Systems im Verhältnis zur
Performance des anderen Systems und mit diesem eins durch Execution Time
wäre halt Speed-up von einem System Y gegen X, nee,
X gegen Y, 1 durch, die Execution Times,
also Execution Time von Y durch Execution Time X.
Und was wir jetzt machen wollen, ist halt die Performance eines einzelnen Prozesses,
ist jetzt mit Pipelining und sonstigen Tricks mehr oder weniger fest,
was können wir erreichen, wenn wir jetzt halt mehrere Prozessoren mehrere Rechner gegebenenfalls dazukaufen.
Gut, man kann sich jetzt leicht überlegen,
dass wenn wir ein gegebenes Programm haben,
dass die Gesamtausführungszeit nicht nur mit einer Stoppuhr gemessen werden kann,
sondern dass sich das offensichtlich zusammensetzt, aus der Anzahl der Befehle,
die da abgearbeitet werden und der Zeit pro Befehl.
Gut, ob ihr das Halteproblem jetzt lösen,
also statisch aus dem Quellcode werden wir das jetzt nicht direkt ernehmen können,
aber wir können eben das Programm laufen lassen und mitzählen,
wie viele Befehle wurden ausgeführt und aus dem Datenblatt des Prozessors wissen wir,
wie viel Zeit pro Befehl benötigt wird.
Damit können wir dann also anschließend die Ausführungszeit berechnen.
Und wir haben jetzt hier offensichtlich zwei Ansatzmöglichkeiten, um das zu verbessern.
Das eine ist halt diesen eher kleiner zu machen,
das heißt weniger Befehle
Das bedeutet bessere Tools oder die Befehle müssen mehr machen.
Und das ist Kleinkram, der entscheidende Punkt ist dieses hier,
um die Anzahl der Befehle zu reduzieren,
ist der optimale Ansatz oder der Ansatz,
wo es sich wirklich lohnt, halt die Verwendung der besten bekannten Algorithmen.
Hier kann man richtig was sparen und das andere ist halt
mehr oder weniger das, was man vom Hersteller wegen gegeben bekommt.
Die zweite Sache ist, dass wir versuchen, den hier zu verkleinern.
Das heißt, wir wollen weniger Zeit pro Befehl benötigen.
Pipelining ist eine Möglichkeit, Speicherzugriffe beschleunigen,
das kommt nächste Woche,
das ist die zweite Möglichkeit
Bessere Technologie ist klar. Und eben einfachere Befehle. Also
hier ist einmal dieses Komplex-Instruction-Set im positiven Bereich und hier das Reused-Instruction-Set-Computer.
Da gibt es also offensichtlich so ein Trade-Off. Und die
andere Idee ist halt, naja gut, was passiert eben,
wenn wir halt die Dinge hier irgendwie auf mehrere Rechner, mehrere CPUs aufteilen?
Das wäre dann also hier der Punkt, parallele Ausführung.
Gut, kann man sich überlegen, dass man das machen möchte.
Und da gibt es jetzt ein ganz fundamentales Limit,
zumindest im Bereich der Rechnerstruktur unter dem Begriff Ander als Gesetz bekannt ist oder bekannt geworden ist.
Das gilt aber ganz allgemein,
also jetzt nicht nur speziell für Programme oder sonstige Sachen,
sondern das kann man ganz allgemein auf irgendwelche Dinge anwenden,
bei denen man ein Qualitätsmaß berechnet und versucht, einen einzelnen Bestandteil zu verbessern.
Hier ist also die Idee, dass wir ein System haben,
das ist also unser Ausgangssystem und wir berechnen ein bestimmtes Programm
und dann haben wir uns eine Funktion X ausgeguckt,
bei dir,
wenn die Idee haben,
wie wir diese Funktion X
besser machen können
Und hier im Ausgangssystem ist halt diese Funktion X so,
dass sie halt einen Anteil der Gesamtzahl belegt, also der gesamten Antwortzeit benötigt.
Und das muss natürlich zwischen Null,
dann wird die Funktion gar nicht verwendet oder eins liegen,
dann wird die Funktion ausschließlich dieses Programm P sein.
Und normalerweise liegt das halt irgendwo dazwischen. So,
und jetzt haben wir die Idee, dass wir diese Funktion X verbessern,
also zum Beispiel auf mehrere Rechner aufteilen, auf mehrere CPUs verteilen.
Das heißt, wir haben jetzt eine zweite Funktion X-Strich und
die ist schneller als X und für diese beiden,
für die neu entwickelte Funktion,
die wir optimiert haben,
haben wir halt so
ein Speed-up
Schon mal ermittelt, was da tatsächlich dann für diesen Teil,
so für diese Funktion X, die wir jetzt durch X-Strich ersetzen, haben wir ein Speed-up.
So, und was ist jetzt der Gewinn für das gesamte Programm?
Das kann man sich ausrechnen. Das ist jetzt erstmal hier so ein etwas kryptischer Ausdruck,
der Speed-up des Gesamtsystems, also im System 2 gegen System 1,
ist eins durch 1-F, das ist halt der Teil,
den wir nicht angerührt haben,
plus F durch SX,
also der Zeitaufwand für die Funktion F ist jetzt geringer geworden,
weil wir das halt beschleunigt haben
Würde mich wundern, wenn jetzt jemand hier aus diesem Ausdruck sofort sieht,
was da passiert, aber wir können uns ein paar Beispiele anschauen.
Erst nochmal die Motivation, was da passiert. Wir haben also
jetzt hier unsere Gesamtprogramm P und das besteht halt aus der Funktion F,
aus dem Anteil F, der Gesamtzeit, für diese Funktion X,
die wir beschleunigen können und aus dem Anteil 1-F,
den wir im Moment nicht optimieren.
Also das ist halt der Anteil, der so bleibt,
wie er war, nennen wir mal den seriellen Anteil,
den wir nicht optimieren und das ist der Anteil des Systems
oder im Moment halt der Gesamtlaufzeit,
die wir optimieren
Und bisher hatten wir die Gesamtzeitantwortzeit Tee. Was wir jetzt versuchen,
ist halt, dieser Teil bleibt gleich, das ist der hier.
Und den Teil optimieren wir jetzt zum Beispiel, indem wir ihn auf mehrere CPUs aufteilen.
Also hier haben wir nur eine CPU und dann haben wir hier,
sorry, NCPUs gleichzeitig aktiv. So, die Zeit,
die wir jetzt hier gewinnen,
ist offensichtlich halt hier gleich geblieben,
also die Gesamtzeit, die wir schon mal hatten,
mal 1 Minus F und dann halt hier F,
aufgeteilt auf M-Parallele CPUs unter der Annahme,
dass das halt sich perfekt parallelisieren lässt.
Dann ist halt dieser Zeitanteil
deutlich geschrumpft
Unsere neue Antwortzeit ist halt dieses hier gegenüber dem.
Und was das Anleitsgesetz jetzt sagt, ist,
dass der Speed-up, also diese Zeit gegenüber diese,
sich berechnet als eins durch eins minus F plus F durch SX.
Und das, was wir jetzt gleich sehen, gilt halt
nicht nur für Befehlsausführung bei Rechnen, sondern für alles Mögliche.
Also wenn dieses hier jetzt zum Beispiel Zeit ist von Hamburg
nach Berlin mit der Bahn zu fahren oder mit dem Flugzeug hier,
dann haben wir zusätzlich eben noch den Zeitaufwand,
um von der Wohnung bis zum Flughafen oder bis zum Bahnhof
zu kommen und entsprechend hier
Das wird sich also dann nur dann lohnen,
wenn dieser Teil hier klein ist gegenüber dem.
Und da kommen jetzt hier gleich mal ein paar Beispiele ins Spiel.
Also, wir haben jetzt hier einen Speed-up,
einer Funktion, die wir verbessert haben,
um den Faktor 10. Das ist also schon mal ganz ordentlich.
Wir haben also irgendeine Berechnung gehabt und davon haben wir einen Teil,
der ist seriell, den fassen wir nicht an.
Und dann einen Teil, in diesem Fall halt eine Funktion,
die 10 Prozent der Ausführungszeit belegt hat bisher und die
machen wir zehnmal schneller.
Dann kann man
das da einsetzen
Funktion F hat 0,1, also 1 minus F ist dann 0,9 und 0,1 durch den Speed-up,
die neue Funktion hat also jetzt hier null ganz wenig Zeitaufwand und lohnt sich das?
Kann man komplett vergessen. Der Gesamtspeed-up ist also jetzt hier gerade mal 10 Prozent,
obwohl wir diese Funktion um Faktor 10 beschleunigt haben.
Tatsächlich lohnt es sich also prinzipiell niemals irgendwie Arbeit in einer Optimierung reinzustecken für irgendwas,
was überhaupt gar nicht oder nicht maßgeblich zum Gesamtergebnis beiträgt.
Also hier was, was nur 10 Prozent der Gesamtzeit ausgemacht hat,
da in Arbeit zu investieren, lohnt sich kaum.
Was schon deutlich nützlicher ist,
dass also an dieser Stelle hier eine Funktion,
die 90 Prozent unserer bisherigen Zeit ausmacht,
da nochmal ein bisschen Hirnschmalz reinzustecken und die zum Beispiel doppelt so schnell zu machen,
dann sieht man, der Gesamtspeed-Up ist immerhin 1,8.
Da hat sich also tatsächlich mal relevant was getan. Genauso,
also hier eine Funktion, die 98 Prozent der Gesamtbrechenzeit ausmacht,
um 10 Prozent zu beschleunigen,
das liefert dann eben auch insgesamt fast ein Faktor 10 Prozent
Während eben hier so eine richtig große Beschleunigung von irgendwas,
was man nicht braucht, nichts bewirkt.
So, das in Extrem kann man sich jetzt auch mal anschauen.
Also jetzt haben wir hier eine Funktion, die immerhin 99 Prozent der Gesamtlaufzeit ausmacht.
Und wir machen diese Funktion, um einen Faktor 10.000 schneller,
was weiß ich, GPU oder Quantencomputer oder sonst irgendwas.
Klingt ja erstmal nicht schlecht. Also hier,
das ist definitiv relevant, hier ein Faktor 10.000,
aber es bleibt eben hier noch so ein Faktor in 1 Prozent übrig.
Und der ist jetzt hier halt noch im Nenner drin.
Und der andere Teil, der ist halt 0,000. Und wenn man das dann ausrechnet,
hat man halt hier 1 durch 0,01, weil halt 71 Prozent übrig bleibt.
Das heißt, wir haben jetzt hier 10.000 Prozessoren reingesteckt und
kriegen insgesamt ein Speed-up von 100 raus.
Das ist also eine ganz wesentliche Sache,
die man mal verstanden haben muss, bevor man anfängt,
irgendwelche Dinge in seinem eigenen Kurs zu optimieren.
Das lohnt sich nur für Dinge, die halt tatsächlich maßgeblich beitragen.
Und selbst, wenn sie sehr, sehr dominant sind,
gibt es hier halt Limits, wenn ein Teil übrig bleibt,
den man nicht optimieren kann, dann nützt es halt nichts hier.
Ganz, ganz viel Aufwand reinzustecken,
weil man dann hier dann doch limitiert wird
Gut, also irgendwas um N-Prozessoren auf Endprozessoren aufzuteilen, wirkt in vielen Fällen.
Nicht Lina A. Also hier haben wir nicht 10.000 und
hier haben wir nicht 4.500 erreicht, sondern deutlich weniger.
Das heißt, da gibt es so einen Kosten-Nutzen-Kompromiss,
wie viel Hardware kann man sich leisten und was kommt dann hinterher tatsächlich noch aus?
So, es gibt dann einige Beispiele, einige Anwendungen,
wo man tatsächlich gute Parallelität und entsprechende Speedup erreichen kann.
Bei den meisten Programmen, die man hier so tagtäglich auf Desktops einsetzte,
ist das von vielen, vielen Forschern analysiert worden.
Und die nicht so wirklich positive Nachricht ist, diese hier,
egal wie viel Hardware man auf so typische Programme,
C, Ja war sonst was, loslässt.
Die meisten Programme haben halt hier so ein Speedup-Potenzial insgesamt von
in der Größenordnung 4. Das heißt,
mit so einem Quadcore-Prozessor,
wie man ihn jetzt im Desktop häufig zur Verfügung hat oder in 8 Jahren oder sowas,
ist man da schon eher im oberen Limit dessen,
was man
überhaupt nur parallelisieren kann
Das liegt einfach daran, wie die Programme derzeit geschrieben werden.
Also wenn man klassische Programme erst mal irgendwie so runterkompiliert hat,
dann ist die Parallelität weg.
Wenn man von vornherein versuchte, parallele Programme zu entwickeln,
dann ist da möglicherweise noch mehr Potenzial drin.
Ich glaube nicht, dass ein Anwaltsgesetz in dieser Form als Formel abgefragt wird.
Das ist aber jetzt wirklich nicht nur klausurrelevant,
sondern das ist eine Sache, die man sich wirklich indirekt klar machen sollte.
Wenn wir halt versuchen, irgendwas zu parallelisieren,
irgendeine Teilfunktion zu optimieren, Und das lohnt sich nur dann,
wenn diese Teilfunktionen auch wirklich signifikant beiträgt ursprünglich.
Und dann kommt natürlich noch dazu, dass wenn wir das irgendwie parallelisieren,
dann müssen wir die Daten auch von einem Prozessor auf verschiedene Prozessoren verteilen und dann hinterher die Zwischenergebnisse wieder zusammensammeln.
Das heißt, über diesen Ursprünglich, über die ursprüngliche Formel
kommt hier tatsächlich noch ein zweiter Faktor in den Nenner hinein,
der den Speedup dann nochmal wieder begrenzt.
Das ist dann nämlich der Kommunikations-Overhead zwischen den einzelnen Teilsystemen,
die dann zusammenarbeiten sollen
So, und ich werde euch vorschlagen, sich diese Sache wirklich
mal nochmal anzuschauen, dass man das wirklich verstanden hat.
Im Skript sind jetzt ein paar Beispiele. Also hier
haben wir jetzt einen Plot, wo die Teilfunktion,
die wir optimieren wollen, tatsächlich um einen Faktor 10 schneller geworden ist.
Und jetzt ist hier geplottet der Anteil der Funktion an einem Gesamtteil,
also dieses F. Ephesid, der Teil des Programms,
den wir beschleunigt haben, 1-F, der Teil irgendwie nicht beschleunigt haben.
Und der Gesamtspeed-Up ist halt, ja, zunächst mal,
wenn wir, wenn die Funktion, die wir jetzt beschleunigt haben,
gar nicht aufgerufen wird, also hier Anteil Null hat,
dann ist der Speed-Up natürlich 1.
Es ändert sich gar nichts an der Performance
Und dann steigt das hier so langsam an.
Also die Funktion ist jetzt zehnmal schneller geworden,
wenn sie 60 Prozent der Gesamtbrechenzeit ursprünglich war,
dann haben wir insgesamt einen Speedup von zwei.
Und den Faktor 10 hier als Speedup erreichen wir tatsächlich nur dann,
wenn die Funktion auch tatsächlich zu 100 Prozent in die ursprüngliche Aufgabe eingeht.
Die alternative Art, das darzustellen,
ist dann halt hier als Funktion der Parallelität.
Abhängig davon jetzt, wie stark die Funktion,
also hier The Speedup als Funktion von N,
für verschiedene Beispiele
von dem Anteil
0,9, 0,8, 0,5 und 1,0. Ja, das ist plausibel.
Das heißt, wenn wir den Funktionen 100 Prozent unserer Rechenzeit
belegt und wir die halt auf zwei, drei, vier,
zehn Prozessoren aufteilen, dann geht halt der Speedup Nina A. hoch.
Wenn wir aber einen seriellen Anteil dabei haben,
den wir nicht entsprechend optimieren können oder nicht aufteilen können auf mehrere Prozessoren,
dann sind wir eher so in diesem Bereich,
hier zum Beispiel eben bei 50 Prozent parallelisierbar und 50 Prozent nicht parallelisierbar,
haben wir halt hier so ein Speedup,
der sich knapp unterhalb von zwei einpegelt
Und von der Steigerung der Funktionen wird auch klar,
dass wenn wir da jetzt 100 oder 1000 Prozessoren nehmen würden,
dann sind wir hinterher immer noch hier bei knapp unterhalb von zwei.
Das ist also für jemanden, der versucht,
eine gute Idee hat, dass man irgendwas schneller machen kann,
das lohnt sich wirklich nur dann, wenn halt der Gesamt,
die Funktion, die man da beschleunigen möchte,
auch tatsächlich signifikant zur Gesamtaufgabe beidringt.
So, das wäre jetzt halt für unendlich
viel schneller und auch da ist das dann halt so,
dass man einen signifikanten Beschleunigung nur dann bekommt,
wenn halt diese Funktion wirklich ernsthaft zum Original beiträgt
So, das war jetzt halt das,
was man theoretisch überhaupt nur erreichen kann.
Jetzt wollen wir uns nochmal anschauen, wie das hardwaretechnisch aussehen könnte.
Das haben wir jetzt einen Mittwoch und heute uns angeschaut,
wie das mit Pipelining funktioniert. Wir haben halt verschiedene Pipeline-Stufen
und verschachteln die einzelnen Befehlsausführungen so ein bisschen ineinander.
Das ist halt so die kleine Stufe von Parallelität,
wo wir halt dann bis zur Anzahl der Pipeline-Stufen-Instruktionen gleichzeitig in Bearbeitung haben.
Mittlerweile kann man sich aber hardwaretechnisch leisten,
dass man nicht nur eine Pipeline hat,
sondern sogar mehrere
Das wäre also auf einem Prozessor mehrere Pipelines parallel.
Wir holen uns jetzt also mehrere Befehle aus dem Speicher,
dekodieren die separat, holen die Operanten,
führen die aus und schreiben die Resultate zurück.
Das sieht jetzt hier sehr, sehr einfach aus,
weil wir das Forwarding und all diese Abhängigkeiten einbauen müssen,
würde das eben in echt dann doch mehr sowieso ein vollständig vernetzter Graf aussehen.
Das heißt, die Hardware ist bei weitem nicht so schön,
wie man das hier in so einem Blogdiagramm zeichnen kann,
sondern das wird viel,
viel komplizierter.
Das ist halt
eine Zeit lang versucht worden
Was man alternativ machen kann, ist unter der Annahme,
dass halt die Rechenzeiten hier von den Funktionseinheiten größer sind als,
sagen wir mal, die Befehlsdekodierungszeit, dass man halt Befehle holt,
dekodiert und dann auch Funktionseinheiten verteilt, die dann ernsthaft zu rechnen haben.
Und am Ende dann so eine Einheit hat,
die das zusammenführt und verwaltet, wann die einzelnen Befehle fertig sind.
Das läuft unter dem Oberbegriff Scoreboarding, hier die Verwaltung von sowas.
Und das ist jetzt nochmal, das werden wir gleich nochmal sehen,
nochmal komplizierter als so zwei separate Pipelines,
bietet aber im Wesentlichen in die beste Performance, die man jetzt erreichen kann,
was letztlich in dieses Prinzip in allen aktuellen Prozessoren verwendet wird.
So ein bisschen konkreter würde das dann so aussehen, sogenannte Superskalare-Pipeline,
wo der Prozessor halt ein Ein- bis N-Instruktionen aus dem Speicher holt, Instruction Fatch,
dann die Kotmacht, und dann ist jetzt so ein komisches,
so ein komischer Blog Rename dazugekommen.
Und dann werden die Dinger hier auf die Funktionseinheiten zugeteilt,
dann auf die, hier sind die Funktionseinheiten,
dann sind also hier zwei Blöcke dazwischen,
die das auf die Funktionseinheiten aufteilen
Und dann hier dieser Block,
der die einzelnen Ergebnisse dann entgegennimmt und wenn die Funktionen Einheiten
fertig sind und entsprechend dann in die Registrier zurückschreibt.
So, das Rename ist jetzt ein ganz interessanter Punkt.
Je mehr man diese Parallelität hat, desto mehr von diesen komischen Datenabhängigkeiten kommen nämlich zusammen.
Die kann man noch weiter unterteilen. Also wenn man das jetzt formell macht,
nämlich das sogenannte Read After Wright, Right After Read und Right After Wright.
Read After Read ist jetzt egal, also wenn wir irgendwas mehrfach lesen wollen,
dass in irgendeinem Speicher und Register drin ist,
das macht natürlich nichts, das können wir beliebig oft tun.
Read After Wright bezieht sich auf die Situation,
dass wir einen Befehl haben, der Daten lesen möchte.
Das war also vorhin in Folien bei der Pipeline,
sorry, immer dieses Register 2,
was vom ersten Befehl geschrieben wurde und dann von den nachfolgenden Befehlen gelesen worden wollte.
Das heißt, hier haben wir halt die Situation,
die wir mit dem Forwarding in vielen Fällen lösen können,
dass wir halt Instruktionen, die danach folgt,
so lange warten lassen müssen,
bis die Daten,
die wir lesen wollen,
tatsächlich geschrieben worden sind
Es gibt aber immer auch diesen Fall,
dass wir halt mehrere Instruktionen in Bearbeitung haben.
Und eine Instruktion möchte einen Wert schreiben, während eine
von den vorhergehenden Instruktionen den Wert noch nicht gelesen hat.
Das kommt bei einer klassischen Pipeline nicht vor.
Aber wenn wir jetzt hier so mehrere Funktionseinheiten haben,
die unterschiedlich schnell arbeiten, dann kann es halt passieren,
dass irgendeine Operationseinheit schneller fertig ist und irgendwas schreiben möchte,
was eine andere erst noch lesen muss.
Das ist dann also dieses Right After Read.
Und Right After Right, das ist dann halt Abhängigkeit,
wo verschiedene Instruktionen ein Datum schreiben wollen.
Möglicherweise geht das sogar, dass man das tun kann,
wenn es anschließend zwischendurch nicht gelesen wird.
Dass eben Daten erst geschrieben werden dürfen,
wenn die vorhergehende Instruktion auch geschrieben hat.
Gut. Dieses hier, Read After Wright,
haben wir gesehen, das kann man im Prinzip mit Forwarding lösen.
Das wird aber dann, je komplizierter die Pipelines werden,
aufwändiger und aufwändiger und aufwändig.
Und für die anderen beiden Fälle gibt es tatsächlich eine einfache Lösung,
das sogenannte Register We Naming.
Das will ich nur ganz kurz zeigen,
um das Prinzip zu erläutern
Was wir als Programmierer sehen, sind halt die Register,
die in der CPU tatsächlich dokumentiert sind.
Also beim 3,86er, wie wir ihn benutzt haben,
halt der Instruction-Pointer und dann unsere 8 Universal-Register.
Tatsächlich ist es aber möglich, dass auf Hardware-Seite viel mehr Register implementiert sind,
nur dass der Assembler-Programmierer und der Compiler diese Register nie zu Gesicht bekommen.
Das ist also eine Unterscheidung jetzt in die tatsächlich dokumentierten Register,
die der Programmierer sieht und dann die möglicherweise zusätzlichen Hardware-Register
die halt nur der Hardwareentwickler und vielleicht die Systemprogrammierer sind.
Die Idee ist aber ganz einfach. Hier haben wir jetzt
zum Beispiel mit vier Befehlen, die halt jetzt eine Addition machen.
Wir wollen eigentlich A plus B plus C berechnen und D plus E-11. Okay,
eine A plus B und C sind in Registern,
die und E und F sind auch in Registern.
Und weil wir nur zwei Operanten jeweils benutzen können,
müssen wir halt hier ein temporäres Register nehmen.
Und dann können wir halt die Operationen fertigstellen und haben halt
A plus B plus C in das Resultat Register 1 reingeschrieben
Und dann hat der Compiler uns jetzt für die zweite Sache nochmal dieses Register-Temp genommen,
um das Zwischenergebnis zu speichern und dann berechnet er temp-f und dann ist das Resultat 2 fertig.
Das ist also nur ein Typ, ein ganz kleines Gutschnipselchen.
Hier sieht man halt auf den ersten Blick,
dass man statt des Registers Temp möglicherweise noch zusätzlich Register nehmen kann,
aber in komplizierteren Situationen kann man tatsächlich so einen Abhängigkeitsgraf aufbauen,
welche Register tatsächlich verwendet werden, welche nur temporär verwendet werden.
Und für die temporär verwendeten können wir tatsächlich halt Re-Naming machen,
das heißt ein festes Register,
Temp zum Beispiel auf EBX oder sowas zu mappen,
könnten wir das halt auf ein Register verwenden,
was nur intern im Prozessor bekannt ist.
Dann würden wir halt, dass dieses hier auf dem Rechner
tatsächlich sowas hier ausführen. Wir nehmen eins von unseren zusätzlichen Reneming-Registern,
berechnen damit das Resultat 1,
dann machen wir hier ein weiteres Reneming-Register,
berechnen die zweite Operation und falls irgendjemand hier unten das Register Temp noch lesen will,
würden wir dann halt hier das Register Temp,
das tatsächlich vorhandene mit dem Reneming-Register füllen
Was gewinnt uns das? Die Idee ist,
dass wir dieses hier nicht paralysieren können,
weil wir halt hier eine Abhängigkeit von dem Temp,
zu dem Temp haben,
während wir hier in Temp 1 und ein Temp 2 haben.
Und hier ist ein Schreibfehler, der sollte hier auch Temp 1 sein.
Aber ich glaube, das ist klar für jemanden,
der sich die Folge anguckt. Was wir jetzt tun können,
ist, dass wir das parallelisieren können. Wir haben also
jetzt hier Temp 1, A plus B und Resultat 1.
Diese Dinger hier sind unabhängig von den Dingen hier,
sodass wir diese Operationen hier parallel zu der ausführen können und dieses hier jetzt entsprechend auch,
wenn es Temp 2 berechnet ist,
sodass wir halt statt vier Instruktionen für ihr Takte
in diesem Fall
nur zwei Takte brauchen
Sieht das jeder, wie das funktioniert? Ich auch immer. Gut,
dann kommen jetzt ein paar Folien, die im Prinzip zeigen sollen,
wie aktuelle Prozessoren tatsächlich dann aussehen und eine ganze Menge Definition oder sowas.
Das möchte ich jetzt einfach mal weglassen und stattdessen tatsächlich zeigen,
wie so ein aktueller Prozessor aussehen könnte.
Good Core 2 ist jetzt auch nicht mehr ganz,
ganz aktuell, aber immerhin noch einigermaßen ähnlich mit dem,
was in den meisten PCs
derzeit so drinsteckt
Die Idee ist jetzt, dass dieses Diagramm,
was da eben war, mit Befehl holen, dekodieren,
Operanten holen und dann diesen vielen Ausführungseinheiten und diesem Blog ganz hinten,
Wright Back, tatsächlich implementiert ist.
Nur es ist jetzt nicht von links nach rechts, sondern von oben nach unten geplottet.
Wir haben also jetzt hier unsere Instruktionen, die wir dekodieren.
Dieser Blog hier oben sorgt dafür, dass man eben nicht nur eine,
sondern hier jetzt bei der Intel-Architektur bis zu sechs Instruktionen in einem Takt holen kann.
Das wird dann hier erstmal in so eine Q eingeführt,
die bis zu 18 Befehle halten kann,
die der dann gegebenenfalls noch umsortiert werden.
Und die werden dann parallel davon dekodiert,
also hier holen wir uns bis zu sechs Instruktionen aus dem Speicher packen,
das erstmal hier in die Q rein, die das gegebenenfalls umsortiert.
Und dann werden vier von den Dingern tatsächlich in einem Takt dekodiert.
Die können also gestartet werden. Dann versuchen die,
ihre Registerapparanten zu holen. Das heißt,
hier ist dann dieses Ding dabei,
was versucht mit dem Register Renaiming entsprechend Werte zu holen und
Resultatregister bereitzustellen
Und das kommt dann hier in eine Datenstruktur, die bis zu 96 Einträge enthalten kann.
Also 96 Registerwerte, die da parallel gelesen und reserviert sind für Zwischenergebnisse.
Und das geht dann hier in die verschiedenen Funktionseinheiten.
Das ist also jetzt nicht nur eine Alu,
sondern wir haben 1, 2, 3, 5,
6, 7, 8, 9, 10,
11 verschiedene Alus, die da parallel rechnen können.
Und wenn die fertig sind,
dann liefern die ihre Resultate halt hier an diese interne Registerbank zurück,
die halt die Operationen tät,
die gerade in der Bearbeitung sind und alternativ dann halt hier
in den Speicher
Wenn die Operanten fertig sind und zurück in den Speicher geschrieben werden sollen.
Und der eigentliche Speicher ist dann hier nicht gezeigt,
der ist dann hier hinten irgendwo angeschlossen.
In Bezug auf das Pipelining-Bild kann man dann halt ihnen letztlich
so die einzelnen Stufen hier so als Pipeline-Stufen ansehen. Das ist ein,
ich glaube, das war jetzt hier ein bisschen weiter vorne,
nicht für den Chord-Zwei-Prozessor, sondern für so einen Pencil-4-Prozessor,
sondern nochmal gezeigt, wie das dann aussehen kann,
wie so eine Pipeline dann tatsächlich aussieht.
Dieses hier ist halt
das Befehl holen
Dann haben wir die Zuordnung der Register und das Renaming von den einzelnen Apparationen,
dann gibt es dann halt hier vier Stufen,
beziehungsweise sogar sechs für dieses Scheduling und das Despatch auf die einzelnen Funktionseinheiten.
Er ist in Stufe 15 und 16,
wir werden dann die Registerwerte geholt,
also hier wird erstmal entschieden,
von wo es kommen soll und die eigentliche Ausführung ist jetzt,
tatsächlich sind die Anus jetzt so schnell,
dass das halt hier in dem Ding tatsächlich nur einen von diesen 20 Takten benötigt,
nachdem wir die Register gemacht haben,
dann halt hier diese Statusflex berechnet und irgendwann am Ende
die Werte zurückgeschrieben
Das ist also hochgradig kompliziert. Und die echten Details,
die sieht halt bestenfalls der Chip-Designer. Das kann man im Einzelnen
kaum noch nachvollziehen, was da eigentlich tatsächlich passiert. So,
das wäre dann, also was ein einzelner Prozessor so hergeben kann,
wenn wir halt nicht nur eine Pipeline nehmen,
sondern wenn wir mehrere Pipelines nehmen.
Aber auch das reicht noch nicht. Mit der Taktfrequenz
sind wir halt relativ stark am Limit der Zeit,
also 10 Gigahertz wäre vielleicht gerade noch möglich,
ist aber nicht wirtschaftlich nützlich,
weil man dann sehr stark kühlen müsste
Also ist die Alternative tatsächlich nur, dass wir mehrere Prozessoren kaufen.
Und dann kommen wir halt in das Problem,
dass wir diesen Overhap berücksichtigen müssen und die Verbindungsstruktur zwischen den
einzelnen Prozessoren, einzelnen Rechnern uns anschauen müssen.
Programmierung ist weitgehend ungelöst. Man kann natürlich seine Streets kaufen oder sowas,
aber wirklich effiziente Umsetzung von klassischen Algorithmen auf solche Rechner wird
immer noch im Einzelfall gemacht.
Gut, und der aktuelle Kompromissen teilt den sogenannten Shared
Memory Prozessoren mit, sagen wir mal,
2 bis 16 Kernen
Aktuelles Smartphone mit einem Vier- oder Acht-Kern-Prozessor und aktuelle PCs mit vier oder sechs oder sowas, sind dann halt der aktuell wirtschaftliche Kompromiss. So,
dazu gibt es eine Klassifikation bei diesem Parallelrechnern,
die nur sagen, wenn man nur bedingt nützlich ist,
die aber überall vorkommt, die sogenannte Flynn-Klassifikation,
das ist irgendwann aus den 60ern, SESD,
Single Instruction, Single Data, das ist das,
was sie uns bisher angeschaut haben und jetzt gibt es bei Verwendung von mehreren Prozessoren halt mehrere Möglichkeiten,
wir können einen Befehl auf mehrere Datenelemente anwenden, also SEMD,
Single Instruction Multiple Data
Das ist tatsächlich recht geliebt. Das bedeutet halt,
dass wir eine Instruktion, wie zum Beispiel Addition oder Multiplikation,
nicht nur auf ein Registerpaar anwenden,
sondern auch ein ganzes Array oder eine ganze Matrix,
das kann man halt im kleinen Stil machen.
Bei den Multimedia-Befehlssätzen, sagen wir mal so ein bisschen auf acht Operanten,
wurde im großen Stil, Beispiel Connection Machine 2,
bis zu 64K-Prozessoren, die aber dann entsprechend leistungsschwach waren.
Und alternativ in das hier, dass man wirklich unabhängige Prozessoren hat,
die unabhängige Programme abarbeiten, also Multiple Instruction, Multiple Data.
Also klassisch multiple Mirkern CPUs, Multikor oder nun Rechner Cluster,
also zum Beispiel, dass Informatiknetz mit mehreren hunderten Workstations oder sowas,
wenn man die alle benutzt, hätte man ein MEMD-System.
Gut, in der ursprünglichen Klassifikation,
so jetzt zweimal zwei Matrix ist dann auch der Fall,
Multiple Instruction,
Single Data da,
der macht natürlich in der Praxis
nicht viel Sinn
Das kann man aber auch feiner aufteilen,
also jetzt nicht nur hier die Klassifikationen von Flynn hier mit vier Klassen,
sondern hier haben wir halt den Einzel von Neumann-Prozessor,
dann haben wir hier die Single Instruction Multiple Data und im Wesentlichen halt zwei Versionen,
entweder, dass wir Datenoperationen auf Lektoren anwenden oder auf Matrizen,
also Vektor oder Feldrechner und dann hier halt den wichtigen Punkt mehrere unabhängige CPUs,
Prozessoren,
Multiple Instruction Multiple Data und dann kann man gucken,
sind es einfach nur Prozessoren,
die gemeinsamen Speicher haben oder sind es separate Rechner,
die halt unabhängigen Speicher haben
Und ich möchte jetzt hier auf die ganzen Abkürzungen gar nicht groß eingehen.
Also das ist der klassische Einzelprozessor bei diesem Single Instruction Multiple Data,
gibt es gängig eben Vektoren oder Matrizen zur Ausfälle,
Ausführung und dann sind wir halt hier bei mehr CPU-Systemen und
hierbei tatsächlich unabhängigen Rechnern.
Ja, das macht jetzt wenig Sinn. Darin,
es sind keine technischen Daten zu dem System.
Das System ist trotzdem einigermaßen legendär,
weil es über viele Jahre halt einer der
schnellsten verfügbaren Rechner war
Da sieht man dann mal, so wie früher Hardware gebaut wurde. Das ist also jetzt nicht ein Mikroprozessor,
sondern hier sind in diesem Turm sind viele,
viele einzelne Prozessoren und in der Mitte dieses Blaue,
das kann man jetzt nicht so genau sehen,
sind halt viele, viele Kabelstränge, die dann zwischen rumhängen.
Das kann man halt mittlerweile miniaturisieren und aktueller Mikroprozessor schafft leicht
diese Leistung hier. Die Idee war aber von vornherein hier,
dass man eben einen Befehlssatz hat,
bei dem man nicht nur Skalabor-Operationen macht,
also Operanten auf einzelnen Registern,
sondern von vornherein auch für Vektoren oder auch immer hat Matrizen und dann eben einen Befehl hat,
sagen wir mal,
addiere dann eben einen ganzen Vektor gemacht und die Hardware hat dann automatisch diese Adressberechnung berechnet um,
was weiß ich, bei der Matrix Multiplikation halt einmal zeilenweise und einmal spaltenweise die entsprechenden Operanten bereitzustellen.
Gut, das Innere ist der Rechner und hier in den Sitzkissen drin,
da ist die Kühlung. Da sieht man dann schon mal,
wie damals so der Kompromiss war, wie viel Platzpunkt. Nein,
die Idee mit dem eine,
der eine Befehl gleichzeitig auch eine ganze Matrix anzuwenden,
führt dann natürlich zu solchen Rechnern,
wo wir halt hier den Kontrollrechner haben und dann die einzelnen Datenrechner als Feld,
in diesem Fall, glaube ich, 8 mal 8 Prozessoren,
also 64 Stück, die jeweils eigene Alus und sowas haben.
Das heißt, ein Befehl hier wird in dieses ganze Feld geschickt.
Die arbeiten dann alle denselben Befehl ab, aber mit unterschiedlichen Daten,
also zum Beispiel auf unterschiedlichen Elementen von so einer Matrix.
Und das Problem bei solchen Dingern ist dann halt immer das Programm,
was man gerade hat, effizient auf die Architektur abzubilden.
Also bei diesem hier muss man halt immer irgendwelche Vielfache von 64 Operationen haben,
sonst liegt ein Teil des RRS brach und wenn man irgendwas hat,
was halt nur seriell abgearbeitet wird,
dann ist halt der hier beschäftigt und die anderen in 63 Dinger stehen einfach nur rum.
Also wie der andere als Gesetz. Sowas hier lohnt sich
halt nur, wenn wir halt Algorithmen haben,
der sich tatsächlich hauptsächlich auf entsprechend viele Prozessoren verteilen lässt.
Deswegen ist dieses hier im Moment beliebter und tatsächlich auch umgesetzt,
halt mehrere letztlich unabhängige CPUs, die entweder das gleiche Programm oder halt
unterschiedliche Programme bearbeiten können und an einem gemeinsamen Speicher hängen.
Hier sieht man dann schon,
dass das wahrscheinlich mit der Skalierung schwer wird,
wenn wir halt hier
nur einen Speicher haben
Deswegen macht das tatsächlich nur Sinn,
wenn jede CPU tatsächlich auch über lokalen Speicher verfügt,
damit die dann aber untereinander Nachrichten austauschen können,
muss man halt trotzdem hier so eine Busstruktur haben.
Und typischerweise eben lokalen,
schnellen Speicher für jeden einzelnen CPU und dann einen größeren,
etwas längeren, langsameren Speicher, der dann gemeinsam genutzt wird.
Das ist vergleichsweise preiswert zu realisieren,
insbesondere seit man halt diese Dinge hier auf einen Chip
integrieren kann und hat entsprechend dann eben auch hohen Markterfolg,
während sowas hier halt wirklich nur für Leute interessant ist,
die irgendwelche Superrechner kaufen wollen
Dann gibt es jetzt halt noch die Frage,
wie der Speicher organisiert ist und wie die Dinger miteinander kommunizieren.
Die paar Folien dazu möchte ich heute noch zeigen.
Und dann kann Andreas nächste Woche die Details dazu machen.
Wir sind also jetzt in der Situation,
dass wir nicht einen einzelnen Instruktionsstrom,
ein einzelnes Programm haben, sondern eine Reihe von CPUs,
die unabhängig voneinander arbeiten und unabhängig voneinander auf den Speicher zugreifen
So, jetzt ist ja halt die Frage, was man
hier machen kann. Und das ist eine Illustration hier aus
dem Buch von Tannenbaum, finde ich ganz passend.
Hier wird halt so eine Bildverarbeitungsaufgabe auf mehrere CPUs aufgeteilt.
Und das lässt sich offensichtlich relativ leicht dadurch parallelisieren,
dass wir halt das Gesamtbild in so kleine Blöcke aufteilen und
jeder Prozessor arbeitet auf seinem lokalen Blog.
Und wenn die miteinander kommunizieren wollen, dann haben wir
halt hier in diesem Fall den gemeinsamen Speicher.
Und das funktioniert halt bis zu einer gewissen Anzahl von CPUs noch ganz gut.
Bei größerer Anzahl von Prozessoren wird man und dann halt
diesen gemeinsamen Speicher irgendwann durch so ein Verbindungsnetz ersetzen.
Und dann haben wir halt die Ms, also die Memories,
dann jeweils lokal für jeden einzelnen Prozessor.
Und die Frage ist jetzt, wie so ein Nachrichtenverbindungsnetz aussieht.
Hier jetzt für unsere Bildverarbeitung sind halt die einzelnen Teilblöcke des
Bildes an die einzelnen Prozessoren verteilt worden.
Die müssen sich also jetzt hier nicht mehr um die Daten
mit den anderen Prozessoren schlagen, sondern können ihre Teilblöcke unabhängig voneinander verarbeiten.
So, welche Verbindungsnetzwerke gibt es und welche sind populär? Eine Möglichkeit ist halt so eine zentrale Verwaltung mit einem Stern,
Nee, rein intuitiv, klar,
dass halt hier dieses Ding im Zentrum des Sterns offensichtlich besondere Bedeutung hat,
besonders leistungsfähig sein muss und die einzelnen Knoten hier außen,
die, so was weiß ich,
denken sie zum Beispiel an einen normalen PC oder sowas und dann haben wir hier in der Mitte einen Router oder sowas, der die Dinger verknüpft.
Das ist aber schon klar,
dass also offensichtlich hier ein Knotenpunkt und ein Bottleneck auftreten wird,
die Alternative vollständige Vernetzung kann man normalerweise nicht leisten,
weil einfach,
weil das viel zu viele Leitungen werden mit größerer Anzahl von einzelnen Rechnern
Deswegen sind halt, also dieses hier führt hier zu Konflikten,
der ist halt sehr, sehr schnell überlastet.
Und dieses hier sind zu viele Leitungen. Deswegen gibt es
dann Dinge wie Warmstrukturen, sodass man halt hier lokal kommunizieren
kann und nur weil Bedarf über die Wurzel gehen muss.
Oder Strukturen wie ein Ring. Gitter sind recht beliebt.
Oder halt sowas wie so eine Toro-Struktur. Das ist halt ein Gitter,
was dann außenrum halt wieder Verbindung von hier einmal außenrum
hat und hier dann gegebenenfalls außen herum hier wieder lang.
Das kann man einigermaßen skalieren,
weil sich halt hier die Anzahl der Verbindungen pro Rechner Grenzen hält,
also hier jetzt jedenfalls vier Verbindungen zu den Nachbarn und hier beim Würfel
wären das dann halt bei größeren Würfeln entsprechend acht Verbindungen pro Rechner,
das kann man sich dann vielleicht noch leisten.
Gut, eine Möglichkeit, um die Speicher an die CPUs anzubinden,
ist dieses hier sogenannte Kreuzschienenverteiler, also ein sehr,
sehr schönes deutsches Wort, oder auf Englisch Crossbar Switch.
Hier haben wir die CPUs,
die halt jetzt jeweils von links nach rechts
so Verbindungsleitungen haben und es gibt Knotenpunkte und die Knotenpunkte
haben nur einen Schalter und über die vertikalen Leitungen sind halt
die Speicher angebunden
Das erfordert vergleichsweise hohen Hardware-Aufwand. Wir haben also offensichtlich,
sagen wir mal, N-CPUs, Endspeicher,
dann haben wir hier N-Quadrat von diesen Scheitern.
Aber wir können in jedem Tag halt zumindest theoretisch jede CPU mit jedem Speicher verbinden.
Natürlich wird es dann hier so eine Entscheidungslogik geben müssen,
die sagt, das wären halt mehrere CPUs auf denselben Speicher.
Also hier zum Beispiel Speicher 2 zugreifen wollen, welche davon dann priorisiert ist.
Aber im Prinzip ist das ein leistungsfähiges System,
wo du halt so eine Matrix von Scheitern haben und dir
jede CPU letztlich in einem Takt an einen Speicher herankommen kann.
Wenn der Aufwand zu hoch ist, dann nimmt man gegebenenfalls sowas hier.
Sogenannte Omega-Netzwerk. Da kann man jetzt nochmal so ein bisschen
die Eigenschaften des Binärcodes noch verfolgen. Die Idee ist jetzt halt,
dass wir einen Logarithmus von N-Stufen haben in einem Baum.
Und jeder von diesen Scheitern kann halt entweder gerade oder gekreuzt werden,
sodass jetzt, was weiß ich, CDU 0 halt,
wenn es gerade gerade ist, auf den Speicher 0 zugreift.
Und wenn wir oft hier zum Beispiel auf diesen Speicher zugreifen wollen,
dann müssen wir uns jetzt hier so einen Verbindungsweg da durchsuchen
Wie das tatsächlich funktioniert,
dann müssen wir so von hier gekreuzt nach hier,
gekreuzt nach hier und dann vielleicht nochmal gerade nach hier,
sodass im Prinzip jede CPU Zugriff auf alle Speicher hat.
Aber es gibt halt bedeutend weniger Kombinationsmöglichkeiten als hier bei
dem Kreuzschen Verteiler. Also hier sind halt vielen,
vielen mehr gleichzeitige Kombinationen möglich als hier, weil abhängig davon,
wie ein so ein Schalter gestellt ist,
jeweils zwei CPUs entsprechend bereits ihre Wahl getroffen haben
Gut, also deren Kreuzschienenverteiler hat halt einen Hardware-Aufwand von im Quadrat.
Dieses hier ist billiger, mit N-Logarhythmus N. Und tatsächlich wird
man halt in vielen Fällen auch das nicht haben,
sondern man hat einfach dieses hier,
man hat die Dinger über einen gemeinsamen Bus miteinander verbunden.
Sie erinnern sich, Steuerleitung, Adressleitung,
Datenleitung und dann so ein Protokoll, wie so ein Buszugriff ausgeführt werden kann.
Dieses hier macht noch Sinn, also was weiß ich,
ein 4K-Prozessor mit einem gemeinsamen Weg zur Außenwelt.
Hier sieht man dann schon, dass das offensichtlich
irgendwann schnell an Limits kommt
Bei 16 Prozessoren mit einem gemeinsamen Bus
wird es schon knapp und es ist völlig klar, das,
was man sich in mir hier tausend Dinger da anschließen an dem Bus,
dass das nicht funktionieren wird,
dass wir dann halt ab einer gewissen Anzahl von Knoten zu solchen Gitterstrukturen oder Formelga-Netzwerken übergehen müssen.
So, damit möchte ich dann erstmal Schluss machen für heute
und beim nächsten Mal wird es dann weitergehen mit so ein paar Beispielen,
insbesondere eben mit dem Shared Memory Multi-Prozessor-Konzept,
wie man dann halt Daten paralysieren kann.
Was Sie sich merken sollten, ist halt für heute
das Konzept von Pipelining und dieses Amda
als Gesetz
Wenn man versucht, irgendwas schneller zu machen,
bitte nicht versuchen, irgendwas zu beschleunigen,
was überhaupt gar nicht beiträgt, das lohnt sich halt überhaupt nicht.
Okay, damit bedanke ich mich für die Aufmerksamkeit und
nächste Woche sollte dann Andreas
wieder übernehmen