Home Computer Prozessor Basics Site Map counter

Adressierungsarten

Einführung

Jeder Prozessor hat als primäre Aufgabe Daten zu verarbeiten. Dazu muss er die Daten vom Arbeitsspeicher holen, manipulieren (das kann nur ein Vergleich mit anderen Daten sein, eine Rechnung oder das Verändern der Bits) und dann wieder irgendwo das Ergebnis ablegen. Er muss also schon bei nicht ganz trivialen Anwendungen die Möglichkeit haben, verschiedene Daten individuell anzusprechen. Das ist die Aufgabe der Adressierung.

Ich will im folgenden die verschiedenen Adressierungsarten die es gibt zuerst erläutern und dann aufzeigen was die Vorteile und Nachteile jedes Ansatzes sind.

Grundlegendes

Jeder Prozessor hat interne Speicherplätze die Register. Das können sehr wenige, minimal zwei sein, das können auch sehr viele sein. Welche Folge die Zahl der Register auf die Architektur und Befehlsverarbeitung hat dazu später mehr. Register haben aber eine herausragende Bedeutung:

Viele CPU unterscheiden  daher zwischen einem Zugriff auf ein Register und einem Zugriff auf den Arbeitsspeicher. Auch Register kann man wie Speicher adressieren, nur braucht man dafür viel weniger Bits als bei der Adressierung des Arbeitsspeichers. Bei Seymour Cray waren es z.B. bei allen seinen Maschinen acht Register. Der Arbeitsspeicher konnte dagegen 256 KWorte, 16 MWorte und 4096 MWorte groß sein. Für acht Register braucht man 3 Bits zum Adressieren, für den Arbeitsspeicher dagegen 18, 24 und 32 Bits. Dadurch kann man den Zugriff auf die Register sehr oft in einem kurzen Opcode unterbringen, während man bei Speicheradressen dies nicht kann. Der Opcode ist die binäre Repräsentation eines Befehls. Er setzt sich oft aus verschiedenen Bereichen wie einem Teil für den Befehl und einem Teil in dem Parameter stehen zusammen. Er gehorcht bestimmten Gesetzen und kann durch Maskierung dekodiert werden.

Eine typische Sequenz die die Adressierung zeigt wäre folgende

  1. Lade A-Register, 1000
  2. Addiere A-Register,2000
  3. Lade (4711),A-Register

Diese Sequenz ist in Pseudocode, ich verwende bewusst leine Befehle existierender Prozessoren. Es zeigt auch drei Adressierungsarten. Die erste lädt einen Wert (1000) in ein Register, die zweite nutzt einen Wert der unmittelbar auf den OpCode folgt (2000) als Operand für eine Addition, der zweite ist ein Register und der Dritte speichert das A-Register im Arbeitsspeicher an der Adresse 4711 ab. Im allgemeinen haben Adressierungsarten immer mindestens zwei Operanden: Eine Quelle von der die Daten kommen und ein Ziel wo die Daten hinkommen. Es ist eine Kopieraktion, daher heißen Mnemonics, (die Befehlskürzel) oft auch "Mov", "Ld" oder "Sto" als Abkürzung für Move, Load oder Store. Viele Befehle, die ein Prozessor durchführt sind solche Aktionen um Daten zu bewegen. In diesem Beispiel wird das Ziel der Addition nicht angegeben. Das ist die Zweiwegeadressierung oder Zweiadressadressierung. Viele einfache Prozessoren können z.B. für Rechnungen als einen der Operanden und als Ziel des Ergebnisses nur ein besonderes Register, den Akkumulator akzeptieren. Dann entfällt die Angabe des Ziels. Dies ist z.B. bei den Prozessoren 4004, 8008 und 8080 bei Intel so. Auf der anderen Seite gibt es bei Rechnungen auch die Dreiadressadressierung die unserem Verständnis entspricht. Wir schrieben ja auch:

X = A + B

A, und X B stehen für Variablen. In einem Prozessor sind dies Speicherplätze. X für den wo das Ergebnis der Rechnung abgelegt wird, A und B für die beiden Operanden. Viele einfache Prozessoren der 4 und 8 Bit Generation aber auch der 16-Bit Generation verwenden die Zweiadressadressierung, die man so formulieren könnte:

A = A + B

Zu lesen ist dies so: Der neue Wert von A ist der alte Wert von A plus dem von B. Das bedeutet man überschreibt einen der beiden Operanden mit dem Ergebnis. Das erscheint zuerst eine schlechte Lösung zu sein, doch es gibt gute Gründe warum auch diese Adressierung eingesetzt wird. Es gibt auch die EinAdressadressierung - dann ist nur ein Operand frei wählbar. Das ist bei 4-Bit und 8 Bit Prozessoren wie dem 8008 meist bei Rechnungen der Fall, es gibt aber auch Befehle bei leistungsfähigen Prozessoren die nur einen Operanden brauchen wie Sprünge, Unterprogrammaufrufe (Adresse), das erhöhen/erniedrigen des wertes um 1 oder das Rotieren und Verschieben von Bits.

Zur Vervollständigung: es gibt auch die Nulladressadressierung. Dabei werden gar keine Arbeitspeicheradressen verwendet. Solche Prozessoren unterstützen dann nur die unten beschriebene unmittelbare Adressierung. Da trotzdem irgendwo Daten abgelegt werden müssen arbeiten diese Prozessoren mit der Architektur einer Stapelmaschine: Man kann bei ihr Werte auf den Stapel ablegen (Push) oder holen (Pop). Da nur auf das letzte Element zugegriffen werden kann gibt es keinen Parameter. Rechnungen verwenden einfach die beiden letzten Stapelelemente als Operanden und legen das Ergebnis wieder auf dem Stapel ab.

