Langsam nähern wir uns dem Ende des technischen Teils dieser Reihe. Das letzte mal wurde das Zweierkomplement erklärt und dargelegt, wie Subtraktion im Rechner funktioniert. Zusätzlich haben wir gesehen, wie ein einfaches Rechenwerk arbeitet; eine wichtige Grundlage der Funktionsweise sind hier die Steuersignale. Und deren Herkunft wollen wir heute klären.


Zuerst gibt es allerdings etwas Theorie.
Die Grundlage der Steuersignale sind sogenannte Instruktionen; eine Instruktion ist eine Bitkette, die eine bestimmte durch den Prozessor auszuführende Aufgabe codiert, zum Beispiel “addiere den Wert 5 auf den im Akkumulator gespeicherten Wert” (wir erinnern uns: der Akkumulator war ein spezielles Register, welcher Ergebnisse und Operanden für das Rechenwerk verwaltet). Eine Instruktion besteht in der Regel aus einem Opcode, welcher die eigentliche Operation (z.B. “addiere”) beschreibt, sowie einer optionalen Anzahl von Operanden (also etwa die zu addierenden Zahlen). Operanden können feste Werte sein, aber auch bestimmte Register beschreiben oder lediglich eine Adresse auf eine bestimmte Speicherzelle enthalten (dazu später noch etwas); die Interpretation der Operanden hängt immer vom Opcode ab. Die Länge einer Instruktion und der einzelnen Bestandteile muss übrigens nicht fest sein, sondern kann auch variabel sein, je nach zugrundeliegender Prozessorarchitektur.

Ein kurzes (fiktives) Beispiel: die Instruktion “0110110101” kann etwa in die Bestandteile 0110, 11 und 0101 unterteilt werden, wobei “0110” der Opcode ist und zum Beispiel die Anweisung “addiere einen festen Wert zu dem in einem Register gespeicherten Wert” codiert, “11” das zu benutzende Register (zum Beispiel den Akkumulator) beschreibt und “0101” schließlich den festen Wert, hier eine 5, codiert. Zusammengenommen ergibt die Bitkette also die Instruktion “addiere den Wert 5 auf den im Akkumulator gespeicherten Wert”.

Die Abarbeitung von Instruktionen durch das sogenannte Steuerwerk im Prozessor erfolgt bei der Von-Neumann-Architektur (welche die Grundlage der meisten modernen Rechner bildet) nach dem Von-Neumann-Zyklus. Dieser besteht üblicherweise aus 4 Phasen:

  • Fetch: die nächste auszuführende Instruktion wird aus dem Speicher geladen und im sogenannten Instruktionsregister gespeichert
  • Decode: die Instruktion im Instruktionsregister wird dekodiert, also grob gesagt in ihre Bestandteile “zerlegt”; konkret werden hier unter anderem die Steuersignale aus der Instruktion extrahiert
  • Fetch Operands: eventuelle Operanden aus dem Speicher werden in die entsprechenden Register geladen
  • Execute: die Anweisung wird ausgeführt

Soweit zur Theorie. Die klingt zwar immer recht schön, lässt aber offen, was nun in den einzelnen Phasen genau passiert. Schauen wir also einmal etwas genauer hinein; dazu benötigen wir noch einige neue Bauelemente, die jeweils an der entsprechenden Stelle näher beschrieben werden.

Phase 1: Fetch

Bevor eine Instruktion verarbeitet werden kann, muss sie erst einmal zur Verfügung stehen, da der Prozessor nur mit Werten arbeiten kann, die in einem seiner Register vorliegen. Jetzt ist die Anzahl der Register eines Prozessors in der Regel sehr begrenzt; es stehen auf keinen Fall genügend zur Verfügung, um viele Instruktionen (welche zusammen übrigens ein komplettes Programm ergeben!) aufzunehmen. Für die Speicherung von Instruktionen existiert im Prozessor ein separates Register (in modernen Rechnern sogar mehrere davon), welches Instruktionen speichert, das sogenannte Instruktionsregister. Die Instruktionen selbst stehen woanders, in der Regel im Cache oder im Arbeitsspeicher.