Im folgenden verwende ich bei Pseudocode einen nicht existierenden Prozessor der 16 Register mit den Namen R0 bis R15 hat. Diese Register sind universell, d.h. sie beherrschen alle Adressierungsarten.

Es gibt dann noch einige Adressierungen die nicht in das Schema passen. Zum einen gibt es einige Spezialregister in der CPU die nicht für das Rechnen gedacht ist. Das ist z.B. der Befehlszeiger, der auf die Adresse zeigt wo der nächste Befehl zu holen ist. ein anderes ist der Stapelzeiger der einen Speicherbereich, den Stack verwaltet, auf den man nicht frei zugreifen kann sondern nur an seinem Ende etwas anfügen oder wegnehmen kann (wie bei einem Stapel Papier). Dann gibt es noch das Flagregister das über das Ergebnis der letzten Operation informiert, z.B. ob das Ergebnis negativ oder positiv ist, die Zahl 0 oder es einen Überlauf gibt. Auf es kann meist nur über bitweise Abfrage zugegriffen werden. Einzelne Architekturen können weitere Spezialregister haben wie Segmentregister, Interruptregister oder Adressregister. Sie werden meistens anders angesprochen als die anderen Register und unterstützen daher nicht oder nur bedingt die unten angegebenen Adressierungsarten.

Von Bedeutung sind auch zwei Begriffe: eine symmetrische Architektur oder eine unsymmetrische Architektur. In einer symmetrischen Architektur kann man alle Befehle und alle Adressierungsarten mit jedem Register durchführen. Dieses sind also universelle Register. Eine unsymmetrische Architektur erlaubt bestimmte Adressierungen oder bestimmte Befehle nur mit bestimmten Registern. Ein Beispiel für eine solche Architektur ist die x86-Architektur. Bestimmte Befehle funktionieren beim Urahn 8086 nur mit bestimmten Registern. So ist das BX-Register für die indizierte Adressierung vorgesehen, dies geht mit keinem anderen Register. Multiplikationen und Divisionen arbeiten immer mit AX als einem Operanden und Ziel. Es gibt allerdings fast keine Architektur die vollkommen symmetrisch ist, also z.B. alle Adressierungsarten kombiniert zulässt (z.B. indizierte Adressierung als Quelle und Ziel bei einem Ladebefehl). In der Regel ist es so das ein Operand immer ein Register ist, entweder als Quelle oder Ziel. Bei der Dreiadressadressierungen ist sogar die Regel, das alle drei Operanden Register sind, da sonst die Befehle zu lang werden würden.

Einsatz der Adressierungsarten nach Operadenanzahl im Befehl

Die vier Grundarten DreiAdress, ZweiAdress, EinAdress oder NullAdress Adressierung haben jeweils bestimmte Vorteile und Nachteile. Die DreiAdress Adressierung entspricht dem wie man von der Mathematik aber auch physikalischen Gesetzen es gewohnt ist Gleichungen zu schreiben z.B. für die Berechnung von Geschwindigkeit (v) und Weg (s)auf Basis von Beschleunigung (a):

v = a * t

s = v * t oder s = 1/2 a * t * t

Ein Compiler kann nun sehr einfach diese Formeln in Programmcode umsetzen und für a,s,v und t jeweils eigene Register nutzen. Daher findet sich diese Adressierung auch in Computern die vornehmlich viele schnelle Berechnungen durchführen vor allem für naturwissenschaftliche Probleme. Die Rechner von Seymour Cray. Angefangen von der CDC 6600 bis zu den Cray Rechnern wie der Cray 1. Bei den Mikroprozessoren ist es der Alpha-Mikroprozessor der die Drei AdressAdressierung einsetzte. Die x86 Architektur ist eine ZweiAdress-Architektur, doch die Befehlserweiterung SSE, welche mehrere Werte mit einem Befehl verarbeitet (üblich beim Bearbeiten ganzer Felder) ist eine DreiAdress Architektur. Sehr wichtig ist das die Abhängigkeit von Befehlen bei dieser Archietektur am geringsten ist, also die Gefahr am kleinsten das aufeinanderfolgende Befehle die gleichen Register benutzen und darauf achten müssen, das nicht ein Befehl etwas überschreibt was ein andere Befehl noch braucht. Auch dies ist ein Grund für den Einsatz dieser Adressierung in den schnellsten Rechnern der Welt, denn so ist es am einfachsten die Verarbeitung von Befehlen zu paralellisieren.

Die ZweiAdress-Architektur ist sehr weit verbreitet bei Computern die weniger für Berechnen als wie für das Verarbeiten von Daten also das kopieren, aneinanderhängen oder vergleichen gedacht sind. Hier gibt es in der Regel zwei Operanden, meistens Zeichenketten die iterativ durchlaufen werden. Das Ergebnis ist eine neue Zeichenkette oder nur ein einfaches Testergebnis wie "String 1 ist an Position 6 im String 2 enthalten". Es kann also ein dezidiertes Register oder ein Speicherplatz für das Ergebnis meist entfallen. Daher findet man diese Architektur sowohl in den frühen Mikroprozessoren z.B. der Intel 8080 oder x86 Architektur wie auch größeren kommerziellen Rechnern das bekannteste ist sicher das IBM System 360 und seine auf dem Befehlssatz basierenden Nachfolger.

Bei der EinAdress-Architektur hat ein Register eine Sonderstellung weil es immer Ziel des Ergebnisses ist wie auch einer der Parameter ist. Der grundlegende Vorteil ist dass dies die interne Architektur sehr stark vereinfacht. Im Prinzip kann man das Ausgaberegister der ALU nehmen und erspart sich so das hin und her Kopieren von Werten. Ein Register ist dann meist Quelle und auch Ziel der Operation, die zweite Quelle kann ein anderes Register oder eine Speicheradresse sein.