Ohne jetzt allzu sehr ins Detail zu gehen: der Cache ist ein kleiner, sehr schneller Speicher, der sich physikalisch in der Nähe des Prozessors (oder sogar in ihm) befindet und unmittelbar benötigte Daten aufnimmt. Je nach Nähe zum Prozessor werden verschiedene Cache-Level unterschieden; so haben moderne Prozessoren in der Regel einen Level-1-Cache, der direkt im Prozessor selber integriert ist, im gleichen Takt wie dieser läuft und eine Größe im Bereich von bis zu wenigen hundert Kilobyte hat. Dazu gibt es in der Regel noch mindestens einen Level-2-Cache außerhalb des Prozessors, der langsamer arbeitet und bis zu einigen Megabyte groß sein kann. Irgendwo weiter hinten in der Speicherhierarchie findet sich dann schließlich der Arbeitsspeicher, der langsamer als die Caches arbeitet, dafür aber auch bedeutend größer ist (momentan sind etwa 4 Gigabyte relativ üblich). Irgendwo in einem Speicher dieser Hierarchie steht auch die nächste Instruktion, die der Prozessor bearbeiten soll, und die gilt es zu finden. Hierfür steht ein separates Register zur Verfügung, der sogenannte Program Counter (PC). Dieser enthält die Adresse der nächsten Instruktion (in Form einer Bitkette), anhand derer die betreffende Instruktion im Speicher gefunden und ins Instruktionsregister geladen werden kann.

Üblicherweise hören Beschreibungen der Funktionsweise eines Prozessors an dieser Stelle auf, aber im Interesse der Vollständigkeit wollen wir hier doch einmal ein wenig tiefer in die Materie schauen; das Laden der Instruktion aus dem Speicher in ein Register ist nämlich an sich ein interessanter Vorgang.

Nehmen wir einmal an, die Instruktion wird aus dem Level-1-Cache geladen (was in der Regel auch der Fall ist). Der Cache ist aus sogenannten SRAM-Zellen aufgebaut; das sind im Grunde Flipflops mit einer speziellen Steuerelektronik. Untenstehende Abbildung (entnommen aus der Wikipedia) zeigt eine derartige Zelle, die ein einzelnes Bit speichern kann.

i-220068e80bcfea5ee24aba825ee485e4-SRAM.svg.png

Die Zelle besitzt die 3 Anschlüsse WL (Word Line), BL und BL. WL dient zur Aktivierung der Zelle – solange an WL 0 anliegt, geben BL und BL eine 0 aus. Soll nun der Wert des in der Zelle gespeicherten Bits ausgelesen werden, wird WL auf 1 geschaltet, woraufhin an BL der gespeicherte Wert (0 oder 1) anliegt (an BL entsprechend der invertierte Wert). Zum Schreiben eines Wertes in die Zelle reicht es, den gewünschten Wert an BL anzulegen (an BL wiederum entsprechend den invertierten Wert) und WL anschließend wieder auf 1 zu setzen (für die technisch Versierteren: damit das funktioniert, muss die an BL/BL angelegte Spannung größer sein als die in den Transistoren gespeicherte, um den aktuellen Wert zu überschreiben). Schaltet man mehrere dieser Zellen in einer zweidimensionalen Matrix (also einem Gitter) zusammen, so kann man damit auch größere Werte speichern – und so sind auch Caches aufgebaut. Üblicherweise hat das Gitter dabei so viele Spalten, wie ein Byte Bits hat (meistens 8, aber nicht immer) und entsprechend so viele Zeilen, wie Bytes im Cache gespeichert werden sollen. Über das Ansprechen einer bestimmten Zeile kann demzufolge ein Byte aus dem Cache ausgelesen werden. Ein Cache mit 5 Bytes, die jeweils aus 4 Bit bestehen, könnte also folgendermaßen aussehen; die einzelnen Bytes werden jeweils über die Eingänge b1 bis b5 angesprochen, wobei die einzelnen Bits der Zeilen an den Ausgängen o1 bis o4 anliegen (die invertierten Ausgänge der SRAM-Zellen habe ich einmal weggelassen):

i-b5be03f6ed9fbd3fa00121e505c079d8-CacheGrid.png

Bleibt noch zu klären, wie denn nun eine bestimmte Zeile angesprochen werden kann. Hierfür wird in der Regel ein sogenannter Decoder benutzt (den wir nachher auch noch einmal benötigen werden). Vereinfacht gesagt ist ein Decoder ein Bauelement, welches ein Signal in ein anderes umwandelt. Das wichtigste dabei ist, dass das Eingangs- und das Ausgangssignal nicht die gleiche Länge haben müssen – ein Decoder kann also etwa eine Bitkette (die ja nichts anderes ist als ein Signal) der Länge 2 in eine Bitkette der Länge 4 überführen. Hierbei können natürlich nie sämtliche Bit-Kombinationen an den Ausgängen erreicht werden (bei einem Eingangssignal mit 2 Bit sind eben nur insgesamt 4 Kombinationen möglich), aber das benötigt man auch gar nicht. Da der Aufbau eines Decoders in der Regel von der Signaltransformation abhängt, die er durchführen soll, spare ich mir diesmal das interaktive Bildchen; nur so viel: er lässt sich mit einfachen Grundbauelementen wie UND-Gattern und Invertern bauen. Im Folgenden der schematische Aufbau eines beispielhaften Decoders mit einem 2-Bit-Signal als Eingang und einem 4-Bit-Signal als Ausgang, gleich mit Wertetabelle (Quelle ist mal wieder die Wikipedia):

i-951bec0de4b6a5e3f56d26a9c32f6ec2-Decoder.svg.png

An dieser Schaltung lässt sich auch gleich sehen, wie ein Decoder helfen kann, einzelne Zeilen in einem Cache anzusprechen, indem er nämlich ein Eingangssignal (die Adresse) immer so transformiert, dass an lediglich einem Ausgang eine 1 anliegt (ein Decoder muss nicht zwangsläufig so arbeiten – die Ausgaben können beliebige Bitketten sein). Mit obigem Decoder könnten 4 Bytes adressiert werden, bei mehr Eingängen dann auch entsprechend mehr Bytes. Formal ausgedrückt: bei einer Adressbreite von n Bits können 2n Bytes adressiert werden; bei den heute üblichen Rechnern mit 32 Bit wären das 232, also etwas mehr als 4 Milliarden Bytes – das entspricht 4 Gigabyte (deswegen bringt es übrigens auch nichts, in einen Rechner mit einem 32-Bit-Betriebssystem mehr als 4 Gigabyte RAM einzubauen – sie können schlicht nicht vollständig adressiert werden).

Damit sind eigentlich die wichtigsten Informationen für die Fetch-Phase verfügbar. Fassen wir sie also noch einmal zusammen:

  1. Die im Program Counter im Steuerwerk gespeicherte Adresse wird an den Decoder des Caches übermittelt (das geschieht übrigens über den sogenannten Adressbus – eine Datenleitung im Computer, die einfach mehrere getrennte Bauelemente miteinander verbindet und so auch die Datenübermittlung über größere Entfernungen ermöglicht).
  2. Der Decoder wandelt das Adresssignal derart um, dass eine einzelne Zeile im Cache aktiviert wird.
  3. Die nun am Ausgang vom Cache anliegende Bitkette wird (wieder über einen Bus, diesmal den Datenbus) zurück an das Steuerwerk übermittelt, wo sie als Instruktion im Instruktionsregister gespeichert wird.
  4. Als letztes wird noch der Program Counter aktualisiert, so dass er auf die nächste Instruktion im Speicher verweist.

Phase 2: Decode

Die größte Magie ist jetzt eigentlich schon vorbei, spannender wird es nicht mehr. In der zweiten Phase wird die nun im Instruktionsregister geladene Instruktion durch einen Decoder im Steuerwerk geschleust (wobei das Register seine gespeicherten Werte zum Beispiel einfach direkt an den Decoder ausgebe könnte), der sie in ihre relevanten Bestandteile (also den Opcode und eventuelle Adressen, Register-Identifizierer und konkrete Werte) zerlegt. Der Decoder hier arbeitet genauso wie der im Cache, nur dass diesmal eben keine einzelne Bitkette mit nur jeweils einer einzelnen 1 darin entsteht, sondern mehrere, variablere Bitketten. Nach dem Ende der zweiten Phase ist also bekannt, was gemacht werden soll und welche Daten dafür zu benutzen sind.