Verwendet wurde sie im populären Minicomputer PDP-11 aber auch den ersten Mikroprozessoren Intel 4004, Intel 8008. Die Intel 8080 Architektur hat einige EinAdressbefehle auch wenn sie im Normalfall eine ZweiAdressArchitektur ist. Um den Nachteil der noch häufigeren Kopieraktionen von Befehlen zu Begegnen führte die PDP-11 die Post- und Präinkrementoperationen ein. Damit kann man sehr elegant Schleifen in Code umsetzen wie diese der Summe eines Arrays:

For I:=1 to Length(Array)

Summe = Summe + Array[i]

Wenn Summe für das bevorzugte Register steht und i für ein Indexregister so wird durch den Postdekrementbefehl es automatisch um 1 erhöht und die Schleife umfasst nur einen Additionsbefehl und einen Vergleichsbefehl ob die Länge schon erreicht ist.

Die NullAdressArchitekeur ist dagegen relativ selten. In ihr gibt es ein Register als Quelle und als Ziel, die zweite Quelle ist der Stapel, wo der Prozessor Rücksprungadressen aber auch Register ablegt. Um die Daten zu verarbeiten muss jeder Operand zuerst in das einzige Register geladen und dann auf den Stapel abgelegt werden, das bedeutet das aus einer Ladeanweisung bei den obigen Architekturen immer zwei werden.

Sie lebt davon, dass ein Computer einen Stapel effektiv nutzen kann. Das erfordert in der Hardware einen schnellen Zugriff auf den Stapel, er sollte sich also nicht im Arbeitsspeicher sondern der CPU befinden, was bei größeren Gleichungen die aufgenommen werden sollen sehr viele Speichelemente nötig macht wie auch der Softwareumsetzung. Viele prozedurale Sprachen wie FORTRAN oder C haben Anweisungen die man nur aufwendig in Stapelinstruktionen umsetzen kann. Z.B. müssen die mathematischen Anweisungen umgeschrieben werden in die umgekehrte Polnische Notation:

aus A = B + C * D - F

wird

D F - B C + *

Es werden also zuerst D und F auf den Stapel gelegt, der Minusoperator holt die beiden letzten Elemente vom Stapel und subtrahiert sie voneinander und legt das Ergebnis auf den Stapel. Dann werden B und C auf den Stapel gelegt, der Plusoperator holt die beiden Zahlen, addiert sie, legt das Ergebnis auf den Stapel. Die beiden obersten Einträge sind nun die beiden Rechenergebnisse. Der Multiplikationsoperator holt diese und legt das Ergebnis der Multiplikation ab. Der Compiler muss also Gleichungen unter Berücksichtigung der Priorität (Punkt vor Strich) umstellen. Das ist aufwendig. Rekursion (eine Funktion ruft sich selbst erneut auf) kann leicht zu einem Stapelüberlauf führen. Daher setzt man idealerweise eine Sprache ein die ein stapelorientiertes Konzept hat, die bekannteste ist Forth. Aufgrund der Dominant von Fortran und C bei den sprächen gibt es nur wenige NullAdressarchitekturen.

Tendenziell wird eine CPU im Aufbau einfacher wenn man immer weniger Angaben über Adressen in einem Befehl hat. Damit einher geht auch oft das die Zahl der Register abnimmt. DreiAdressarchitekturen von Cray haben 64 Register, die meisten Zweiadressarchitekturen nur 8 bis 16 und einige EinAdressarchitekturen nur zwei oder drei Register. Der Nachteil der abnehmenden Adressen pro Befehl ist, das man für komplexere Berechnungen die mehr als eine arithmetische Operation voraussetzt immer mehr Zwischenregister für Zwischenwerte braucht, die dann bei Null- bis ZweiAdress-Architekturen weitere Ladebefehle verursachen.

Ein weiterer Aspekt ist das jede Adresse Platz braucht. Ein Befehl besteht aus einem Opcode für die Operation die ausgeführt werden soll. Mit 3 Bit kann man acht Operationen definieren die man schon bei einfachen Prozessoren braucht z.B. für die vier Grundrechenarten jeweils für Zahlen mit und ohne Vorzeichen (das Vorzeichen das man für negative Zahlen braucht ist meist das höchstwertige Bit. Berücksichtigt man dies nicht so kann man aus der Addition zweiter positiver Zahlen z.B. eine negative erhalten auch wenn man den Wertebereich nicht verletzt). Jeder Operand und dass Ergebnis braucht dann weitere Bits um ihn anzugeben. Wenn es z.B. Register sind und ein Prozessor 16 Stück hat so braucht man 4 Bits pro Operand. Damit hat eine NullAdressarchietektur 3 Bits für die Befehle, eine EinAdressArchitektur 7 entsprechend 11 und 15 Bits für Zwei und drei Operanden.

Das bedeutet: In ein Byte kann man zwei Befehle einer Nulladressarchitektur packen, aber nur einen mit einem Operanden. Für zwei öder drei Operanden braucht man sogar zwei Bytes. Man kann also die bei weniger Operanden nötigen zusätzlichen Operationen die meist im Kopieren oder Verschieben von Werten bestehen ausgleichen durch kürzeren und dadurch schnelleren Code. eine gute Architektur findet einen Kompromiss zwischen diesem Vor- und Nachteil und der hängt auch vom Einsatzgebiet des Computers ab.