Phase 3: Fetch Operands

Genauso, wie in der ersten Phase die Instruktion aus dem Speicher geladen wurde, werden in der dritten Phase eventuell benötigte Daten geladen; die Adressen hierfür wurden in der vorherigen Phase ja aus der Instruktion extrahiert, sind also bekannt. Das Vorgehen ist hierbei identisch zu dem in Phase 1, lediglich mit dem Unterschied, dass die geladenen Daten in anderen Registern gespeichert werden.

Phase 4: Execute

In der letzten Phase werden schließlich alle gesammelten Daten vereint und die geladene Instruktion ausgeführt. Vereinfacht ausgedrückt ist ein Prozessor dabei so verdrahtet, dass durch den gespeicherten Opcode automatisch die passenden Steuersignale an die benötigten Teile des Prozessors gesendet werden; wenn wir an das Rechenwerk aus dem letzten Teil denken, könnte durch den Opcode also das Steuersignal für den Addier-/Subtrahiereingang generiert werden.

Zusammenfassung

Wenn man sich jetzt den ganzen Prozessor einfach um noch viel mehr (Rechen-)Funktionen angereichert vorstellt, hat man ein ziemlich gutes Bild davon, wie so ein Computer tief im Inneren funktioniert. Auch moderne Prozessoren funktionieren nach dem in diesem und den letzten Kapiteln beschriebenen Prinzip. Natürlich sind sie weitaus komplexer in der Hinsicht, dass sie mehr Register, ein viel größeres Rechenwerk und insgesamt über mehr Funktionalität verfügen, aber das Prinzip ist wirklich das gleiche.

Damit haben den technischen Teil der Arbeitsweise von Computern geklärt. Wir haben die theoretischen Grundlagen des dualen Zahlensystems geklärt; wir haben besprochen, wie aus Transistoren einfache logische Gatter aufgebaut werden können; anschließend wurde gezeigt, wie mit Hilfe dieser logischen Gatter zunächst einzelne Bits miteinander verrechnet und gespeichert werden können; danach haben wir uns mit der Speicherung von ganzen Bitketten beschäftigt und komplexere Rechenoperationen besprochen; heute schließlich wurde erklärt, wie all die vorher vorgestellten Methoden zusammenwirken und einen Prozessor formen.

Wenn im Kommentarbereich keine besonderen Wünsche über weitere technische Bestandteile eines Rechners, die hier noch besprochen werden sollen, geäußert werden, würde ich diese Reihe hiermit für (erfolgreich) beendet erklären und mich in Zukunft der Programmierung von Computern zuwenden. Wir haben schließlich zwar geklärt, wie ein Computer Instruktionen verarbeiten kann, aber woher diese Instruktionen letzten Endes kommen, ist noch offen – und hier kommt der Programmierer ins Spiel. Aber dazu in Zukunft mehr.