Hier ein Beispiel: selbst die einfachsten 8 Bit Mikroprozessoren haben folgende arithmetisch-logische Befehle:

  1. UND-Verknüpfung zweier Zahlen
  2. ODER-Verknüpfung zweier Zahlen
  3. XOR-Verknüpfung zweier Zahlen
  4. Addiere zwei Zahlen
  5. Addiere mit Überlauf zwei Zahlen
  6. Subtrahiere zwei Zahlen
  7. Subtrahiere mit Überlauf zwei Zahlen
  8. Vergleiche Zwei Zahlen

Das sind acht Operationen. Sie würden in 3 Bits kodiert werden. Möchte man nun mit einem Byte für die Operationen und die Angaben der Register auskommen, so blieben 5 Bits übrig. Das wären bei den einzelnen Architekturen:

Deutlich wird, das wenn man einen Code hat der möglichst wenig Bytes beansprucht, und das hängt dann auch mit dem verfügbaren Arbeitsspeicher zusammen, dann wird man eine Adressierungsart mit ein oder Zweiadressarchitektur wählen. So verwundert es nicht das 8 Bit Mikroprozessoren meist diese nutzten. Auf der anderen Seite benötigt man bei einem 32 oder 64 Bit Rechner alleine für die Adresse 32 Bit oder 64 Bit. Da macht es keinen Sinn dann für Befehle die nur mit internen Registern abreiten diese nur 8 oder 16 Bits breit zu machen, da die Architektur dann immer 32 oder 64 Bits aus dem Arbeitsspeicher holen wird. Daher haben sich die Zwei/Dreiadressschema vor allem bei Rechnern die viele Bits verarbeiten durchgesetzt, man nutzt dann eben ein 32 Bit Wort für einen Befehl, das lässt bei der Dreiadressarchitektur 8 Bits für den Befehl (256 Stück) und noch je 8 Bits für jede der Adressen zu, also 256 Register.

Da Speicheradressen oder konstante Werte sehr viel mehr Bits als die Register brauchen limitieren die meisten Architekturen die Benutzung von Adressen und Konstanten meist auf einen der Operanden oder das Ziel, selten auch zwei. Viel effektiver ist es ein Register auf den Speicher zeigen zu lassen und dieses zu verändern. (direkte oder Indexadressierung).

Mitzusammenhängend ist natürlich auch der Durchsatz und die Codegröße. Während die Codegröße automatisch mit der Länge eines durchschnittlichen Befehlswortes steigt ist es beim Durchsatz durchaus nicht so. Zwar ist Null- oder Einadresscode kompakter aber man benötigt für eine bestimmte Operation mehr Anweisungen weil für Zwischenergebnisse dann umkopiert werden muss. In praktischen Benchmarks hat sich die Zweiadressarchitektur als die effizienteste herausgestellt. Das liegt daran das bei den meisten Mathematischen Ausdrücken man zwar mit einem Zwischenergebnis weiter rechnet, aber die meisten anderen Operanden nur einmal im Ausdruck vorkommen. So kann man einen Operanden gleichzeitig als Quelle und Ziel (für ein Zwischenergebnis) nutzen.

Adressierungsarten

Registeradressierung

Die einfachste Adressierung ist die Registeradressierung. Bei der Registeradressierung sind beide Operanden bei Bewegeoperationen, bzw. auch das Ziel bei Rechnungen ein Register. Beispiele für solche Befehle sind z. B:

Inkrementiere R0

Lade R1,R2

Addiere R3,R4,R5

In der Regel ist die Registeradressierung die schnellste mögliche. Das liegt an zwei Eigenschaften: Die Daten sind schon im Prozessor, sie müssen also nicht erst vom Speicher in den Prozessor übertragen werden. Zum zweiten benötigt man nur wenige Bits, um die 16 Register in unserem Beispiel zu adressieren. Hier sind es 4 Bits. Diese kann man in den ersten ein, zwei Bytes eines Befehles unterbringen, das bedeutet der Befehl ist auch schnell vom Speicher geholt, dekodiert, also komplett übersetzt. Eine Adresse kann dagegen bei 64 Bit Prozessoren bis zu 64 Bit lang sein, das sind acht Byte.  Zudem ist nachdem man die Adresse hat noch ein Zugriff auf den Speicher nötig um die Daten aus dieser Adresse zu holen.

Unmittelbare Adressierung

Bei der unmittelbaren Adressierung folgt ein Operand direkt dem Opcode. Er wird als Konstante betrachtet. Bei der unmittelbaren Adressierung kann die Konstante daher nur eine Quelle, niemals Ziel einer Rechenoperation sein. Beispiele:

Lade R1,1000

Addiere R3,R2,2000

Da die unmittelbare Adressierung die Konstante als Bestandteil des Befehls betrachtet ist sie zwar langsamer als die Registeradressierung (der Befehl ist länger durch die konstante und sie wird zuerst in ein internes, nicht von außen sichtbares Zwischenregister geladen) aber noch deutlich schneller als die folgende direkte Adressierung.

Direkte Adressierung

Bei der direkten Adressierung wird der Operand der dem Befehl folgt oder das angegebene Register als Adresse angesehen. In der Regel muss daher auf den Arbeitsspeicher zugegriffen werden um die Daten zu holen. Am Beispiel des Ladebefehls

Lade R1,(R2)

möchte ich dies verdeutlichen

Die Klammern geben an, das es sich um eine direkte Adressierung handelt. Dadurch kann man sie von der Register oder unmittelbaren Adressierung unterscheiden. Der Prozessor macht bei diesem Befehl folgendes: Nimm den Inhalt des Registers R2 als Adresse. Lege diese Adresse auf den Adressbus und fordere den Speicher auf, den Inhalt dieser Speicherzelle zu übermitteln. Enthält R2 z.B. die Konstante 2000, so wird man den Inhalt der Speicherzelle 2000 holen und in R1 laden, nicht aber den Inhalt von R2, also 2000. Die Angabe einer Adresse als Variable entspricht in Programmiersprachen einem Zeiger. (Pointer). Der Zeiger zeigt auf die eigentlichen Daten. Dies ist nicht nur die einfachste Möglichkeit auf den Speicher frei zuzugreifen (die Registeradressierung kennt keine Adressen und bei der direkten Adressierung ist der Wert konstant, kann also nicht geändert werden, wie der Inhalt einer Speicherzelle). Programmiersprachen nutzen auch den Pointer als Datenstruktur. Variablentabellen enthalten z. b. Zeiger auf die Daten anstatt die Daten selbst (die Zeiger sind immer gleich groß, nämlich so viele Bytes wie eine Adresse in der jeweiligen Architektur hat, die Daten selbst können variabel groß sein (Byte, Wort, Fließkommazahl und Zeichenketten haben unterschiedliche Größen) und bei einigen Datentypen ist auch die Größe veränderlich. (Zeichenketten, Felder, Listen).

In einer Hochsprache mit einem Feld wäre z.B. die Anweisung

X = Feld[A]

eine direkte Adressierung. Die Variable A wird das Adresse des Feldelementes interpretiert. Dessen Inhalt wird X zugewiesen. Die direkte Adressierung ist auch mit Registern möglich:

Lade R1,(1000)

Add R3,R1,(R2)

Wird zuerst in R1 den Inhalt von Speicherstelle 1000 laden und dann dazu den Inhalt der Speicherzelle addieren deren Adresse in R2 steht. Der Wert der vorher in R2 steht kann durch unmittelbare Adressierung geladen werden - ob der Inhalt eines Registers als Daten oder Adressen interpretiert wird ist dem Prozessor egal. Da der Prozessor zuerst die Adresse an den Speicher übermitteln muss und dann die Daten einlesen muss dauert eine direkte Adressierung noch länger als die unmittelbare.

Viele Prozessoren bieten auch die direkte Adressierung mit Speicheradressen. Dann würde man obige beide Befehle so schreiben:

Add R3,R1,(1000)

Also Addiere zu R2 den Inhalt den der Speicher bei Adresse 1000 aufweist.

Indirekte Adressierung

Noch eine Ebene weiter geht die indirekte Adressierung. Bei der direkten Adressierung enthält ein Register die Adresse der Speicherzelle, in der der eigentliche Wert steht. Bei der indirekten Adressierung enthält die Speicherzelle dagegen erneut eine Adresse und erst die Speicherzelle, die durch diese Adresse angesprochen wird den Wert selbst. Wenn die direkte Adressierung für einen Zeiger steht, dann ist die indirekte Adressierung das Referenzieren des Zeigers, das heißt das Auflösen des Zeigers um an die Daten selbst zu kommen. Auch hier hilft ein Vergleich mit einer Hochsprache weiter. Man kann das obige Feld auch nutzen um die Indizes einer Tabelle zu speichern:

 X = Tabelle[Feld[a]]

in Psuedeocode:

Lade R0,((r1))

Addiere R1,R2,((R3))

Die doppelten Klammern stehen für indirekte Adressierung. Logischerweise ist nun durch zwei aufeinanderfolgende Adress- und Speicherzugriffe die Ausführung noch langsamer als bei der direkten Ausführung. Die indirekte Adressierung wird benötigt wenn ein Kulturprogramm einen Wert verändern soll und dies eine komplexe Datenstruktur ist (bei einfachen Werten reicht die direkte Adressierung).

Indizierte Adressierung

Die Indizierte Adressierung ist sehr nützlich bei größeren Datenstrukturen in Programmiersprachen und Betriebssystemen. Komplexe Daten werden zu Strukturen fester Größe zusammengefasst. Aus dem Töglichen Leben könnte eine Struktur zB. aus folgenden Feldern bestehen:

die Klammern geben jeweils an wie viele Bytes belegt werden, für den Vornamen würden also 15 Bytes für 15 Buchstaben zur Verfügung stehen. Will man nun auf den Nachnamen kommen so muss man 15 zur Adresse des Vornamens addieren und für die Straße 75. Das ist ziemlich umständlich, daher verfügen viele Prozessoren über eine Spielart der Adressierung wo man zu einer Basisadresse noch eine Verschiebung angeben kann und so ohne Rechnungen und sichern / laden der Basisadresse direkt auf die folgenden Felder zugreifen kann. Bei der indizierten Adressierung gibt es sehr viele Spielarten, dich im im folgenden erläutern will:

Mit festen Versatz (displacement)

Dies entspricht der direkten Adressierung, nur wird vor dem Zugriff zu der Adresse noch ein fester Wert addiert und das Resultat als Adresse interpretiert. Es ist sozusagen eine Mischform aus unmittelbarer und direkter Adressierung, Das könnten z.B. Befehle sein wie

Lade R0,(R1+10)

Addiere R3,R2,(R1+20)

Der Sinn liegt in Verbunddatenstrukturen wie sie Betriebssysteme und Programmiersprachen (siehe obiges Beispiel) definieren. Bei letzteren heißen diese z. B. Struct (C) oder Record (Pascal). In Pascal könnte ein Record für ein Konto bei einer Bank so definiert sein:

Kontenrecord = record
  Wert : Double;
  Kontonummer: Integer;
  LetzterUmsatz: date;
end;

In meinem Computersystem wären Date und double jeweils 8 Byte lang, die Kontonummer 4 Byte lang. Würde in R1 die Basisadresse des Records stehen so könnte man mit R1+8 auf die Kontonummer und mit R1+12 auf das Datum zugreifen. Aufgrund der nötigen Addition brauchen diese Befehle noch länger als die direkte Adressierung.

Die flexiblere Lösung ist die Indizierung mit einem Indexregister. Hier gibt es eine Reihe von Varianten. Gängig sind drei:

Addition eines Indexregisters

Die Addition eines Indexregisters zu einem Basisregister liefert die Adresse für den direkten oder (seltener) indirekten Zugriff. Auch hier kann das Indexregister eine feste Konstante als Displacement enthalten, es kann aber einfacher programmtechnisch manipuliert werden. beim festen Displacement ist dieses Bestand des Opoles und kann nicht geändert werden.

Lade R0,(R1+R2)

Addiere R0,R1,(R2+R3)

Sehr oft sind Indexregister Register, die nur für die Indizierung genutzt werden, so bei der X86-Architektur.

Addition eines Indexregisters und eines festen Displacements

Die Kombination beider obigen Formate. Mit dem Indexregister kommt man in einem Feld von einem Eintrag zum nächsten, mit dem Displacement greift man auf die Unterelemente eines Eintrags zu.

Lade R0,(R1+R2+100)

Addiere R4,R3,(R0+R2+10)

Addition des Produkts zweier Indexregister

Will man sich über ein Feld von Datenstrukturen bewegen, so ist es am einfachsten ein Indexregister mit der Größe der Datenstruktur zu belegen und ein zweites mit einem Zähler. Dieser wird bei jedem Zugriff erhöht. Aufgrunddessen das eine Multiplikation hardwaretechnisch sehr aufwendig ist bieten nur wenige CPUs diese Möglichkeit oder sie eingeschränkt auf ganze Zweierpotenzen um z.B. Bytes, 16-Bit, 32-Bit und 64-Bit große Daten ansprechen zu können. die Multiplikation mit dem Faktor zwei ist hardwaretechnisch durch das Schieben des Registerinhalts um ein Bit nach links leicht möglich.

Lade R0,(R1*R2+R3)

Addiere R4,R3,(R0*R1+R2)

Addition des Produkts zweier Indexregister und eines Displacements

Entspricht dem oberen mit der Kombination des festen Wertes damit kann man in einem Feld des obigen Rekordtyps auf ein Element (Kontonummer, Wert) zugreifen. Nehmen wir dieses Beispiel und definieren ein Feld

Var x : Array of Kontenrecord;

Würde man in Pascal auf das 4-te Element (die Zählung beginnt mit 0) und das Element Kontonummer zugreifen (8 Bytes vom Recordbeginn entfernt) so könnte man dies in Assembler so lösen:

Lade R0,Adresse(x)

Lade R1,Size(Kontenrecord)

Lade R2,3

Lade R4,(R0+R1*R2+8)

R= enthält die Basisadresse des Feldes, also die Adresse des ersten Elementes. R1 die Größe jedes Elementes, R2 den Arrayindex, hier 3 für das vierte Element und 8 ist das feste Displacement das im vierten Element die Adresse der Variable Kontonummer enthält. In einer Hochsprache entspricht dies:

R4=Feld[3].Kontonummer

Bisher waren alle Indizierungsoperationen analog der direkten Adressierung. Je nach Prozessorarchitektur sind aber auch indirekte Adressen als Ziel der Indizierung denkbar. Das benötigt man bei Sprungtabellen von Zeigern die dann auf die einzelnen Adressen zeigen.

Prä und Postoperationen

Aufgrund der Bedeutung von indizierten Operationen gibt es bei vielen Prozessoren noch die Option der Veränderung eines Registers vor oder nach dem Laden. Vor allem Indexregister werden meist bei einem Zugriff erhöht oder erniedrigt, da man hier Datenstrukturen durchläuft. Viele Prozessoren wie z.B. die MC68000 oder die VAX-Architektur haben daher Prä- oder Postinkrement/-decrement Befehle. Diese erhöhen den Wert des Registers (Inkrement) oder erniedrigen es (Dekrement) nach dem Zugriff (Post) oder vor dem Zugriff (Prä). Viele Architekturen implementieren nicht alle Optionen so haben nur wenige Architekturen einen Prä-Inkrementbefehl. Da diese Möglichkeiten schon vorhanden waemr als man die Sprache C entwickelte, hat diese Sprache sogar den ++ / -- Operator eingeführt der dies in der Hochsprache abbildet. Es entsprechen (in C)

Herausforderungen für die Rechnerarchitektur

Es ist klar, das innerhalb der Reihe die hier skizziert ist, die Adressierungsarten zunehmend komplexer werden. Das bedeutet das man zunehmend mehr Ressourcen braucht um die Adressierungsraten in Hardware zu implementieren. Wenn man sich die Möglichkeiten der Prozessoren die nacheinander entwickelt wurden anschaut so fällt dies deutlich auf:

Prozessor Register Unmittelbar Direkt Indiekt Indirekt mit festen Offset Indiziert mit Basis und Indexregister Indiziert mit Basis und Indexregister und Displacement Indiziert mit Basis und Indexregister und Displacement (Multiplikation) Unterstützung Post/Präoperationen
Intel 4004 (4 Bit) x x        
Intel 8008 (( bit erste Generation) x x x  
Intel 8080 (8 Bit zweite Generation) x x x x          
Zilog Z80 (8 Bit dritte Generation) x x x x x        
Intel 8086 (16 Bit) x x x x x x x    
Motorola 68000 16/32 Bit x x x x x x x x  
VAX 78032 (32 Bit VAX-.Architektur als Mikroprozessor) x x x x x x x x x

Neben der Komplexheit - RISC Prozessoren gehen eher den weg weniger komplexe Befehle zu haben, dafür diese schneller auszuführen - gibt es noch andere Gesichtspunkte. Am Beispiel eines 8-Bitters will ich die Problematik für den Maschinencode zeigen:

Selbst einfache 8-Bit Prozessoren haben folgende Befehle mit zwei Operanden: Addition, Addition mit Carry, Subtraktion, Subtraktion mit Carry, Logisches UND, Logisches Oder, Logisches Exklusiv-Oder. Das sind nur sieben Operationen. Wenn für jeder Operand die Möglichkeit besteht das es ein Register oder ein Register direkt sein kann, dann gibt es bei 8 Registern 16 x 16 Kombinationsmöglichkeiten bei der ZweiAdressAdressierung. (Je 8 für das Register und 8 für das direkte Nutzen des Registers jeweils für den Quell und Zieloperanden). Man benötigt für diese 256 Kombinationen alleine ein Byte. Dazu noch  drei Bit für die sieben Operationen (mit Ladebefehlen sind es 8) in einem zweiten Byte. So werden Befehle recht lang. Das ist bei einem 8-Bit System bei dem der Speicherplatz meist auf 64 KByte limitiert ist, ist dies doch sehr nachteilig.

Bei den 16-Bittern ist der adressierbare Speicher größer (1-16 MByte), doch hier kommen mit der Indizierun,g die bis zu drei Register als Operanden einsetzt, neue Adreessierungsarten hinzu sodass die Probleme bleiben. Erst bei der 32 Bit Generation hat man sowohl genügend adressierbaren Speicher, wie auch entsprechend (32 Bit) lange Befehlswörter. Zudem beherrschen diese Prozessoren oft die Dreiadressadressierung.

Dazu kommt das viele der Prozessoren dann aber auch unterschiedliche Datenformate kennen - 8, 16, 32  und heute sogar 64 Bit breit. Für jedes dieser Datenformate braucht man Ladebefehle und auch Befehle zum Verarbeiten. Multipliziert man dies mit der Registerzahl und den verschiedenen Adressierungsmöglichleiten kommt man auf sehr viele Kombinationsmöglichkeiten, die man alle kodieren muss.

Es gibt für dieses Problem eine Reihe von Lösungen. Die erste ist die Spezialisierung. Ein sehr gutes Beispiel dafür ist die Intel Serie ab dem 8008 bis zu den heutigen Haswellprozessoren. (wobei sich seit der Core-Mikroarchitektur nichts an den Adressierungen getan hat) Hier sind nicht alle Register gleichberechtigt. Beim 8080 als einem einfachen Vertreter ist der Akkumulator immer ein Operand und ein Ziel einer 8-Bit Rechenoperation - schon brauchen diese anstatt 64 Kombinationsmöglichkeiten nur noch 8. Bei 16-Bit-Operationen hat das Register HL die gleiche herausragende Bedeutung. Der direkte Zugriff über ein Register geht ebenfalls nur über Register HL bei 8 Bit und der über Register DE und HL bei 16 Bit. So bringt man trotz nominell 7 Register alle Adressierungsmöglichkeiten in einem Byte unter.

Die zweite ist der Verzicht auf Adressierungsmöglichkeiten. RISC Prozessoren mit einem festen Befehlsschema können nicht so viele Möglichkeiten unterstützen. Sie verzichten zum einen darauf Bytes und 16-Bit Werte zu verarbeiten und unterstützen nur Daten, die so breit wie die Register sind, das sind je nach Architektur 32 und 64 Bit. Zum zweiten konzentrieren sie sich auf die wichtigsten, am häufigsten genutzten Adressierungsarten.

Eine weitere Möglichkeit ist es die Register in Daten- und Adressregister zu trennen. Anhänger dieser Philosophie war Seymour Cray. Seine Supercomputer - von der CDC 6600 bis zur Cray 3 verwendeten mehrere Registersätze. Darunter immer einen für Daten und einen für Adressen. Ein Datenregister kann in der Regel nur durch ein anderes Datenregister oder eine Konstante geladen werden. Adressregister nehmen nur Adressen auf und werden als Quelle bei Operationen genutzt bzw. wenn Daten in den Speicher gehen auch als Ziel. Das verringert die Anzahl der Kombinationsmöglichkeiten, auch weil man die Registerzahl so reduzieren kann - bei Cray waren es jeweils 8 Register für Daten und Adressen - zusammen sind es aber 16 Register. Wenn ein Prozessor die Harvard Architektur einsetzt (getrennte Adressbereiche für Daten und Code) dann liegt dieses Konzept auch auf der Hand: Code, der auch Konstanten enthält kommt immer aus dem Codebereich. Daten, über die über Adressen zugegriffen wird dagegen aus dem Datenbereich. Weiterhin kann man so Adressregister auf die Breite auslegen, die der Adressraum hat und die Datenregister auf die Breite, welche die Daten haben. Diese Angaben sind nicht immer übereinstimmend (eigentlich nur bei 32-Bittern gleich groß): Rechner mit weniger als einer 32-Bit Architektur haben oft einen größeren Adressbereich als sie Daten verarbeiten können. 8 Bitter z. B. einen Adressbereich von 16 Bit, 16-Bitter einen von 20, 24 oder 32 Bit. Es gibt auch den umgekehrten Fall. Crays Supercomputer hatten Datenbreiten von 60 und 64 Bit, der Adressbereich betrug dagegen 18,24 oder 32 Bit.

Wird eine solche Trennung vorgenommen, so kann man auch mit Seiteneffekten arbeiten wie dies Cray bei der CDC 6600 und 7600 tat. Beschrieb man bestimmte Adressregister mit einer neuen Adresse, so löste dies automatisch ein Laden des Wertes dieser Adresse in eine vorgegebenes Datenregister aus. Schrieb man z.B. das Adressregister A1 neu, so wurde das Patentregister X1 mit dem Wert aus dieser Adresse gefüllt. Andere Datenregister hatten die umgekehrte Funktion. Beschrieb man sie, so wurde der Wert gleich an die Adresse im Speicher abgelegt der in einem anderen Adressregister stand. Beschrieb man das Register X7 z.B. so wurde der Wert an der Adresse im Speicher abgelegt die im Register A7 stand. Der Vorteil dieser Seiteneffekte ist, das der Befehlssatz sehr kompakt ist. Es entfallen die meisten der Ladeoperationen, da diese durch einen Automatismus ersetzt wurden. Allerdings wurden von Kunden bemängelt das dies die Programmierung umständlich machte und Cray gab es bei den den späteren Modellen auf.

Ein weiterer Nachteil der zunehmend komplexeren Adressierungsoperationen ist, das sie immer mehr Zeit benötigen. Bei den Indizierten Modi liegt dies durch die Rechnungen die durchgeführt werden müssen auf der Hand. aber auch bei den einfachen Modi muss man weil man nicht alle Informationen im Register hat auf den Speicher zugreifen um Daten aus adressierten Speicherzellen auszulesen. Die Ausführungszeit steigt daher an wie ich bei dem Beispiel zweier Intel Prozessoren zeigen will (man kann nur innerhalb eines Prozessors vergleichen, weil der 8086 z.B. durch ein vorrauschauendes Lesen weiter entwickelt als der 8080 ist zudem hat er durch den 16 Bit Datenbus Vorteile wenn 16-Bit anstatt 8 Bit Werte geladen werden müssen).
Adressierung Intel 8080 Takte pro Befehl Intel 8086 Takte pro Befehl
Register 4 2
Unmittelbar (8/16 Bit) 7 /10 4
Register-direkt (8/16 Bit) 7 / 10 10
Adresse-direkt (8/16 Bit) 13 / 16 10
Register indirekt 8 Bit 8080, 16 Bit 8086 7 8/9+
Register indiziert 16 Bit   8+

Durch die Prozessorentwicklung ist es heute so das komplexere Adressierungsarten sich nicht mehr so stark aufhielten. Bei den einfachen Prozessoren mussten diese beim Holen von Daten warten bis der Arbeitsspeicher diese nachdem er die Adresse erhalten hatte übergab. Synchrone RAMs die gleich die darauffolgenden Daten automatisch liefern und Caches mit geringer Zugriffszeit haben die Zugriffe mit direkten oder indirekter Adressierung beschleunigt, vor allem wenn der zweite Operand eine Adresse war. Pipelines sorgen heute dafür, dass Prozessoren pro Takt einen Befehl, mit mehreren Rechenwerken wie sie seit dem Pentium Standard sind sogar mehrere Befehle pro Zyklus ausführen können, selbst wenn diese komplizierte Adressierungen beinhalten.

Bei älteren Prozessoren war es dagegen sehr sinnvoll die Daten möglichst in den Registern zu halten und Adressierungen über Register anstatt Adressen im Opcode durchzuführen. Compiler hatten dazu eigene Einstellungen wie "User Register-Variables". Diese führte auch dazu das RISC Architekturen sehr viele Register aufweisen.



© des Textes: Bernd Leitenberger. Jede Veröffentlichung dieses Textes im Ganzen oder in Auszügen darf nur mit Zustimmung des Urhebers erfolgen.

Zum Thema Computer ist auch von mir ein Buch erschienen. "Computergeschichte(n)" beinhaltet, das was der Titel aussagt: einzelne Episoden aus der Frühzeit des PC. Es sind Episoden aus den Lebensläufen von Ed Roberts, Bill Gates, Steve Jobs, Stephen Wozniak, Gary Kildall, Adam Osborne, Jack Tramiel und Chuck Peddle und wie sie den PC schufen.

Das Buch wird abgerundet durch eine kurze Erklärung der Computertechnik vor dem PC, sowie einer Zusammenfassung was danach geschah, als die Claims abgesteckt waren. Ich habe versucht ein Buch zu schreiben, dass sie dahingehend von anderen Büchern abhebt, dass es nicht nur Geschichte erzählt sondern auch erklärt warum bestimmte Produkte erfolgreich waren, also auf die Technik eingeht.

Die 2014 erschienene zweite Auflage wurde aktualisiert und leicht erweitert. Die umfangreichste Änderung ist ein 60 Seiten starkes Kapitel über Seymour Cray und die von ihm entworfenen Supercomputer. Bedingt durch Preissenkungen bei Neuauflagen ist es mit 19,90 Euro trotz gestiegenem Umfang um 5 Euro billiger als die erste Auflage. Es ist auch als e-Book für 10,99 Euro erschienen.

Mehr über das Buch auf dieser eigenen Seite.

Hier geht's zur Gesamtübersicht meiner Bücher mit direkten Links zum BOD-Buchshop. Die Bücher sind aber auch direkt im Buchhandel bestellbar (da ich über sehr spezielle Themen schreibe, wird man sie wohl kaum in der Auslage finden) und sie sind natürlich in den gängigen Online-Plattformen wie Amazon, Libri, Buecher.de erhältlich.

Sitemap Kontakt Impressum / Datenschutz Neues Hier werben / advertisment here Buchshop Bücher vom Autor Top 99