Kommentare (12)

  1. #1 etth
    August 24, 2011

    Jetzt wo bekannt ist, wie ein Prozessor Zahlen addieren kann stellt sich mir die Frage, ob die Addition denn reicht, um alle notwendigen Operationen auszuführen. Die Division ist meines Wissens die einzige Rechenoperation, die nicht als Addition umschrieben werden kann. Wie wird diese durchgeführt? Und tun die Dinger wirklich nicht mehr als so grundsätzliche Rechenoperationen durchzuführen?

  2. #2 Marcus Frenkel
    August 24, 2011

    @etth
    Die Division mit ganzen Zahlen (also Division mit Rest) lässt sich ziemlich einfach als wiederholte Subtraktion umschreiben. Von der ersten Zahl wird die zweite einfach so lange abgezogen, bis ein Wert < 0 rauskommt - dann hat man das Ergebnis und den Rest. Etwas anders sieht es natürlich bei reellen Zahlen aus - aber vor denen habe ich mich mit voller Absicht etwas gedrückt, da insbesondere die Erklärung der Rechenoperationen hier ziemlich umfangreich werden würde. 😉 Teilweise arbeiten Prozessoren mit den grundsätzlichen Rechenoperationen, teilweise sind bestimmte Operationen aber auch anders umgesetzt; da sind schon einige Tricks möglich, die dann auch meist im Rechenwerk fest verdrahtet werden. Die Multiplikation wird zum Beispiel auch nicht einfach über wiederholtes addieren umgesetzt (was ginge, aber langsam wäre), sondern über eine Kombination aus Addition und Verschiebungen (Shiften, dafür waren die Schieberegister gut). Die Rechenoperationen beschränken sich natürlich auch mehr oder weniger auf die ALU - außerhalb davon kann der Prozessor natürlich auch noch Dinge, vor allem Daten hin- und herschicken und (de-)codieren. Aber ein Großteil der Arbeit findet schon in der ALU statt - nicht umsonst wird die Leistung eines Prozessors auch in FLOPS, also floating point operations per second (im Grunde Berechnungen) angegeben.

  3. #3 Dagger
    August 25, 2011

    Schöne Serie! Erinnert mich gut ans Studium 🙂

  4. #4 Jan von nebenan
    August 26, 2011

    Sehr schöner Artikel! Ein paar technische Sachen gäbe es vielleicht schon noch, aber ich weiß nicht, ob das nicht zu tief in die Materie geht. Ich fände z.B. interessant, mal auf die Features der neueren Prozessoren einzugehen. Die sich ja nicht nur deswegen so schnell, weil die Taktfrequenz höher ist: SIMD, Pipelining etc. wären solchen Themen, oder auch “intelligente” Funktionen wie branch prediction (Prozessor versucht zu ermitteln, ob eine If-Bedigung wahr oder falsch sein wird, und lädt entsprechende Daten vor – Trefferquote bis 98% bei modernen CPUs).

    Und eine Sache noch: Misst man die Leistung der ALU nicht in MIPS (million instructions per second)? FLOPS bezieht sich doch auf die FPU, die du ja geschickt ausgelassen hast… 😉

  5. #5 Marcus Frenkel
    August 26, 2011

    @Jan von nebenan
    Ui. Branch prediction ist natürlich ein ganz schöner Brocken. 😉
    Pipelining wäre aber in der Tat ganz interessant; ich hatte überlegt, es noch in diesem Artikel zu bringen, es dann aber auf Grund der Länge gelassen. SIMD ginge ja Richtung Prozessorarchitektur – da müssen dann auch noch SISD, MISD und MIMD erklärt werden. Ich überleg mir mal, ob das schon zu technisch ist oder nicht…oder zu magisch, da ich da auch nicht alles erklären kann und oft genug auf die Verdrahtung in der CPU verweisen muss. 😉

    Das mit den FLOPS war etwas ungeschickt ausgedrückt, ja. Man misst die Leistung einer ALU unter anderem in FLOPS (und meint dann damit die FPU darin). Die MIPS gibt es ja auch, aber die sind ähnlich aussagekräftig wie die Taktrate. 😉

    Und mit der FPU werde ich mich nur beschäftigen, wenn hier mindestens 5 Leute einen derartigen Wunsch äußern – Gleitkommawerte sind einfach zu garstig und mathematisch. 😉

  6. #6 icehawk
    August 27, 2011

    Erstmal möchte ich sagen, dass ich den Wunsch von Jan teile und auch gerne etwas über moderne Features lesen würde.

    Aber zu deiner einen Anmerkung:
    “für die technisch Versierteren: damit das funktioniert, muss die an BL/BL angelegte Spannung größer sein als die in den Transistoren gespeicherte, um den aktuellen Wert zu überschreiben”
    Das verwirrt mich etwas: Braucht man dann nicht mit der Zeit immer höhere Spannungen? Bei mehreren Ghz kommen ja schon ein paar Überschreibvorgänge zusammen: Wann geht der CPU der Saft aus? ^^

  7. #7 Marcus Frenkel
    August 27, 2011

    @icehawk
    Das Wort “gespeichert” war hier etwas unglücklich gewählt – ein Transistor speichert natürlich keine Spannung, das wäre ein wenig schwierig. Aber die beiden Transistor-Paare in der Mitte “speichern” ein Bit dadurch, dass sie sich in einem von zwei sogenannten stabilen Zuständen befinden, was die Verteilung der Leitfähigkeit und damit Spannung angeht – es reicht übrigens eine sehr niedrige Spannung, um so einen Zustand aufrecht zu erhalten. Zum Überschreiben muss kurzfristig eine stärkere Spannung angelegt werden, um die bisherige zu “überschreiben” – aber die stärkere Spannung bleibt nicht in den Transistoren bestehen. Somit braucht man auch nicht immer höhere Spannungen.

  8. #8 Engywuck
    August 31, 2011

    och, gegen FPU in leicht verdaulicher Form hätte ich nichts einzuwenden 😉 Addieren kann ja jeder ^^

    Zum Thema “Multiplizieren über Addition”: mein Opa hatte einen mechanischen Tischrechner (mit Motor) mit den drei Grundrechenarten: Addition, Subtraktion, Multiplikation (Division ist ja nicht einfach….). Damit habe ich als Kind gerne gespielt 🙂
    Man überlegt sich dann aber recht schnell, ob man “4 * 50” oder “50 * 4” berechnet. In ersterem Fall wird 4 – plus 4 – plus 4 – plus 4 …. gerechnet. Das Ganze mit Höllenlärm wegen der Mechanik samt Motor und entsprechendem Zeitaufwand (so 1-2 Operationen pro Sekunde….).
    Angeschafft wurde das Teil damals übrigens von der Milchgenossenschaft im Ort, weil mein Opa als “Zahlstelle” sich geweigert hat, die Auszahlungen von Hand zu berechnen. Bei seinem Vorgänger hatten die Mitglieder sich immer beschwert, er habe falsch gerechnet, mit der Maschine haben sie’s dann einfach geglaubt… 😉

  9. #9 Marcus Frenkel
    August 31, 2011

    Ja ja, die lieben Optimierungen bei der Befehlsausführung. Die gibt es ja immer noch – nur in hochkomplexer Form. 😉

    Ok, da sich nun einige was zu Gleitkommazahlen gewünscht haben, werde ich dazu wohl auch noch irgendwann einen Artikel bringen, in der Hoffnung, damit nicht die letzten Leser zu vergraulen. 😉

  10. #10 rolak
    September 4, 2011

    Hi etth, mit einer den üblichen Divisions-Algorithmen angemessenen Verzögerung: Da Fließkommazahlen letztendlich nur ganze Zahlen sind, läßt sich auch deren Division auf add/sub reduzieren. Es gibt meines Wissens nur einen wesentlichen Unterschied: Liegt ein HW-Multiplizierer für n-bit-Zahlen vor, kann mit minimalem Aufwand ein Algorithmus für k*n-bit-Zahlen implementiert werden (k² zu addierende Zwischenergebnisse) während einem ein HW-Dividierer diese Möglichkeit nicht bietet.
    In der Praxis werden jedoch schnellere Techniken genutzt (SRT, N/R, Goldschmidt) — was dann vielleicht schon erste Vorschläge für den FPU-Artikel wären, da ansonsten doch exakt genauso gerechnet wird wie vom schriftlichen multiplizieren/dividieren gewohnt. Nur halt im Binärsystem.

    Branch predicition ist doch, wenn auch wesentlich für Tempo und effiziente Pipeline-Auslastung, ‘nur’ eine Kombination aus statistischer Auswertung (zB saturating counter) und Schmierzettel (zB BTB).
    Beim Z80 war die Ausführungszeit ja noch fest – doch auch in diesem Zusammenhang hatte ich ziemliche Schwierigkeiten, einen Programmierer davon zu überzeugen, daß ~150 gesparte Takte bei dem 1aus256-Sonderfall die je 2 Takte für den nicht ausgeführten bedingten Sprung in keiner Weise aufwiegen. Auch eine Art von branch prediction 😉

    Du wirst schon keinen vergraulen, Marcus. Schlimmstenfalls werden einige posts seltener gelesen als andere(wenn schon nicht seltener geklickt), doch das dürfte bei noch so sorgfältiger Vorabfilterung eigentlich bei keinem Themenkomplex zu vermeiden sein.

  11. #11 Michi
    September 23, 2011

    Vielen vielen Dank! Die Serie hat mir viel geholfen!

  12. #12 ilikepepsi
    September 26, 2011

    @Marcus:

    Hi, hast du vor in der “Programmierung von Computern” Reihe die Funktionsweise von Compilern näher zu beleuchten? Falls nicht, das fände ich sehr interessant.