Skript zur gleichnamigen Lehrveranstaltung von Prof. Dr. E. Nett

Technische Informatik I Nett, Pfohl Ausgabe 2013

Arbeitsgruppe Echtzeitsysteme und Kommunikation Otto-von-Guericke-Universität Magdeburg

Inhaltsverzeichnis 1. Einführung 1.1. Der Transistor als analoges System . . . . . . 1.2. Das binäre System . . . . . . . . . . . . . . . . 1.3. Das Abstraktionsprinzip und seine Ebenen 1.4. Hardware oder Software? . . . . . . . . . . .

I.

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

Rechnerarchitektur I

9 9 10 10 12

15

2. Prinzipielle Funktionsweise eines Rechners 17 2.1. Von-Neumann-Rechner . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 2.2. Grobstruktur der Organisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 3. Struktur der CPU 3.1. Die Daten- und Adressleitungen der CPU . . . . . . . . . . . . . . . . . . . . . 3.2. Datenregister . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3. Adressregister . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

21 21 26 27

4. Struktur des Hauptspeichers 4.1. Speichertechnologie . . . . . . . . . . . . . . . . 4.2. Logischer Speicheraufbau . . . . . . . . . . . . 4.2.1. Bits, Bytes und Wörter . . . . . . . . . . 4.2.2. Speicheradressierung . . . . . . . . . . . 4.2.3. Bedeutung von Binärinformationen . . 4.2.4. Byte-Ordnung . . . . . . . . . . . . . . . 4.3. Speicherorganisation . . . . . . . . . . . . . . . . 4.3.1. Adressdecoder . . . . . . . . . . . . . . . 4.3.2. Partielle Adressdekodierung . . . . . . 4.3.3. Vollständige Adressdekodierung . . . 4.3.4. Blockweise Adressdekodierung . . . . 4.3.5. Struktur von Speichersystemen . . . . 4.4. Dynamischer Speicher vs. Statischer Speicher 4.5. Andere Formen von Halbleiterspeicher . . . . 4.5.1. ROM . . . . . . . . . . . . . . . . . . . . . 4.5.2. EPROM . . . . . . . . . . . . . . . . . . . 4.5.3. EEPROM . . . . . . . . . . . . . . . . . . 4.5.4. PROM . . . . . . . . . . . . . . . . . . . . 4.5.5. Pseudo-ROM . . . . . . . . . . . . . . . .

29 31 36 36 37 37 39 39 40 41 42 43 43 44 47 47 48 48 49 49

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . .

5. Programm- und Datenstrukturen 51 5.1. Datentransfer RAM-CPU . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51

3

Inhaltsverzeichnis 5.2. Arithmetische und logische Operationen auf Daten . 5.2.1. Befehlsausführung . . . . . . . . . . . . . . . . . 5.2.2. Lineare Befehlsfolge . . . . . . . . . . . . . . . . 5.2.3. Sprünge . . . . . . . . . . . . . . . . . . . . . . . . 5.2.4. Zustandscodes . . . . . . . . . . . . . . . . . . . . 5.3. Adressierungsarten . . . . . . . . . . . . . . . . . . . . . 5.3.1. Adressierung eines direkten Werts . . . . . . . 5.3.2. Indirekte Adressierung . . . . . . . . . . . . . . 5.3.3. Indexadressierung . . . . . . . . . . . . . . . . . 5.3.4. Indexadressierung mit Verschiebung . . . . . . 5.3.5. Autoinkrement-Adressierung (postincrement) 5.3.6. Autodekrement-Adressierung (predecrement) 5.4. Assemblersprache . . . . . . . . . . . . . . . . . . . . . . 5.4.1. Assemblerdirektiven . . . . . . . . . . . . . . . . 5.5. Assemblierung und Ausführung von Programmen . 5.6. Zahlennotation . . . . . . . . . . . . . . . . . . . . . . . . 5.7. Grundlegende Operation für Ein- und Ausgabe . . . . 5.8. Stack (Stapelspeicher) . . . . . . . . . . . . . . . . . . . . 5.9. Queues (Warteschlangen) . . . . . . . . . . . . . . . . . . 5.10. Subroutinen (Unterprogramme) . . . . . . . . . . . . . 5.10.1. Parameterübergabe . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

51 54 54 55 57 58 58 58 60 61 63 63 63 65 67 69 69 72 74 75 77

II. Rechnerentwurf

81

6. Kombinatorische Schaltnetze 6.1. Die Basisgatter . . . . . . . . . . . . . . . . . 6.2. Beispiele für kombinatorische Schaltnetze 6.2.1. Beispiel 1: Mehrheitsentscheider . . 6.2.2. Beispiel 2: Multiplexer . . . . . . . . 6.2.3. Beispiel 3: XOR . . . . . . . . . . . . 6.3. Äquivalenz von Schaltnetzen . . . . . . . . 6.4. Zusammenfassung . . . . . . . . . . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

83 83 85 85 86 87 90 91

7. Methoden zur Analyse und Synthese 7.1. Einführung in die Boolesche Algebra . . . . . . . . . . . . . . . . . 7.1.1. Beispiel: Entwurf eines 2-Bit-Multiplizierers . . . . . . . . 7.1.2. Assoziativität von NAND und NOR . . . . . . . . . . . . . 7.1.3. Zusammenfassung boolesche Algebra . . . . . . . . . . . . 7.2. Minimierung von Schaltfunktionen mit Karnaugh-Diagrammen 7.2.1. Karnaugh-Diagramme . . . . . . . . . . . . . . . . . . . . . . 7.2.2. Don’t-care-Zustände . . . . . . . . . . . . . . . . . . . . . . . 7.3. Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

93 93 96 101 101 101 102 105 106

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

8. Sequenzielle Schaltnetze 109 8.1. RS-Flipflop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 8.1.1. Beispiel Impulsfolgengenerator . . . . . . . . . . . . . . . . . . . . . . . 113 8.2. Getaktete Flipflops . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114

4

Technische Informatik I

Inhaltsverzeichnis 8.3. D-Flipflop . . . . . . . . . . . . . . . . . 8.3.1. Beispiel m-Bit-Datenbus . . . . 8.4. JK-Flipflop . . . . . . . . . . . . . . . . . 8.5. Master-Slave-Flipflop . . . . . . . . . . 8.6. Flankengesteuerte Flipflops . . . . . . 8.7. Zusammenfassung Flipflops . . . . . . 8.8. Beispiele . . . . . . . . . . . . . . . . . . 8.8.1. Register . . . . . . . . . . . . . . 8.8.2. Schiebe- und Rotationsregister 8.8.3. Frequenzteiler und Zähler . . . 8.9. Zustandsdiagramme . . . . . . . . . . . 8.9.1. Beispiel Drei-Bit-Zähler . . . . 8.10. Testen von digitalen Schaltungen . . . 8.10.1. Test durch Pfadsensitivierung 8.11. Zusammenfassung . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

9. Rechnerarithmetik 9.1. Zahlensysteme mit unterschiedlichen Basen . . . . . . . . . . . 9.2. Zahlbasiswechsel . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.2.1. Dezimal zu binär, oktal, hexadezimal . . . . . . . . . . . 9.2.2. Binär zu dezimal . . . . . . . . . . . . . . . . . . . . . . . 9.2.3. Oktal zu dezimal . . . . . . . . . . . . . . . . . . . . . . . 9.2.4. Hexadezimal zu dezimal . . . . . . . . . . . . . . . . . . 9.3. BCD – Binary Coded Decimal . . . . . . . . . . . . . . . . . . . . 9.4. Darstellung ganzer Zahlen mit Vorzeichen . . . . . . . . . . . . 9.4.1. Vorzeichen-/Betrag-Darstellung . . . . . . . . . . . . . . 9.4.2. Einerkomplement-Darstellung . . . . . . . . . . . . . . . 9.4.3. Zweierkomplement-Darstellung . . . . . . . . . . . . . . 9.5. Überlauf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.6. Addition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.6.1. Halbaddierer . . . . . . . . . . . . . . . . . . . . . . . . . . 9.6.2. Volladdierer . . . . . . . . . . . . . . . . . . . . . . . . . . 9.6.3. Addition von Wörtern . . . . . . . . . . . . . . . . . . . . 9.6.4. Vorausschauende Addierer . . . . . . . . . . . . . . . . . 9.7. Multiplikation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.7.1. Erste Version einer Multiplikationshardware . . . . . . 9.7.2. Zweite Version einer Multiplikationshardware . . . . . 9.7.3. Finale dritte Version einer Multiplikations-Hardware 9.8. Der Booth-Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . 9.9. Schnelle Multiplikation . . . . . . . . . . . . . . . . . . . . . . . . 9.10. Ganzzahldivision . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.10.1. Erste Version einer Divisions-Hardware . . . . . . . . . 9.10.2. Zweite Version einer Divisionshardware . . . . . . . . . 9.10.3. Finale dritte Version einer Divisionshardware . . . . . 9.10.4. Division mit Vorzeichen . . . . . . . . . . . . . . . . . . . 9.11. Festkommazahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.12. Gleitkommazahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.12.1. Mögliche Systeme für Gleitkommazahlen . . . . . . . .

Technische Informatik I

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

115 116 118 119 120 122 122 122 123 125 126 127 128 129 131

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

133 . 135 . 136 . 137 . 138 . 138 . 138 . 138 . 139 . 140 . 141 . 142 . 143 . 144 . 144 . 144 . 146 . 149 . 152 . 153 . 154 . 155 . 155 . 162 . 163 . 164 . 166 . 166 . 168 . 169 . 170 . 171

5

Inhaltsverzeichnis 9.12.2. Akkurate Arithmetik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174 9.12.3. Vereinfachte Algorithmen zur Berechnung von Gleitkommazahlen . 176 9.13. Zusammenfassung Computerarithmetik . . . . . . . . . . . . . . . . . . . . . . 177 10. Codes 10.1. Zeichencodes . . . . . . . . . . . . . . . . . . . . . . . . 10.1.1. ASCII oder „ISO 7-bit character code“ . . . . 10.1.2. ASCII-Fortentwicklung . . . . . . . . . . . . . 10.1.3. Unicode . . . . . . . . . . . . . . . . . . . . . . . 10.2. Bildcodierung . . . . . . . . . . . . . . . . . . . . . . . . 10.3. Ungewichtete Codes . . . . . . . . . . . . . . . . . . . . 10.4. Huffman-Codierung . . . . . . . . . . . . . . . . . . . . 10.5. Fehler erkennende Codes . . . . . . . . . . . . . . . . . 10.5.1. Arbeitsweise Fehler erkennender Codes . . . 10.5.2. Fehlererkennung durch Parität . . . . . . . . . 10.5.3. Zyklische Redundanzcodes oder CRC . . . . 10.6. Fehlerkorrektur durch Hamming-Codierung . . . . 10.6.1. Weitere Eigenschaften von Hamming-Codes

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

III. Rechnerarchitektur II

179 179 180 180 182 182 183 186 188 189 191 194 197 199

201

11. Implementierung der Kontrolleinheit 11.1. Mikroprogrammierbare Steuereinheiten . . . . . . . 11.1.1. Steuerung der Mikrobefehlsfolge . . . . . . . 11.1.2. Implementierung des Lese-Ausführ-Zyklus 11.2. Hartverdrahtete Steuereinheiten . . . . . . . . . . . . 11.2.1. Vom Befehlscode zur Ausführung . . . . . . . 11.2.2. Das Lese-Ausführ-Flip-Flop . . . . . . . . . . 11.3. Hartverdrahtet oder mikroprogrammierbar? . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

203 203 207 208 209 212 214 216

12. RISC-Prozessoren 12.1. Befehlsklassen . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.2. Beispiele für den RISC-Ansatz . . . . . . . . . . . . . . . . . 12.3. Wünschenswerte Eigenschaften einer RISC-Architektur 12.3.1. Befehlsformat . . . . . . . . . . . . . . . . . . . . . . 12.3.2. Registerfenster . . . . . . . . . . . . . . . . . . . . . . 12.3.3. Fenster und Parameterübergabe . . . . . . . . . . . 12.3.4. RISC-Architektur und Pipelining . . . . . . . . . . 12.3.5. Zugriff auf den externen Speicher . . . . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

219 . 219 . 220 . 221 . 222 . 222 . 223 . 226 . 231

13. Realisierung einer leistungsfähigen Speicherarchitektur 13.1. Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.2. Cache-Struktur und Arbeitsweise . . . . . . . . . . . . . . 13.2.1. Direct-mapped-Cache . . . . . . . . . . . . . . . . 13.2.2. Beispiel für Ladezugriffe auf einen Cache . . . . 13.2.3. Fehlerbehandlung bei Direct-Mapped-Cache . . 13.2.4. Beispiel: Cache in der DECstation 3100 . . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

6

Technische Informatik I

. . . . . .

233 233 237 237 239 242 243

Inhaltsverzeichnis 13.2.5. Vorteile durch räumliche Lokalität . . . . . . . . . . . . . . . . . . 13.2.6. Entwurf eines Speichersystems mit Cacheunterstützung . . . . 13.2.7. Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.3. Messen und Verbessern der Cacheleistung . . . . . . . . . . . . . . . . . . 13.3.1. Beispiel für die Berechnung der Cacheleistung . . . . . . . . . . . 13.3.2. Reduzierung der Fehlerraten durch flexiblere Blockplatzierung 13.3.3. Beispiel: Assoziativität in Caches . . . . . . . . . . . . . . . . . . . 13.3.4. Finden eines Blocks im Cache . . . . . . . . . . . . . . . . . . . . . 13.3.5. Tag-Größe und Mengenassoziativität . . . . . . . . . . . . . . . . . 13.3.6. Auswahl des zu ersetzenden Blocks . . . . . . . . . . . . . . . . . 13.3.7. Verringerung der Fehlerstrafe durch mehrere Cache-Ebenen . . 13.3.8. Leistung von Mehrebenencaches . . . . . . . . . . . . . . . . . . . 13.3.9. Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.4. Virtueller Speicher . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.4.1. Adressumsetzungskonzepte . . . . . . . . . . . . . . . . . . . . . . 13.4.2. Platzierung und Wiederfinden einer Seite . . . . . . . . . . . . . . 13.4.3. Seitenfehler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.4.4. Schreibzugriffe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.4.5. Beschleunigung eines Speicherzugriffs mit dem TLB . . . . . . . 13.4.6. Zusammenwirken von virtuellem Speicher, TLB und Caches . . 13.4.7. Implementierung von Schutz durch virtuellen Speicher . . . . . 13.4.8. Behandlung von Seitenfehlern und TLB-Cachefehlern . . . . . . 13.4.9. Blockplatzierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.4.10.Blockidentifizierung . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.4.11.Blockersetzung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.4.12.Schreibstrategie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.5. Zusammenfassung Speicherarchitektur . . . . . . . . . . . . . . . . . . . . 14. Parallelrechner 14.1. Kommunikationsmodelle . . . . . . . . . . . . . 14.1.1. Multiprozessoren . . . . . . . . . . . . . 14.1.2. Multicomputer . . . . . . . . . . . . . . . 14.2. Verbindungsnetzwerke . . . . . . . . . . . . . . 14.2.1. Switching . . . . . . . . . . . . . . . . . . 14.2.2. Routing-Algorithmen . . . . . . . . . . 14.3. Leistung . . . . . . . . . . . . . . . . . . . . . . . 14.3.1. Hardwaremetriken . . . . . . . . . . . . 14.3.2. Softwaremetriken . . . . . . . . . . . . . 14.3.3. Methoden zur Performanzsteigerung . 14.4. Taxonomie von Parallelrechnern . . . . . . . . 14.5. Zusammenfassung . . . . . . . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . .

246 250 252 252 254 256 258 260 261 262 263 263 265 266 268 269 271 273 274 277 278 279 281 282 283 284 285

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

289 291 291 293 296 299 302 304 304 305 307 310 312

15. Literatur 315 15.1. Empfohlene Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 315 15.2. Ergänzende Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 315

Technische Informatik I

7

1. Einführung Heutzutage ist der Computer ein allgegenwärtiges Werkzeug geworden. Dass sich das so entwickelt hat, ist zu einem großen Teil der Verdienst der Entwicklung effizienter Nutzerschnittstellen: Ein Anwender bekommt nur noch das für ihn Wesentliche zu Gesicht; wie ein Rechner intern funktioniert, bleibt ihm dagegen verborgen. Dieses Verbergen von Detailinformationen nennt man Abstraktion. Der Programmierer, der das Anwendungsprogramm des Nutzers geschrieben hat, benötigt auch kein ausgereiftes Detailwissen über die expliziten Vorgänge im Prozessor. Auch er verwendet eine Schnittstelle, die ihrerseits ein bereits abstrahiertes Bild der Vorgänge im Rechner widerspiegelt. Im Vergleich zum erstgenannten Anwender muss der Programmierer aber bereits mehr über die Innereien eines Computers wissen. Diese Abstraktionsunterteilung über das Wissen der Funktionsweise eines Computers ließe sich entsprechend fortführen. Sie ist notwendig, da der Computer ein technisches Gerät ist, welches sich vom Vorgehen eines Menschen stark unterscheidet. Die Abstraktion ist der Weg, Mensch und Maschine „in einen Topf“ zu bekommen. Sie als zukünftig programmierende Menschen erfahren in dieser Lehrveranstaltung die grundlegenden Dinge, die den Aufbau eines Computers ermöglichen: Ausgehend von elektronischen Bauteilen wird auf deren Verknüpfung zu komplexen Schaltungen eingegangen und schließlich erklärt, wie Computer rechnen und es möglich ist, trotz der Beschränktheit auf die Darstellung von Null und Eins beliebige Daten abzuspeichern. Sie bekommen dadurch einen Blick „hinter die Kulissen“ und verstehen danach wahrscheinlich besser, warum sich ein Computer verhält, wie er sich verhält.

1.1. Der Transistor als analoges System Ein Computer ist im Wesentlichen nichts anderes als ein Haufen elektronischer Schaltungen. Diese sorgsam verknüpften Bauelemente bestehen wiederum aus noch kleineren Bauelementen. Die Grundeinheit einer elektronischen Schaltung ist der Transistor. Als analoges Bauelement arbeitet er mit unendlich vielen Werten aus einem bestimmten Wertebereich (Betriebsspannung, Strom). In der Regel liegen Ein- und Ausgangsspannungen von wenigen Volt an. Für die Darstellung von Informationen ist es sinnvoll, einen endlichen Wertebereich (Alphabet) mit einer bestimmten Anzahl von Elementen (Symbolen) zu nutzen. Dazu wird der Transistor als Schalter betrieben. Die beiden Schaltzustände „offen“ und „geschlossen“ erlauben es, eine Information mit Hilfe des Alphabetes auf bestimmte Abschnitte des Wertebereiches (Symbole) abzubilden (siehe Abbildung 1.1). Auffällig ist hierbei, dass jeweils der Eingabebereich größer als der Ausgabebereich ist. Dadurch kann ein „Rauschen“ (d. h. Spannungsschwankungen) auf dem Übertragungsweg von der Ausgabe einer Schaltung zur Eingabe der folgenden toleriert werden. Die angegebenen Grenzen für die Spannungsbereiche H und L werden von den Herstellern

9

1. Einführung

Abbildung 1.1.: Logik-Pegel am Beispiel von Standard-TTL garantiert. Die Differenz zwischen den Grenzen wird deshalb als garantierter statischer Störabstand bezeichnet. Bei realen Systemen führt jedoch nur ein sehr kleiner Teil des Wertebereiches im verbotenen Bereich zu keiner definierten Zuordnung, sodass auch relativ stark verrauschte Signale (verzerrte Symbole) zugeordnet werden können. Solche Eigenschaften machen sich so genannte Fehler korrigierende Codes, wie sie bei der Nachrichtenübertragung eingesetzt werden, zunutze.

1.2. Das binäre System In der Digitaltechnik bzw. in einem Computer umfasst das digitale Alphabet zwei Elemente: In Zahlen- und Sprachdarstellung 0 (Null) und 1 (Eins), in der Logik auch F(alse) und T(rue) genannt. Dieses Alphabet heißt Binärsystem, die Symbole sind binäre Ziffern (binary digits, kurz Bits). Ein Bit ist die kleinste Informationseinheit in einem Computer. Die Elemente des Alphabetes 0 und 1 werden auf die Spannungsbereiche für L (Low) und H (High) abgebildet. Die Verwendung des Binärsystems resultiert aus folgenden Tatsachen: • Die Symbole sind sich so unähnlich wie möglich; sie haben eine einheitliche Distanz und können somit nicht verwechselt werden. • Das Prinzip der Zweiwertigkeit ist häufig anzutreffen: ja/nein, wahr/falsch, Strom fließt/fließt nicht (vgl. Aussagenlogik der Lehrveranstaltung Logik)

1.3. Das Abstraktionsprinzip und seine Ebenen Ein Computer lässt sich als ein System, welches aus mehreren aufeinander aufbauenden Ebenen besteht, modellieren. Dabei baut jede Ebene auf der darunterliegenden Ebene

10

Technische Informatik I

1.3. Das Abstraktionsprinzip und seine Ebenen Ebene

Abstraktion

Beispiel

5

Anwendungsebene

Programme wie Word, Excel, Email ...

4

höhere Programmiersprachen

C, C++, Java ...

3

Assembler

symbolische Darstellung von Maschinenbefehlen

2

(Betriebssystem)

Programm in Maschinensprache

1

Maschinensprache

SW/HW-Schnittstelle eines Rechners

0

Hardware (Gatter)

definiert den digitalen, physikalischen Rechner

Tabelle 1.1.: Abstraktionsebenen auf, was es erlaubt, von den Details der unteren Ebene zu abstrahieren, d. h. sie in der Abstraktion zu „verstecken“. Im Bereich der Hardware gibt es zwei wesentliche Ebenen: • Logische oder Gatterebene (Gatter wie z. B. UND, ODER sind die kleinsten Komponenten einer logischen Schaltung) • Physikalische oder Transistorebene (Transistor als kleinste Komponente von elektronischen Schaltungen) Es bleibt die Frage, wie dem Computer beigebracht werden kann, Probleme zu lösen. Dazu muss der Hardware der Lösungsweg in kleinsten Schritten verständlich gemacht werden. Das ist Aufgabe der Software eines Rechners. Dazu müssen zunächst einmal die einzelnen Schritte formal beschrieben werden, d. h. es muss ein Algorithmus geschaffen werden. Dieser wird anschließend in einer dem Computer verständlichen Sprache ausformuliert. Die Sprache wiederum ist definiert durch den Befehlssatz, d. h. die Menge der Befehle, welche die Hardware ausführen kann. Da die Sprache des Rechners nur aus Bitfolgen besteht, welche aus der Sicht des menschlichen Betrachters schwer verständlich ist, ist es notwendig, eine weitere Sprache mit einer definierten Menge von Befehlen zu entwerfen (Assemblersprache), welche durch den Computer (bzw. durch Software in Form eines Assemblers) in die Befehle seiner Maschinensprache übersetzt werden. Da auch das Schreiben von Assemblercode noch nicht optimal für den menschlichen Programmierer ist, existieren aufbauend auf der Assemblersprache höhere Programmiersprachen wie zum Beispiel C oder Pascal (das Übersetzungsprogramm heißt hierbei Compiler). Diese Herangehensweise an eine Problemlösung ist eine wiederholte Anwendung des Abstraktionsprinzips. Eine detaillierte Darstellung der Ebenen könnte z. B. wie in Tabelle 1.1 aussehen. Sowohl Hardware- als auch Softwaredesigner benutzen das Abstraktionsprinzip, um die Komplexität des Rechners besser zu beherrschen. Für das Verständnis des Prinzips sei noch gesagt, dass es für die praktische Implementierung einer Ebene ausreicht, die Ebene darunter zu kennen: Die Details der Implementierung werden in der nächsttieferen Ebene verborgen. Ein Beispiel: Wenn man versucht, Quellcode einer Hochsprache in Assemblercode umzuwandeln, ist es überflüssig zu wissen, wie dieser Assemblercode in Maschinensprache umgesetzt wird, solange man alle nötigen Assemblerbefehle und ihre Funktion kennt. Für die Implementierung einer Abstraktion gibt es prinzipiell verschiedene Möglichkeiten. In der Lehrveranstaltung Technische Informatik I und diesem (dazugehörigen) Skript werden die Ebenen 0 und 1 behandelt.

Technische Informatik I

11

1. Einführung Die darüberliegenden Ebenen brauchen uns nicht zu interessieren, denn Hardware und Software sind zueinander logisch äquivalent, d. h. jeder Befehl, der von einer Softwareebene implementiert wird, kann auch direkt durch Hardware ausgeführt werden und umgekehrt. Dabei ist die Hardwareimplementierung eines Befehls deutlich schneller. Ein gutes Beispiel hierfür sind moderne Grafikkarten mit Chipsätzen, die Berechnungen von komplexen Bildern selbst durchführen, anstatt dies der langsameren Software zu überlassen. Die Implementierung wurde also „nach unten“ verlagert. Auch der umgekehrte Weg ist möglich: In eingebetteten Systemen kommen zum Teil abgespeckte Prozessoren zum Einsatz, die keinen Gleitkommazahlenverarbeitenden Teil haben. Darum muss die Berechnung mit Kommazahlen vollständig in Software durchgeführt werden. Dies entspricht einer Verlagerung der Implementierung „nach oben“.

1.4. Hardware oder Software? Eine der wichtigsten Entscheidungen beim Entwurf eines Rechners ist, was wird durch Hardware und was durch Software realisiert. Folgende Kriterien beeinflussen die Entscheidung: • Kosten der Implementierung • Geschwindigkeit • Zuverlässigkeit • Häufigkeit der unveränderten Nutzung Eine direkte Ausführung in Hardware ist in der Regel der schnellste und zuverlässigste Weg, allerdings auch der teuerste (je zuverlässiger, desto teurer! Beispiel: militärische Anwendungen). Ein Programm für die existierende Hardware zu schreiben, welches denselben Befehl ausführt, ist zwar billiger, dauert aber in der Programmausführung länger, da die Hardware in diesem Fall nicht optimiert ist. Hierbei ist die Frage, wie häufig der Befehl voraussichtlich genutzt wird, in die Überlegungen mit einzubeziehen. Die Hardware definiert die Grenzen eines Rechners, insbesondere ihre Geschwindigkeit (z. B. muss der Navigationscomputer eines Flugzeuges Kurskorrekturen „schnell genug“ berechnen können). Wie schnell ein Rechner nun ist, bestimmt die Hardware und ihre Einsatzsteuerung. Die Grenzen sind offensichtlich davon abhängig, wie ein Rechner eingesetzt wird: • Workstation (Arbeitsplatzrechner, Bürorechner) • Supercomputer (für naturwissenschaftliche Berechnungen in Gentechnik, Wetterbzw. Klimaforschung, Urknallsimulation usw.) • Server (zum Beispiel für Webseiten) • Eingebettetes System (automatische Kontrolle in Autos, Waschmaschinen, Backöfen), eingebettet in ein Gesamtsystem und meist unsichtbar für den Benutzer, sogenannte „Konsumentenelektronik“ Letzterer Typ ist heutzutage mit Abstand der häufigste Einsatztyp für Rechner, speziell Mikroprozessoren. Da meist bei diesen Anwendungen Interaktion mit der Außenwelt des zu kontrollierenden Systems stattfindet, spielt Zeit neben der Korrektheit der Ausführung eine große Rolle; zeitliche (rechtzeitige) Ausführung zu garantieren ist implementierungsabhängig. Man muss die Details kennen, Abstraktion hilft hier leider nicht

12

Technische Informatik I

1.4. Hardware oder Software? weiter, da sie kein Mittel zur Garantie von zeitlicher, rechtzeitiger Ausführung ist (Ein Flugzeugpassagier braucht nicht zu wissen, wie die Instrumente im Cockpit funktionieren, der Pilot schon.).

Technische Informatik I

13

Teil I.

Rechnerarchitektur I

15

2. Prinzipielle Funktionsweise eines Rechners Das was einen Rechner ausmacht, ist die Fähigkeit sehr effizient und schnell Befehle zu verarbeiten. Bei den Rechnern, die hier betrachtet werden, spricht man von Maschinenbefehlen. Der Begriff „Maschinenbefehl´´ soll die Nähe zum Rechner ausdrücken. Er wird auf einer sehr tiefen Ebene ausgeführt und bestimmt sozusagen das Tun des Rechners. Die Maschinenbefehle sind die unterste Softwareschicht im Rechner. Unter ihnen gibt es „nur“ noch die elektronische Schalttechnik. Komplexe Maschinenbefehle können zum Teil jedoch aus Mikrocode bestehen. Die Maschinenbefehle sollen einem Programmierer eine einfache Möglichkeit bieten, Programmlogik zu implementieren. Eine mögliche Architektur für die Ausführung ist der Von-Neumann-Rechner.

2.1. Von-Neumann-Rechner Der Von-Neumann-Rechner ist ein Konzept zur Realisierung von Rechnern. Er besteht im wesentlichen aus drei Komponenten. Der CPU, dem Speicher und der Ein-/AusgabeEinheit. Die Architektur, nach der der Von-Neumann-Rechner entwickelt wurde, heißt Stored-program-Architektur. Zum einen bedeutet das, dass dieser Rechner Programminstruktionen elektronisch speichert und zum anderen, dass sich Programme und Daten im selben Speicher befinden. Die CPU mit Rechen- und Steuerwerk übernimmt dabei die aktive Rolle. Die Programme liegen zusammen mit den Daten in einem eindimensional adressierbaren RAM-Speicher und das Programmzähler-Register zeigt auf die nächste ausführbare Instruktion. Bei der Ausführung von Instruktionen wird von der CPU ein Befehl aus dem Speicher geholt und ausgeführt. Benötigt der Befehl Daten, so werden diese ebenfalls aus dem Speicher geladen. Im Normalfall wird danach der Inhalt des Programmzähler-Registers um eins erhöht. Es existieren jedoch Befehle, die das Programmzähler-Register bewusst manipulieren können, um damit z. B. Schleifen oder Funktionen zu realisieren. Eine weitere Art von Befehlen setzt den Wert des Programmzähler-Registers je nach Abhängigkeit von verschiedenen Status-Bits eines weiteren Registers.

2.2. Grobstruktur der Organisation Grob betrachtet arbeitet ein Rechner folgendermaßen (visualisiert in Abbildung 2.1): • Die Daten werden von der Eingabe gelesen (z. B. von einem Terminal) und in den Speicher geschrieben. • Der Hauptprozessor bekommt über den Datenpfad Daten aus dem Speicher.

17

2. Prinzipielle Funktionsweise eines Rechners • Die Kontrolleinheit steuert die Operationen (Befehle) aller anderen Komponenten und koordiniert sie. • Der Ausgang liest Daten aus dem Speicher und schreibt sie z. B. an einen Drucker, ein Terminal oder einen Massenspeicher

Abbildung 2.1.: Grobstruktur der Organisation eines Rechners Generell wird der Prozessor insgesamt als das Gehirn des Rechners bezeichnet. Die Komponenten kommunizieren über so genannte Busse (vgl. Abbildung 2.2). Dieses Kommunikationsmedium ist meist bidirektional und geteilt, d. h. alle Komponenten können lesend und schreibend darauf zugreifen.

Abbildung 2.2.: Kommunikation der Komponenten über einen Bus Auf dem Datenpfad im Prozessor befinden sich die ALU (Rechenwerk, engl. Arithmetic Logic Unit) und die Register. Die ALU ist eine komplexe Schalteinheit, in der die meisten Operationen eines Rechners ausgeführt werden. Register (sowohl spezielle als auch allgemeine) sind sehr einfache und schnelle Speichereinheiten innerhalb der CPU. Jedes Register kann einen Operanden speichern. Die Wortlänge des Operanden ist dabei fest definiert. Sie werden zur kurzzeitigen Speicherung von häufig gebrauchten Operanden seitens der ALU genutzt. Aufgrund ihrer geringen Zugriffszeit (Register sind im

18

Technische Informatik I

2.2. Grobstruktur der Organisation Allgemeinen deutlich schneller als normale Speichereinheiten) erhöht sich somit die Geschwindigkeit der Ausführung der Operationen. Insgesamt sind ALU und Kontrolleinheit sehr viel schneller als die anderen Komponenten des Rechners. Die Kontrolleinheit versetzt den Prozessor in die Lage, alle Aktivitäten zu kontrollieren, zu koordinieren und zu organisieren, ähnlich wie ein Gehirn im menschlichen Körper gegenüber den anderen Organen.

Abbildung 2.3.: Verbindung Prozessor – Hauptspeicher; Erklärung der Einzelteile in Tabelle 2.1 CPU-Komponente

Erklärung

MAR

Memory Address Register, Speicheradressregister: enthält Adresse, zu oder von (je nachdem, ob Lesen oder Schreiben angeregt ist) der ein Wort transferiert wird

MDR

Memory Data Register, Speicherdatenregister: enthält das Datenwort, welches geschrieben oder gelesen werden soll

IR

Instruction Register, Befehlsregister: enthält den Befehl, der momentan ausgeführt wird

PC

Program Counter, Programmzähler: Der Programmzähler zeigt auf den nächsten Befehl, der aus dem Speicher geholt wird, und heißt so, weil er die Befehle hochzählt (er zählt also keine Befehle).

R 0 –R n−1

n allgemeine Register

Tabelle 2.1.: Aufgaben der CPU-Komponenten Die Kontrolleinheit der CPU dekodiert den Inhalt des IR und bestimmt dabei die auszuführenden Operationen. Wozu die allgemeinen Register und die ALU genutzt werden können, zeigt Abbildung 2.4. Der Datenpfad besteht aus den allgemeinen Registern (in der Regel 8–64) und der ALU. Die typische Operation ist eine Register-Register-Operation: Zwei Registerinhalte (Operanden) werden nahezu gleichzeitig in die ALU geladen, diese führt eine Operation aus (in Abbildung 2.4 eine Addition) und das Resultat wird in ein Register zurückgespeichert

Technische Informatik I

19

2. Prinzipielle Funktionsweise eines Rechners

Abbildung 2.4.: Grobstruktur des Datenpfades (welches auch ein Operandenregister sein könnte, das in diesem Fall überschrieben werden würde). Diese Prozedur nennt man den Datenpfadzyklus; er ist das Herz des Prozessors. Je schneller er ist, desto schneller kann der gesamte Rechner werden (die tatsächliche Gesamtgeschwindigkeit hängt jedoch von vielen weiteren Faktoren ab). Des Weiteren gibt es sogenannte Register-Speicher-Operationen: Bei diesen wird ein Operand über MAR und MDR in ein ALU-Register geladen. Die Dateneinheiten, die zwischen Register und Speicher hin- und herbewegt werden, nennt man Datenworte. Sie passen genau in ein Register, d. h. sie haben die gleiche Wortlänge. Zum Ende der Einführung fassen wir die prinzipielle Arbeitsweise eines Rechners noch einmal zusammen: • Der Rechner akzeptiert Informationen in der Form von Programmen und Daten durch Eingabeeinheit und speichert sie. • Von da werden sie in der Regel in die ALU geladen. • Verarbeitete Informationen verlassen den Rechner durch eine Ausgabeeinheit. • Alle Aktivitäten innerhalb des Rechners werden gesteuert durch seine Kontrolleinheit. Wie sind die Komponenten nun im Einzelnen entworfen (insbesondere Prozessor, Speicher, Ein- und Ausgabe) und wie funktionieren sie in sich und miteinander? Diese Fragen werden im Folgenden beantwortet.

20

Technische Informatik I

3. Struktur der CPU 3.1. Die Daten- und Adressleitungen der CPU Abbildung 3.1 zeigt einen Auszug aus einem CPU-Schema. Im Schema werden der Einfachheit halber nur die Adresspfade und die für das Lesen eines Befehls aus dem Speicher (instruction fetch) relevanten Pfade dargestellt. Die Datenpfade, welche benötigt werden, um einen Befehl tatsächlich auszuführen, wurden ausgelassen. Die Adresspfade stellen Schnellstraßen dar, auf denen die Adressen von einem Teil des Prozessors zum anderen gelangen. Eine Adresse gibt an, auf welcher Position im Speicher sich Daten befinden. Es gibt zwei Arten des Informationsflusses in einem Rechner: Adressen und Daten, wobei mit Daten gewöhnlich Anweisungen, Konstanten und Variable gemeint sind. Progr.-zähler

MAR

Inkrementer Hauptspeicher

Befehlsregister Op-Code Operand

SteuerEinheit

MBR

Takt

Adresspfad Datenpfad

Steuersignale

Abbildung 3.1.: Vereinfachtes CPU-Schema Hierzu müssen zwei Bemerkungen gemacht werden. Zuerst einmal gehören die Befehle, die wir hier beschreiben, zu einer Klasse von Anweisungen, die man Ein-Adress-Befehle (one-address instructions) nennt. Das heißt, die Befehle bestehen nur aus zwei Feldern: Dem Operationscode und einem Operanden. Es gibt noch weitere Befehlsklassen (z. B. Zwei- und Drei-Adress-Anweisungen), die zwei oder mehr Operanden haben. Der zwei-

21

3. Struktur der CPU te erwähnenswerte Punkt ist, dass die CPU, die wir hier betrachten, stark vereinfacht ist. Viele reale Prozessoren haben ein solch langes Befehlsformat, dass es notwendig ist, eine Instruktion aufzuspalten und sie in nachfolgenden Speicherpositionen zu hinterlegen. All dies bedeutet, dass die CPU zwei oder mehr Lesezyklen durchlaufen muss, um eine Anweisung aus dem Speicher zu holen. Die Steuereinheit (CU = Control Unit) entnimmt dem Befehlsregister (IR = Instruction Register) den Operationscode und erzeugt Signale, welche alle Teile der CPU steuern. Bei vielen Rechnern liegt der Wert für den Zeitraum zwischen den einzelnen Taktimpulsen im Intervall von 0,01 bis 1 Mikrosekunden (d. h. 10−8 bis 10−6 Sekunden). Dabei ist es die Steuereinheit, die dafür verantwortlich ist, dass: • der Inhalt des Programmzählers in das Speicheradressregister (MAR = Memory Address Register) bewegt wird • ein Lesezyklus durchgeführt wird und • die Inhalte des Speicherpufferregisters (MBR = Memory Buffer Register) in das Befehlsregister überführt. Später werden wir die Steuereinheit, die das komplexeste Bauteil der CPU ist, näher ansehen und demonstrieren, wie sie beim Interpretieren des Anweisungscodes arbeitet. Nachdem wir nun die Befehls-Lesephase betrachtet haben, schauen wir uns an, was sonst noch nötig ist, um Befehle auszuführen. In Abbildung 3.2 wurden Datenpfade und ein Adresspfad vom Adressfeld des Anweisungsregisters (Adresse des Operanden) zum Speicheradressregister zum vereinfachten Schema aus Abbildung 3.1 hinzugefügt. PC

MAR

Inkrementer Hauptspeicher

IR Op-code Operand

MBR

Datenregister D0 CU

Steuersignale

ALU

Adresspfad Datenpfad Steuersignale der CU

Abbildung 3.2.: CPU-Schema, um Datenpfade erweitert

22

Technische Informatik I

3.1. Die Daten- und Adressleitungen der CPU Weitere Änderungen gegenüber der vorigen Abbildung sind die Zugabe eines Datenregisters D 0 und einer Arithmetik-Logik-Einheit (ALU = Arithmetic Logic Unit). Das Datenregister enthält temporäre Zwischenergebnisse während einer Berechnung. Ein solches Datenregister ist in einer Ein-Adress-Architektur notwendig, da binäre Operationen (z. B. +, −, ·, / etc.) mit einem durch die Anweisung spezifizierten Operanden und dem Inhalt des Datenregisters stattfinden. Das Ergebnis der Operation wird wieder im Datenregister hinterlegt und überschreibt somit den originalen Operanden. Später werden wir sehen, dass reale Mikroprozessoren wie der 68000 mehr als ein Datenregister haben (der 68000 hat sogar acht: D 0 bis D 7 ). Einige ältere Rechner und viele der ersten Generation der 8-Bit-Mikroprozessoren haben nur ein allgemeines Datenregister, welches man Akkumulator nannte. In den Lehrveranstaltungen „Algorithmen und Datenstrukturen“ und „Theoretische Informatik“ werden Registermaschinen definiert. Diese beinhalten ebenfalls ein solches Akkumulatorregister, welches meist c 0 genannt wird. Von der ALU werden zwei Arten von Operationen ausgeführt: Arithmetische und logische Operationen. Der grundlegende Unterschied zwischen arithmetischen und logischen Operationen ist, dass bei logischer Operation kein Übertrag erzeugt wird, wenn mit den Bits A i des Wortes A und B i des Wortes B gearbeitet wird. Typische arithmetische Operationen sind die Grundrechenarten. Logische Operationen umfassen die Basis der booleschen Funktion (ODER, UND, NICHT, XOR) und die Shift-Operationen. Berücksichtigen wir (bedingte) Sprünge, muss unser Blockdiagramm erweitert werden (Abbildung 3.3). Zum Ausführen von Sprüngen muss es ermöglicht werden, Sprungadressen in den Programmzähler zu laden. Daher wird ein Adresspfad zwischen Befehlsregister und dem Programmzähler eingefügt. Wir benötigen des Weiteren ein CarryFlipflop für den Übertrag bei Addition und Subtraktion sowie beim Schieben. Sinnvoll erscheint auch, statt nur einem einzelnen Flip-Flop gleich ein echtes Zustandsregister (CCR = Condition Code Register) einzusetzen. Nachdem die CPU eine arithmetische oder eine logische Operation ausgeführt hat, aktualisiert sie die Bits (Flags) ihres Statusregisters, um das Ergebnis zu beschreiben. Die interessanten Bits sind in Tabelle 3.1 aufgelistet. C Z N V

Ist 1, wenn ein Übertrag auftrat, sonst 0 Ist 1, wenn das Ergebnis gleich Null ist, sonst 0 Ist 1, wenn das Ergebnis negativ ist, sonst 0 Ist 1, wenn ein Überlauf auftrat, sonst 0

Tabelle 3.1.: Häufig verwendete Bits im Statusregister Überlauf bedeutet dort, dass eine Operation mit einem oder zwei vorzeichenbehafteten Zweierkomplementwerten ein Ergebnis lieferte, welches außerhalb des erlaubten Intervalls lag. Ein arithmetischer Überlauf tritt während einer Addition genau dann auf, wenn das Vorzeichen des Ergebnisses verschieden von den Vorzeichen beider Operanden ist. Siehe auch Kapitel Rechnerarithmetik. Tabelle 3.2 zeigt einige Beispiele zu Statusbits. Diese werden in Abhängigkeit von der Rechnung und dem sich ergebenden Bitmuster des Ergebnisses gesetzt. Beispielsweise entsteht bei der 2. Operation ein Carry (C = 1), also ein Übertrag, während kein Überlauf (V = 0) eintritt. Die ALU hat hier nur eine binäre Addition ausgeführt, ohne die Daten in irgendeiner Form zu interpretieren. Die Interpretation der Daten, also auch des Ergebnis-

Technische Informatik I

23

3. Struktur der CPU

PC

MAR

Inkrementer Hauptspeicher Sprungadresse

IR Op-Code Operand

MBR

Datenregister D0

C

CU

Steuersignale

ALU

C Z N V ...... Zu testende Bedingung

Zustandsregister (CCR)

Abbildung 3.3.: Erweitertes CPU-Schema. Hinzugekommen sind ein Adresspfad und ein Statusregister ses und der Statusbits, ist Aufgabe des Programms. Handelte es sich beispielsweise um eine vorzeichenlose Integer-Addition, so ist der darstellbare Bereich verlassen worden, was durch einen Übertrag (C = 1) angezeigt wird. Handelte es sich jedoch um eine vorzeichenbehaftete Zahlendarstellung im Zweierkomplement, so wurde der darstellbare Zahlenbereich nicht verlassen (z. B. −1 + 1 = 0), was durch das nicht gesetzte ÜberlaufBit (V = 0) angezeigt wird. Da das Ergebnis 0 ist, wird das Zero-Bit gesetzt ( Z = 1). Das Vorzeichenbit ist eine Kopie des höchstwertigen Bits (N = 0). Operand 1 00000011 11111111 01100110 11001001

+ + + + +

Operand 2 00000100 00000001 00110010 10100000

= = = = =

Ergebnis 00000111 00000000 10011000 01101001

Statusbits C=0, Z=0, N=0, C=1, Z=1, N=0, C=0, Z=0, N=1, C=1, Z=0, N=0,

V=0 V=0 V=1 V=1

Tabelle 3.2.: Vier Beispiele zu Auswirkungen einer Addition auf die Statusbits Das Statusregister wird mit der Steuereinheit verbunden, um bestimmte Zustände nach einer Operation abzutesten. Wir brauchen nun einen Mechanismus, der es uns erlaubt, einen bestimmten Weg im Programm einzuschlagen, falls das Ergebnis eines Tests „wahr“ ist, und einen anderen, falls

24

Technische Informatik I

3.1. Die Daten- und Adressleitungen der CPU dieses Ergebnis „falsch“ lautet. Eine Erweiterung in Abbildung 3.3 besteht aus einem zusätzlichen Pfad zwischen dem Adressfeld des Befehlsregisters und dem Programmzähler. Dadurch ist der Rechner in der Lage, den Zustand des Statusregisters auszuwerten. In Abhängigkeit dieses Ergebnisses wird entschieden, welcher Wert in den Programmzähler geladen wird. Diese Art von Befehlen heißen bedingte Verzweigung oder auch bedingter Sprung. In der Beschreibung sagten wir, dass eine Verzweigung ausgeführt wird, wenn ein Bit gesetzt ist. Natürlich kann man auch genau dann verzweigen, wenn das Bit nicht gesetzt ist (z. B. Z = 0). Die Methode, mit der Verzweigungen letzten Endes wirklich im Rechner implementiert werden, wird im Zusammenhang der näheren Betrachtung der Steuereinheit besprochen. Abbildung 3.4 zeigt, dass ein zusätzlicher Datenpfad zwischen dem Operandenfeld des Anweisungsregisters und der ALU für den Umgang mit Literalen nötig ist. Literale sind Operanden, deren Wert direkt durch den Befehl selbst gegeben wird (immediate addressing mode). PC

MAR

Inkrementer Hauptspeicher

IR Op-Code Operand

MBR

Datenregister D0 CU

ALU

C Z N V ...... CCR

Abbildung 3.4.: CPU-Schema mit Datenpfad zwischen Anweisungsregister und ALU Nachdem wir jetzt einen kurzen Blick auf die CPU eines einfachen hypothetischen Rechners geworfen haben, werden wir uns die Struktur einer komplexeren CPU ansehen, sehr ähnlich der Struktur eines 68000, welcher den Beginn der sogenannten modernen Mikroprozessoren markiert. Die Rechnerarchitektur wurde in der Vergangenheit stärker durch technologische Fortschritte vorangetrieben als durch Fortschritte in der Theoretischen Informatik oder in der

Technische Informatik I

25

3. Struktur der CPU Programmiertechnik. Das heißt, dass ein Programmierer oder Endnutzer eines Rechners die Leistung bekommt, die der Ingenieur ökonomisch produzieren kann. Wenn wir kurz Mainframe-Rechner und Minicomputer vergessen, waren die siebziger Jahre des 20. Jahrhunderts vor allem durch die Einführung des 8-Bit-Mikroprozessor mit sehr wenigen internen Registern geprägt. Durch Fortschritte in der Halbleitertechnologie arbeiteten viele in den Achtzigern eingeführte Mikroprozessoren mit 16- oder 32-Bit-Worten und hatten mehr Register eingebaut. Wir werden einen kurzen Blick auf die Architektur einer CPU werfen, welche die heutigen mächtigen Mikroprozessoren besser repräsentiert. Abbildung 3.5 stellt die Struktur einer CPU dar, mit mehreren (Daten- und Adress-) Bussen und Registerfeldern (statt nur einem einzelnen Datenregister wie in den bisherigen Abbildungen). Obwohl Abbildung 3.5 nur als Beispiel einer CPU gedacht ist, ähnelt es sehr stark der Struktur des 68000-Mikroprozessors. Bus D

Bus E PC

MAR Bus F

A0

HauptSpeicher

A1 Bus A

A2 A3 A4

MBR

Bus B Bus C

D0 IR

D1

Op-Code Adresse A5

D2

A6

D3

A7

CU

ALU1

D4 D5

SteuerSignale D6 D7 ALU2 CCR

Abbildung 3.5.: CPU-Schema, ähnlich dem des 68000

3.2. Datenregister Die acht Datenregister des 68000 machen diesen Mikroprozessor mächtiger als einige seiner Vorgänger, welche nur ein allgemeines Datenregister (Akkumulator) hatten. Dadurch können wir die Ausführung von Programmen beschleunigen, da Daten in Registern nicht mehr aus dem Hauptspeicher geholt werden müssen. Zum Beispiel bedeutet ADD.B

D0,D1

„addiere die Inhalte von D 0 und D 1 und lege das Ergebnis in D 1 ab“. Dabei wird kein Speicherzugriff benötigt (d. h. keine Lese- oder Schreibzyklen). Die einzige Schwierig-

26

Technische Informatik I

3.3. Adressregister keit, die sich durch die Verwendung von mehreren Registern ergibt, ist die Bürde, die sich dem Programmierer dadurch auflädt. Das heißt, der Programmierer muss selbst entscheiden, welches Register denn nun welche Variable enthalten soll. Da die überwiegende Zahl der Programmierer in Hochsprachen programmieren, fällt die Entscheidung, welches Register welche Daten halten soll, dem Compiler zu und erspart dem Programmierer somit das Problem der Registerzuweisung. Programme prozessorspezifisch zu optimieren wird durch die Nutzung von Registerprogrammierung erreicht, wenn dafür spezielle Unterprogramme in Assembler geschrieben werden.

3.3. Adressregister Einer der größten Unterschiede zwischen den Architekturen der CPU aus Abbildung 3.5 und den vorher beschriebenen einfacheren CPUs ist die Zugabe von acht Adressregistern, A 0 – A 7 . Adressregister heißen so, weil sie benutzt werden, um die Hauptspeicheradresse von Operanden zu speichern. Wir werden die Funktion der Adressregister hier nicht im Detail besprechen, da sie später genauer beschrieben werden. Jedoch lohnt es sich trotzdem anzudeuten, wie sie tatsächlich genutzt werden. 1. Beschleunigung von Befehlen, die mit indirekter Adressierung arbeiten. Dabei wird im Befehl die Adresse eines Zeigers auf einen Operanden angegeben, z. B. ADD (A1),D0. Würde dieser Zeiger im Hauptspeicher stehen, so würden zum Laden des Operanden zwei Hauptspeicherzugriffe benötigt. Durch die Verwendung eines Adressregisters wird ein Hauptspeicherzugriff eingespart. Dieser Effekt kommt bei komplexen Indexadressierungen noch mehr zum Tragen. 2. Dynamische Berechnung von Adressen zur Laufzeit durch den Rechner selbst. Da wir arithmetische Operationen auf die Inhalte von Adressregistern anwenden können, können wir mit variablen Adressen arbeiten. Die Berechnung von Adressen zur Laufzeit erlaubt es uns, auf Tabellen oder anderen Datenstrukturen zu operieren, wie wir noch sehen werden. Das macht das Programmieren flexibler und funktionaler; höhere und komplexere Datenstrukturen können benutzt werden. Man sieht daraus, dass komplexere CPUs von heute in erster Linie der Performanzsteigerung dienen. Für das Verstehen der prinzipiellen Funktionsweise soll uns jedoch eine hypothetische Primitiv-CPU genügen.

Technische Informatik I

27

4. Struktur des Hauptspeichers Speichern ist die kurz- oder langfristige Änderung einer oder mehrere physischen Eigenschaften einer Materie. Das gilt auch für den menschlichen Speicher, das Gehirn. Ohne Speicher wären wir z. B. nicht in der Lage, einem Kinofilm zu folgen, da alles, was vor einem bestimmten Zeitpunkt passiert wäre, entschwunden wäre. Wenn der Film angesehen wird, verursacht das optische Signal vom Auge tatsächlich Veränderungen im Gehirn – das Ereignis ist geschehen, aber der Effekt verbleibt. Der Film selbst ist ebenfalls ein Speicher. Die Lichtphotonen, die einmal durch eine Szene produziert wurden, verändern die chemische Struktur in einer Schicht aus Silberhalogenid auf einer Folie aus Kunststoff. Sowohl menschlicher Speicher als auch Fotofilm teilen sich eine Eigenschaft, die man Vergesslichkeit nennt. Menschlicher Speicher kann allmählich nachlassen, wenn er nicht durch Wiederholung oder Erinnerung aufgefrischt wird. Sogar Farbfilme verblassen langsam, wenn sie hellem Sonnenlicht ausgesetzt werden. Mindestens eine Art von Rechnerspeicher, der dynamischer Speicher genannt wird, vergisst ebenfalls die gespeicherten Daten, wenn sie nicht periodisch aufgefrischt werden. Da ein großer Teil der Rechnertechnologie (z. B. der Prozessor) mit binärer arithmetischer und Boolescher Algebra verknüpft ist, ist es sinnvoll zu erwarten, dass Rechnerspeicher ebenfalls dieser Richtung folgt. Die meisten Speichersysteme speichern Informationen in binärer Form, indem sie eine Zweiwertigkeit des Speichermediums ausnutzen. Menschen tun dies ebenfalls bei einigen Anwendungen außerhalb des Rechners. Zum Beispiel denke man an den sprichwörtlichen Knoten im Taschentuch. Die Information wird als ein Bit gespeichert: Knoten oder kein Knoten. Man könnte sich nun fragen, warum Menschen nicht einfach verschiedene Arten von Knoten binden und somit die Informationskapazität des Taschentuchs erhöhen. Die wichtigste Voraussetzung für ein binäres Speicherelement ist, dass zwei (zumindest für einen kurzen Zeitabschnitt) stabile Zustände existieren und dass diese Zustände durch eine Energieschwelle getrennt werden. Wenn es keine Energieschwellen gäbe, welche die Zustände voneinander trennen, wäre es möglich, dass ein gespeicherter Binärwert seinen Zustand z. B. durch die kleinste elektrostatische Veränderung ändert. In unserem Beispiel des Taschentuchs wird ein erheblicher Energiebetrag benötigt, um einen Knoten zu binden oder zu lösen. • Speicherzelle: Eine Speicherzelle ist die kleinste Informationsspeicherungseinheit und kann eine einzelne logische Null oder logische Eins beinhalten. Speicherzellen werden oftmals zusammengefasst, um Wörter zu bilden. Die Position jeder Zelle (oder einer Gruppe von Zellen) im Speicher wird durch ihre Adresse angegeben, oftmals physische Adresse genannt, um sie von der logischen Adresse eines Operanden zu unterscheiden, die durch den Rechner erzeugt wird. • Zugriffszeit: Die Zugriffszeit ist eine der wichtigsten Parameter einer jeden Speicherkomponente und ist die Zeit, die zum Lesen von Daten aus einer bestimmten Speicherstelle vom Beginn des Lesezyklus an benötigt wird. Die Zugriffszeit setzt

29

4. Struktur des Hauptspeichers











30

sich aus zwei Teilen zusammen. Die Zeit, die zum Ausfindigmachen der erforderlichen Speicherzelle benötigt wird, und die Zeit, bis die Daten von der Speicherzelle verfügbar sind. Streng genommen sollten wir sie als Lesezykluszugriffszeit bezeichnen. Da aber viele Halbleiterspeicher identische Lese- und Schreibzugriffszeiten haben, wird der Begriff Zugriffszeit normalerweise sowohl für Lese- als auch für Schreibzugriff verwendet. Dies trifft jedoch nicht auf alle Formen von Speichern zu. Es gibt Geräte, die unterschiedliche Zeiten für das Lesen und Schreiben haben. Wahlfreier Zugriff (random access): Wenn man diesem Ausdruck das erste Mal begegnet, könnte man annehmen, dass er bedeutet, dass wiederholt eine Speicherzelle zufällig ausgewählt wird, bis die gewünschte Zelle gewählt wurde. Dies ist nicht der Fall. Wenn ein Speicher so gestaltet wurde, dass die Zugriffszeit auf jede Zelle darin gleich ist, unabhängig von der eigentliche Position dieser Zelle, so bezeichnet man diesen Speicher als Speicher mit wahlfreiem Zugriff (random access memory, RAM). Das heißt, dass die Zugriffszeit auf Speicher mit wahlfreiem Zugriff konstant ist und nicht von der Position der Daten, auf die zugegriffen wird, abhängig ist. In der Praxis bedeutet dies, dass hinsichtlich der Zeit, die für das Lesen eines Wortes aus dem Speicher benötigt wird, der Prozessor keine Aktion starten muss, da alle Lesezyklen die gleiche Dauer haben. Ein anderer Ausdruck für wahlfreien Zugriff ist direkter Zugriff. Im täglichen Leben ist die nächste Entsprechung für Wahlfreien Speicher ein Selbstwahltelefonsystem. Die Zeit für den Aufbau einer Verbindung zu jedem beliebigen Teilnehmer (Zugriff) ist konstant und hängt nicht von dessen physischen Position ab. Serieller Zugriff: Bei Speicher mit seriellem Zugriff ist die Zeit für den Datenzugriff von der physischen Position der Daten innerhalb des Speichers abhängig und kann aus einem breiten Sortiment von Werten für jedes bestimmte System entspringen. Normalerweise bewegen sich die Daten durch ein Lese-/Schreibgerät, sodass beim Zugriff auf eine bestimmte Speicherzelle die Wartezeit davon abhängt, wie schnell die Speicherzelle sich zum Ein-/Ausgabegerät bewegen kann. Beispiele für Speicher mit seriellem Zugriff sind Magnetbänder, Plattenlaufwerke, Shiftregister und Magnetblasenspeicher. Serieller Zugriff wird auch als sequentieller Zugriff bezeichnet. Flüchtiger Speicher (volatile memory): Flüchtiger Speicher verliert seine Inhalte, wenn die Energiequelle entfernt wird. Dieser Ausdruck trifft auf die meisten Formen von Halbleiterspeichern zu, in dem die Daten als Ladung eines Kondensators oder als Zustand eines Transistors (an oder aus) in einer bistabilen Schaltung gespeichert werden. Speicher, der auf Magnetismus basiert, ist im Allgemeinen nichtflüchtig, da der magnetische Zustand nicht von der Energieversorgung abhängt. Nur-Lese-Speicher: (read-only memory, ROM). Nur-Lese-Speicher kann seine Inhalte auslesen, jedoch (unter normalen Umständen) nicht verändern lassen. Wahre Nur-Lese-Speicher sind per Definition nichtflüchtig. Nur-Lese-Speicher wird häufig benutzt, um Betriebssysteme, Interpreter, Assembler oder andere Systemsoftware (Mikroprogramme) in Mikroprozessoren bereitzustellen. Statischer Speicher: Sobald Daten einmal in eine statische Speicherzelle geschrieben wurden, verbleiben sie dort, bis sie entweder durch Überschreiben mit neuen Daten verändert werden oder die Energiequelle entfernt wird, falls der Speicher flüchtig ist. Statische Halbleiterspeicherzellen verwenden normalerweise über Kreuz verbundene Transistoren (d. h. Flipflops), um die Daten zu halten.

Technische Informatik I

4.1. Speichertechnologie • Dynamischer Speicher: Dynamischer Speicher speichert Daten in Form elektrischer Ladung in der Kapazität zwischen den Elektroden eines Feldeffekttransistors. Da der Kondensator nicht perfekt ist, entweicht die Ladung allmählich, sodass der Kondensator entladen wird und die Daten verloren gehen. Wann immer dynamischer Speicher, der viel billiger als statischer Speicher ist, benutzt wird, muss periodisch (z. B. alle zwei Millisekunden) eine zusätzliche Schaltung dafür sorgen, dass die Ladung des Kondensators wiederhergestellt wird. Diese Operation bezeichnet man als Speicherauffrischung (refresh).

4.1. Speichertechnologie Es gibt viele Wege, binäre Informationen zu speichern, und jede Technik hat ihre eigenen Vor- und Nachteile (Kosten, Geschwindigkeit, Energieverbrauch, Größe usw.). Einige der am häufigsten verwendeten Methoden, die zur Informationsspeicherung benutzt werden, sind: • Elektrisch mit Rückführung: Ein elektronischer Speicher wird in einem bestimmten logischen Zustand gehalten, weil seine Ausgabe wieder als Eingabe benutzt wird, sodass der Zustand konstant bleibt. Ein Flipflop ist ein solches Speicherelement. • Elektrisch mit einer gespeicherten Ladung: Eine elektrische Ladung (ein Überfluss und Mangel an Elektronen) wird in einem Leiter gehalten, der elektrisch von der Umwelt isoliert ist, um ein Entweichen der Ladung zu verhindern. Die Information wird als Ladung oder als fehlende Ladung gespeichert. Das Speicherelement entspricht einem Kondensator und ist das grundlegende Speicherelement dynamischer Halbleiterspeicherchips. • Magnetisch: Einzelne Atome besitzen ein magnetisches Feld, das durch die Drehung der Elektronen hervorgerufen wird (spin). Die Drehung hat eine von zwei Richtungen (auf- oder abwärts), sodass zwei mögliche magnetische Zustände entstehen. Da die Ausrichtung der Elektronendrehung infolge der viel stärkeren thermischen Vibrationen der Atome zufällig ist, gibt es keinen allumfassenden magnetischen Effekt. Eine bestimmte Art von Materialien zeigt jedoch Ferromagnetismus, bei dem die Wechselbeziehungen zwischen den Drehungen von benachbarten Elektronen eine parallele Ausrichtung unter ihnen auslösen. Unter diesen Umständen werden alle Atome in der Materiemenge mit ihren Drehungen gleichmäßig ausgerichtet und das Material ist magnetisiert. Da wir in der Lage sind, ein Material mit seinen Elektronendrehungen durch Magnetisierung in einen von zwei Zuständen zu versetzen und diese Zustände auch wieder zu erkennen, können magnetische Materialien in Rechnerspeichern verwendet werden. • Strukturell: Indem man die Struktur, Form oder Dimensionen (sprich: Größe) eines Gegenstandes verändert, kann man eine sehr große Anzahl von möglichen Zuständen darstellen. In der Praxis verwendeten früher beispielsweise Lochkartenoder -streifen-Systeme das „vorhanden/nicht vorhanden“-Prinzip, bei dem in Papier entweder Löcher gestanzt oder nicht gestanzt waren. Heutzutage findet man strukturelle Speicher in Form von gepressten CDs, DVDs oder Blu-ray Discs. Dort werden Daten durch gepresste Vertiefungen (pits) und Erhöhungen (Lands) abgelegt. Bei beschreibbaren optischen Speichern ändert man beim Beschreiben durch Erhitzen die Reflexionseigenschaften des Trägermediums.

Technische Informatik I

31

4. Struktur des Hauptspeichers • Räumlich: In einem räumlichen Speicher werden die Daten als bewegliche Welle gespeichert, die durch ein bestimmtes Medium reist. Ein Gedankenexperiment ermöglicht den Blick in die Vergangenheit: Man verlässt die Erde mit einer Geschwindigkeit schneller als das Licht, was heutzutage natürlich (noch?) unmöglich ist, und betrachtet die vergangenen Ereignisse auf der Erde durch das emittierte Licht, welches von der Erde mit der konstanten Geschwindigkeit von ungefähr 300 000 km/s ausgestrahlt wurde. Der Speicher des Ereignisses wird durch einen Strom von Photonen dargestellt, die sich durch den Weltraum bewegen. In der Frühzeit der Rechnerentwicklung wandelte man Informationen in Schallimpulse um, die sich durch mit Quecksilber gefüllte Lampen bewegten. Wenn die Impulsfolge, die die gespeicherte Binärsequenz darstellt, von einem Ende der Röhre zum anderen gelangt ist, wird sie erkannt, verstärkt und wieder in Umlauf gebracht. Diese Art von Speicher fand Verwendung als Blasenspeicher (bubble memory), eine Idee, die sich nicht durchsetzen konnte. Statische Halbleiterspeicher waren aus dem Blickwinkel von Entwickler und Ingenieur einfach zu benutzen und wurden normalerweise in kleinen oder mittelgroßen Speichern gefunden. Abbildung 4.1 zeigt den 62256, einen typischen Halbleiterspeicherchip, ein 256K-CMOS-RAM. HM62256BLP/BLFP/BLSP Series A14

1

28

A12

2

27

WE

A7

3

26

A13

A6

4

25

A8

A5

5

24

A9

A4

6

23

A11

A3

7

22

OE

A2

8

21

A10

A1

9

20

CS

A0

10

19

I/O7

I/O0

11

18

I/O6

I/O1

12

17

I/O5

I/O2

13

16

I/O4

VSS

14

15

I/O3

VCC

Pin-Name A0–A14 I/O0–I/O7 CS WE OE VCC VSS

Funktion Adress-Eingabe Dateneingabe/-ausgabe Chip select Write enable Output enable Spannungsversorgung Masse

(Draufsicht)

Abbildung 4.1.: Statischer RAM 62256 32Kx8 Die Abkürzung CMOS gibt die Technologie zur Herstellung des Chips an (vgl. TTL) und die 256K kennzeichnen die Kapazität des Speichers in Bits. Alle Halbleiterspeicher werden in Form ihrer Kapazität (z. B. 1K, 4K, 16K, 64K, 256K, 1M, 4M, 16M Bits) und ihrer Organisation angegeben. Die Organisation einer Speicherkomponente ist ein Ausdruck für die Art und Weise, in der die Speicherstellen intern angeordnet sind. Speicherkomponenten werden als m Wörter mit jeweils n Bits organisiert (die totale Kapazität ist somit m × n ). Einige Speicherkomponenten sind bitweise organisiert, sodass ein solcher bit-organisierter 256K-Chip aus 256K Speicherstellen besteht, die jeweils ein Bit groß sind. Andere sind halbbyteweise angelegt, sodass ein 16K-Chip aus 4K Stellen besteht, die jeweils 4 Bits umfassen. Der 62256 ist byteweise aus 32K-Wörtern zu je acht Bits angeordnet. Solche byteorganisierten

32

Technische Informatik I

4.1. Speichertechnologie Chips eignen sich für kleine Speicher in Mikroprozessorsystemen, in denen ein oder zwei Chips für den Bedarf des Prozessors an Lese-/Schreibspeicher genügen können. Die Pinbelegung des 62256 ist in Abbildung 4.1 abgebildet. Der Schaltkreis hat 28 Kontakte, von denen 15 die Adresseingänge sind, um eine der 215 = 32768 möglichen Positionen auszuwählen. Acht Datenleitungen übertragen Daten vom 62256 während eines Speicherlesezyklus und empfangen Daten vom Prozessor während des Schreibzyklus. Über zwei Kontakte wird der Chip mit elektrischer Spannung versorgt. Die folgenden drei Kontrollkontakte bestimmen die Operation der Speicherkomponente: CS R/W

Chip Select (Auswahl des Chips) Read/Write (Lesen/Schreiben)

OE

Output Enable (Ausgabe aktivieren)

0, wenn der Chip ausgewählt wurde 0, um Daten zu schreiben, wenn der Chip ausgewählt ist 0 für Datenausgabe

Überstrichene Bezeichnungen weisen darauf hin, dass der Kontakt low-aktiv ist, d. h. eine elektrische Null als logische Eins und umgekehrt gewertet wird; der Kontakt wird also mit negativer Logik angesprochen. Damit der Chip Lese- oder Schreiboperation ausführen kann, muss der CS-Pin im Zustand elektrisch Null (logisch Eins) sein. Wenn dies nicht der Fall ist, ignoriert der Speicherschaltkreis die Signale an allen anderen Pins. Durch das Deaktivieren des Schaltkreises mit CS = 1 (intern mittels Tri-State-Gatter) ist es möglich, mehrere der Schaltkreise an einem Bus zu betreiben. Voraussetzung ist, dass nur ein Schaltkreis gleichzeitig aktiv ist. Der R/W -Eingang bestimmt, ob der Chip die Daten an seinen acht Dateneingängen speichern (R/W = 0 →Speichern) oder Daten zu diesen Kontakten übertragen soll (R/W = 1 →Lesen). Der Kontakt OE wird benutzt, um den Tri-State-Bustreiber während eines Prozessorlesezyklus zu aktivieren und zu allen anderen Zeitpunkten zu deaktivieren. Nicht alle Speicherchips haben einen separaten OESteuereingang. Viele Hersteller sparen den einen Kontakt ein, indem sie OE mit CS verbinden (d. h. dass die Ausgangsdatenpuffer automatisch aktiviert werden, wenn CS = 0 und R/OE = 1). Daten werden in diesem und anderen statischen RAM-Chips in Flipflops gespeichert, von denen jedes aus vier bis sechs Transistoren besteht. Abbildung 4.2 zeigt den internen Aufbau eines Chips dieser Art. Glücklicherweise befindet sich sämtliche Elektronik, die zur Adressdekodierung und zum Lesen und Schreiben verwendet wird, bereits auf dem Chip, was den Entwurf des Speichersystems stark erleichtert. Abbildung 4.3 demonstriert, wie der 62256 mit einem 68000-Prozessor verbunden wird. Der 68000 arbeitet mit einem 16 Bit breiten Datenbus. Da ein 62256-Modul nur einen 8Bit-Datenbusanschluss aufweist, schließt man zwei 62256-Module parallel an und “stapelt” die Bytes, um so “ganze” Datenwörter zu erhalten. Beide Speichermodule werden je über ein 15-Bit-Wort adressiert (215 = 32K Wörter Adressraum). Der 68000 verfügt über einen größeren Adressraum von 23 Bit (223 = 8M Wörter Adressraum). Es ist folglich notwendig, für den 68000 festzulegen, welchen Teil des größeren Adressraums er ansprechen muss, um auf die tatsächlich angeschlossenen Speicherbausteine zuzugreifen. Der Einfachheit halber belegt man die unteren 15 Bits der 23-BitAdresse des 68000, um ein Wort auf einem der Speicherbausteine zu adressieren. Anhand der übrigen 8 Bits ( A 16 – A 23 ) wird entschieden, welches der beiden Speichermodule angesprochen und per CS aktiviert wird. Wie das genau geschieht, weiß der Adressdekoder (Adressdekodierungsstrategien werden in Abschnitt 4.3.1 besprochen). Über die Buszu-

Technische Informatik I

33

4. Struktur des Hauptspeichers

VCC VSS

(MSB) A12

A7 A6 A8

¥ ¥ ¥ ¥ ¥

A13 A14

¥ ¥

Zeilendekoder

A5

SpeicherMatrix 512x512

¥ ¥ ¥

A4 (LSB) A3

I/O0 ¥ ¥ ¥ ¥ ¥

¥ ¥

Spalten-E/A

¥ ¥ ¥ ¥ ¥ ¥

Spaltendekoder

EingabeDatenKontrolle

¥

¥

I/O7

¥ ¥

A2 A1 A0 A10 A9 A11

(LSB)

(MSB)

¥ ¥

¥ ¥

CS

Zeitpulsgenerator

WE

Lese/Schreibe-Kontrolle

OE

Abbildung 4.2.: Aufbau des 62256 (Pin-Beschreibung wie auf Abbildung 4.1) griffssteuerungsausgänge UDS und LDS (Upper/Lower Data Strobe) wird die Gültigkeit des oberen bzw. unteren Bytes bestimmt. Einer der Speicherparameter, die für den Entwickler von größtem Interesse sind, ist der zeitliche Verlauf des Speichervorgangs. Diesen stellt man in einem Zeitdiagramm dar, in dem Ursache und Wirkung von Lese- oder Schreibanweisungen abgebildet werden. Der Entwickler des Systems ist für die Beziehung zwischen den Informationen auf den Adress- und Datenbussen und den Speichersteuereingängen zuständig. Abbildung 4.4 zeigt das vereinfachte Zeitdiagramm für einen Speicherchip während eines Lesezyklus. Abbildung 4.4 zeigt das Zeitdiagramm des Adressbusses als zwei parallele Linien, die sich an Punkt A und Punkt B schneiden. Die Verwendung von parallelen Linien ist eine Konvention und bedeutet, dass einige der vierzehn Adressleitungen im logischen Zustand Null und einige im Zustand Eins sind. Es ist aber nicht der eigentliche logische Zustand der Adressleitungen, der von Interesse ist, sondern die Zeit, zu der die Inhalte des Adressbusses für die Dauer des aktuellen Speicherlesezyklus stabil werden. Im Punkt A der Abbildung 4.4 haben sich die Inhalte des Adressbusses vom vorherigen Wert aus vollständig verändert und sind nun stabil. Dieser Punkt wird als Referenz für einige Zeitparameter des Speichers genommen. Da logische Übergänge niemals sofort passieren, wird ihre Veränderung üblicherweise durch eine schräge Linie dargestellt. Zwischen den Punkten A und B gibt der Adressbus die Speicherstelle an, aus der gerade gelesen wird. Während dieser Zeit darf sich die Adresse vom Prozessor nicht ändern. Die Zeit zwischen A und B ist die minimale Zyklenzeit des Speichers. Ein Wert von 80 ns be-

34

Technische Informatik I

4.1. Speichertechnologie Unteres Byte Adressbus

A1–A15

A0–A14 62256 32Kx8 RAM

Datenbus

D0–D15

D 0– D 7 0

OE R/W

R/W

CS

Oberes Byte A0–A14 62256 32Kx8 RAM LDS

D 0– D 7

UDS

R/W

A16–A23

OE

0

CS

& & AdressDekoder

Abbildung 4.3.: 62256-RAM, verbunden mit einem 68000-Prozessor

deutet, dass ein weiterer Speicherzugriff nicht beginnen kann, solange nicht mindestens 80 ns nach dem aktuellen Zyklus verstrichen sind. Die R/W -Leitung muss den gesamten Lesezyklus über gleich logisch Eins sein. Man überlege sich nun die Operation einer Speicherkomponente während eines Lesezyklus. Der Prozessor legt eine Adresse an den Adressbus, die einer Stelle im Speicher entspricht. Die höherwertigen Adressleitungen vom Prozessor ( A 16 – A 23 ) veranlassen, dass die Ausgänge des Adressdecoders berechnet werden und so die Speicherkomponente auswählen, wie in Abbildung 4.3 beschrieben. Im Punkt C von Abbildung 4.4 geht der Chipauswahleingang CS auf logisch Null, wodurch die Tri-State-Bustreiberausgänge mit dem Datenbus verbunden werden. Bis zu Punkt E werden die Inhalte des Datenbuses durch eine einzelne Linie dargestellt, die mitten zwischen den beiden logischen Ebenen liegt. Diese Konventionen bedeutet, dass der Datenbus in einem undefinierten Zustand und von den Datenausgangsverbindungen des Speichers abgenabelt ist. Wenn die Tri-State-Ausgangsverbindungen zum Zeitpunkt E durch das Setzen von CS auf Null eingeschaltet werden, ändert sich der Zustand des Datenbusses auf einen definierten Wert und Daten erscheinen an den Ausgangsklemmen. Unglücklicherweise ist noch nicht genügend Zeit verstrichen, um das adressierte Speicherwort zu lokalisieren und seine Inhalte zu empfangen. Folglich sind die Inhalte des Datenbus zwischen den Zeitpunkten E und F ungültig und können nicht verwendet werden. Zum Zeitpunkt F sind die Daten gültig und die Zeit zwischen den Zeitpunkten A und F heißt Lesezugriffszeit des Chips.

Technische Informatik I

35

4. Struktur des Hauptspeichers Zeit

A

B

Lesezykluszeit > 80 ns Adresse von der CPU

1 Adresse gültig 0 C 1

CS 0 E

F

D

1 Daten vom Speicher

Bus undefiniert 0

Lesezugriffszeit < 80 ns

Datenhaltezeit

Abbildung 4.4.: Zeitdiagramm Lesezyklus für statisches RAM Am Ende des Lesezyklus (Zeitpunkt B) beginnen sich die Inhalte des Adressbus zu verändern. Durch die Übertragungsverzögerung im Chip ändern sich die Daten an den Ausgangskontakten nicht, bevor eine zugesicherte Zeitspanne vorüber ist. Diese Verzögerung nennt man Datenhaltezeit und sie ist die Dauer zwischen den Punkten B und D. Die obige Beschreibung ist eine sehr vereinfachte Beschreibung eines Lesezyklus. Eine vollständigere Beschreibung entnehme man weiterführender Literatur. Der Schreibzyklus ist dem Lesezyklus sehr ähnlich, außer dass R/W im Zustand logisch Null sein muss und dass der Prozessor Daten an die Dateneingänge des Chips legt. Abbildung 4.5 zeigt das vereinfachte Zeitdiagramm für den Schreibzyklus eines 62256-RAM. Während eines Schreibzyklus werden die Daten an den Dateneingängen des Speichers angelegt, R/W auf Null gesetzt und CS ausgelöst. Die Daten werden mit der fallenden Flanke des R/W -Eingangs im Chip verriegelt. Die kritischen Zeitparameter in einem Schreibzyklus sind die Länge des Schreibimpulses (d. h. die minimale Zeit, die R/W = 0 sein muss) und die Dateneinricht- und Datenhaltezeiten im Hinblick auf die fallende Flanke von R/W . Die eigentlichen Besonderheiten der jeweiligen Speicherkomponente werden durch ihren Aufbau bestimmt und der Entwickler sollte das technische Datenblatt sehr aufmerksam lesen, bevor er es in einer Schaltung benutzt.

4.2. Logischer Speicheraufbau 4.2.1. Bits, Bytes und Wörter Betrachten wir zunächst den Hauptspeicher, der auch die Namen Arbeitsspeicher oder RAM trägt. Der Hauptspeicher eines Rechners besteht aus Millionen von Speicherzellen, von denen jede eine Binärziffer, ein sogenanntes Bit aufnehmen kann. Ein einzelnes Bit stellt die kleinste Informationseinheit dar. Man fasst Bits zu Gruppen fester Größe zusammen. Die gebräuchliche Gruppierung besteht aus 8 Bit und wird als Byte bezeichnet. Hauptspeicher sind in der Regel so organisiert, dass nicht einzelne Bytes, sondern eine

36

Technische Informatik I

4.2. Logischer Speicheraufbau A

B Lesezykluszeit > 80 ns

Adresse von der CPU

1 Adresse gültig 0 1

CS 0 1 R/W 0 1 0 Dateneinrichtzeit Datenhaltezeit

Abbildung 4.5.: Zeitdiagramm Schreibzyklus für statisches RAM Gruppe von mehreren Bytes betrachtet wird. Diese Bytegruppen werden Wörter genannt. Ein Wort besteht also aus m Bytes oder aus n = m · 8 Bit. Computer haben üblicherweise eine Wortlänge von 16 bis 64 Bit. Um effizient mit dem Speicher zu kommunizieren, wird in jedem Fall auf ganze Wörter lesend oder schreibend zugegriffen, selbst wenn nur ein einzelnes Byte oder gar Bit von Interesse ist.

4.2.2. Speicheradressierung Damit auf die Wörter im Hauptspeicher zugegriffen werden kann, wird jedem Wort aufeinander folgend eine eindeutige Adresse zugeordnet. Die Adresse ist in der Regel eine von 2k Zahlen aus dem Zahlenbereich von 0 bis 2k − 1, wobei k eine positive Ganzzahl ist. Die Adressen bilden damit den physischen Adressraum des Rechners (siehe Abbildung 4.6). Der Hauptspeicher dieses Rechners kann also bis zu 2k Wörter aufnehmen, die aus mehreren Byte bestehen. Eine 24-Bit-Adresse erzeugt zum Beispiel einen Adressraum von 224 (16.777.216) Speicherzellen. Diese Zahl wird üblicherweise als 16M (16 Megaworte) geschrieben, wobei 1M die Zahl 220 (1.048.576) ist. Eine 32-Bit-Adresse erschafft einen Adressraum der Größe 232 oder 4G (4 Gigaworte).

4.2.3. Bedeutung von Binärinformationen In Speicherzellen stehen zunächst einmal nur Binärinformationen. Erst die Interpretation der Bits und Bytes entscheidet darüber, ob es sich um eine Zahl, mehrere Zahlen, eine Zeichenkette, einen Befehl mit oder ohne Operanden oder anderes handelt. Die Interpretation wird erst bei einer Programmausführung implizit durchgeführt: Lautet bei-

Technische Informatik I

37

4. Struktur des Hauptspeichers

n Bits

Adresse 0

Wort 0

1

bn-1

i

... ... ...

Wort 1

b1 b0

Wort i

k

k

Wort 2 -1

2 -1

Abbildung 4.6.: Adressierung des Hauptspeichers durch Worte

spielsweise eine Anweisung “Hole Befehl von Adresse 18”, so wird erst in dem Moment klar, dass Wort 18 ein Befehl darstellt. Schaute man ohne diese Zusatzinformation in den Speicher, sähe man lediglich eine Kette von Einsen und Nullen. Wenn ein Speicherwort einen Befehl darstellt, spezifiziert ein Teil des Wortes, welche Operation durchgeführt werden soll. Die anderen Teile können benutzt werden, um Operanden oder Operandenadressen zu spezifizieren. Üblicherweise wird der Begriff Feld für diese Teile verwendet. Ein maschinennaher Befehl belegt normalerweise maximal 8 Bit, sodass bei einem 32-Bit-Wort die übrigen 24 Bit je nach Befehl Operanden enthalten, z.B. einen Wert, der addiert werden soll. Das 8-Bit-Operationsfeld kann 28 (256) mögliche Befehle kodieren. Die Adressinformationen können angegeben werden und beinhalten oft Verweise auf CPU-Register. Die verschiedenen Arten, mit denen Operanden in Maschinenbefehlen bezeichnet werden, heißen Adressierungsarten. Im Hauptprozessor gibt es ein Register (d. h. Speicher für ein Wort), welches die Adresse des als nächstes auszuführenden Befehls enthält. Dieses Register heißt Programmzähler (engl. program counter). Alle Speicherworte, auf die der Programmzähler irgendwann während der Programmausführung verweist, werden als Befehle interpretiert. Wird auf eine Adresse im Operandenfeld des Befehls verwiesen, so ergibt sich daraus, dass man hinter dieser Adresse Daten vorfindet. Was für Daten, bestimmt sich aus der Art des Befehls, also aus dem Operationsfeld. Befehle und ihre Operanden müssen nicht die Länge von genau einem Wort haben. Es ist üblich, dass eine gewisse Flexibilität bei der Angabe, ob ein Operand kürzer oder länger als ein Wort ist, möglich ist. Eine Operandenlänge von 8 Bits (das bereits erwähnte Byte) ist recht gebräuchlich, da diese Größe benutzt wird, um Zeichendaten zu kodieren. Um zu ermöglichen, dass Befehle auf einzelne Bytes zugreifen können, ist die kleinste logisch adressierbare Speichereinheit in den meisten Rechnern ein Byte statt eines gesamten Wortes. Das bedeutet, dass aufeinanderfolgende Speicheradressen sich auf aufeinanderfolgende Bytes im Hauptspeicher beziehen. Der Rechner wird in diesem Fall als byte-adressierbar bezeichnet. Gleichwohl wird beim Speicherzugriff weiterhin nur auf ganze Wörter zugegriffen. Die Byteadressierung wird durch eine zwischengeschaltete Logik ermöglicht.

38

Technische Informatik I

4.3. Speicherorganisation

4.2.4. Byte-Ordnung Ein wichtiger Punkt, über den man beim Speicheraufbau Bescheid wissen muss, ist die Anordnung der Bytes innerhalb eines Speicherworts. Im Gegensatz zur intuitiven Annahme, dass die Bytes (und damit die Bits) innerhalb eines Speicherworts von groß nach klein geordnet sind, nutzen die meisten PCs und Laptops einen anderen Ansatz. Ohne Beschränkung der Allgemeinheit betrachtet wir zur Erklärung 32-Bit-Wörter. Ein solches Wort lässt sich in vier Byte unterteilen. Wir benutzen als Beispielwort die Zahl BA 98 76 54 (in hexadezimaler Darstellung1 , entspricht der Dezimalzahl 3.130.553.940). Angenommen, diese Zahl liegt an der Adresse 1000. Lesen wir von dieser Adresse die Werte BA 98 76 54, so heißt dies, dass die Zahl mit dem großen Ende beginnt. Die englische Bezeichnung Big Endian hat sich hierfür etabliert (Eselsbrücke: Big Endian – big end first). Treffen wir stattdessen an der Adresse 1000 die Bytes 54 76 98 BA an, so wird im Speicher das Gegenstück von Big Endian verwendet: Little Endian. Wichtig ist hierbei, dass nur die Bytes die Reihenfolge verändern, nicht aber die Bits innerhalb der Bytes. Die Intel-x86-Rechnerarchitektur, die in den meisten PCs und Notebooks verwendet wird, nutzt Little Endian. Big Endian wurde im PowerPC und in Motorola-68000-Prozessoren eingesetzt. Ebenfalls gibt es für die Bitordnung entsprechend unterschiedliche Anordnungskonzepte. Die PowerPC-Architektur ordnet die Bit innerhalb eines Bytes nach Little Endian. Die Stellen entsprechen also 20 21 22 23 24 25 26 27 . Intel- und Motorola-Architekturen nutzen dagegen eine Big-Endian-Anordnung, ein Byte entspricht also der Anordnung 27 26 25 24 23 22 21 20 . Es gibt noch weitere Byte-Ordnungs-Konzepte (Stichwort Middle Endian), auf die in dieser Lehrveranstaltung aber nicht eingegangen wird.

4.3. Speicherorganisation Wir werden uns nun anschauen, wie die verschiedenen Halbleiterspeicherkomponenten, die ein Speichersystem bilden, im Hinblick auf den Mikroprozessor angeordnet sind. Es geht also um die Speicherorganisation aus der Sicht des Prozessors. Die eindeutig adressierbaren Stellen (z. B. 16M Bytes beim 68000) die den Speicher eines Mikroprozessorsystems bilden, stellt man sich oft als ausgelegte Spalte von Stellen (wie ein Paket Endlospapier) vor, die der Reihe nach durchnummeriert werden, von 00 . . . 0 bis 11 . . . 1. Diese fiktive Kolonne nennt man Adressraum. Da verschiedene Regionen des Adressraum in Blöcke von aufeinanderfolgenden Stellen zusammengefasst werden können, bezeichnet man die resultierende Gruppierung üblicherweise als Speicherabbildung. Die Blöcke, welche die Speicherabbildung bilden, beziehen sich auf Speicherschaltkreise (d. h. Blöcke des RAM, ROM) oder logischen Einheiten (d. h. Programmen, Subroutinen, Editoren etc.). Abbildung 4.7 veranschaulicht dies. Für unsere Betrachtung der Adresskodierung werden wir uns, solange nicht anders angesagt, mit 8-Bit-Mikroprozessoren mit einem 16-Bit-Adressbus, der durch A 0 – A 15 aufgespannt wird (also 64K Bytes Adressraum), befassen. Wir tun dies, weil der 68000 23-Bit1 Hexadezimale Zahlen nutzen als Basis die Zahl 16. Als zusätzliche Symbole für die Zahlen 10–15 werden

die Buchstaben A–F verwendet. Jeweils vier Bit lassen sich auf genau eine hexadezimale Ziffer abbilden, weshalb ein Byte mit zwei hexadezimalen Ziffern dargestellt werden kann.

Technische Informatik I

39

4. Struktur des Hauptspeichers 00... 0 00... 1

0000

Speicheranfang

Programm 1 16K-Abschnitt 216 = 65.536 eindeutige Speicherplätze

3FFF 6000 Programm 2 4K-Abschnitt 6FFF

11... 0 11... 1

Speicherende E000 Programm 3 8K-Abschnitt 8-Bit-Daten

FFFF

Abbildung 4.7.: Adressraum und Speicherabbildung Adressen nutzt, welche unsere Erklärungen, wie Adressdekodierung funktioniert, nur unnötig verkomplizieren würden.

4.3.1. Adressdecoder Wenn der Speicher eines 8-Bit-Mikroprozessorsystems aus einer Speicherkomponente mit 64K eindeutig adressierbaren Stellen aufgebaut wäre, würde das Problem der Adressdekodierung nicht existieren. Jede der Adressausgangsleitungen des Mikroprozessors ( A 0 – A 15 ) wäre einfach mit der entsprechenden Adresseingangsleitung der Speicherkomponente verbunden. In der Mehrheit der Mikroprozessorsysteme jedoch haben die eigentlichen Speicherkomponenten nicht nur weniger als 64K eindeutig adressierbare Stellen, darüber hinaus sind Art und Größe der Speicherkomponenten oftmals bunt gemischt (z. B. 8K × 8-RAM und 16K × 8-ROM). 16-Bit-SystemAdressbus A0–A15

A15

...

A14

...

A9

A0

... A0

... A9

M1 1K x 8

A0 CS1

A9 M2 1K x 8

CS2

8-Bit-Systemdatenbus D0–D7

Abbildung 4.8.: Zwei 1K × 8-Speicher werden mit einem 16-Bit-Adressbus verbunden

40

Technische Informatik I

4.3. Speicherorganisation Man stelle sich die Situation aus Abbildung 4.8 vor, in der zwei 1K ×8-Speicherkomponenten mit einem Systemadressbus verbunden sind, der aus 16 Leitungen besteht. Die zehn Systemadressleitungen A 0 – A 9 sind mit den entsprechenden Adresseingängen der zwei Speicherkomponenten M1 und M2 verbunden. Wann immer eine Stelle (eine von 210 = 1024) in M1 adressiert wird, wird auch die entsprechende Stelle in M2 adressiert. Die Datenausgänge von M1 und M2 sind mit dem Systemdatenbus verbunden. Da die Datenausgänge von M1 und M2 miteinander verbunden sind, ist es notwendig, dass die Datenbustreiber der Speicherkomponenten Tri-State-Gatter sind. Das heißt, dass jeweils nur eine der Speicherkomponenten Daten auf den Systemdatenbus legen kann. Dies wird erreicht, indem man einen Chipauswahl-Eingang CS zu jedem der Speicherchips hinzufügt. Ist der Chipauswahl-Eingang von M1 oder M2 inaktiv, wird der dazugehörige Datenbustreiber deaktiviert, sodass keine Daten auf den Bus gelegt werden können. Die restlichen A 10 – A 15 bestimmen, welcher Chip adressiert wird. Möge CS1 eine Funktion der Adressleitungen A 10 – A 15 sein, so dass CS1 = f 1 (A 15 , A 14 , . . . , A 10 ) ist. Genauso sei CS2 eine Funktion der gleichen Leitungen, sodass CS2 = f 2 (A 15 , A 14 , . . . , A 10 ) Nehmen wir an, wir würden f 1 und f 2 so wählen, dass es keine Werte von A 15 , A 14 , . . . , A 10 gäbe, sodass CS1 und CS2 gleichzeitig aktiviert sind. Unter diesen Umständen ist der Konflikt zwischen M1 und M2 gelöst und die Speicherabbildung des Systems umfasst nun zwei unabhängige 1K-Speicherblöcke. Es gibt mehrere verschiedene Strategien, um A 10 – A 15 zu dekodieren (d. h. f 1 und f 2 entsprechend zu wählen). Diese Strategien können in drei Gruppen eingeteilt werden: Partielle Adressdekodierung, vollständige Adressdekodierung und Blockadressdekodierung.

4.3.2. Partielle Adressdekodierung Partielle Adresskodierung ist die einfachste und folglich die kostengünstigste Form, Adressdekodierung zu implementieren. Abbildung 4.9 zeigt, wie zwei 1K-Speicherblöcke mit einem Systemadressbus verbunden werden, ohne dass gleichzeitig auf beide Speicherblöcke zugegriffen wird. Der Konflikt zwischen M1 und M2 wird gelöst, indem man CS1 direkt mit A 15 des Systemadressbus und CS2 wiederum über einen Inverter mit A 15 verbindet. Auf diese Weise haben wir M1 ausgewählt, wenn A 15 = 0 ist, und M2 , wenn A 15 = 1. Obwohl wir so eine Unterscheidung zwischen M1 und M2 für den geringen Preis eines einzelnen Inverters erreicht haben, haben wir aber einen weitaus höheren Preis bezahlt. Wenn A 15 = 0 nun M1 und A 15 = 1 M2 auswählt, folgt, dass immer M1 oder M2 ausgewählt sind. Abbildung 4.10 gibt die Speicherabbildung für das obige System an, aus der ersichtlich ist, dass M1 32 mal in der oberen Hälfte des Speicherraums wiederholt wird, und entsprechend M2 32 mal in der unteren Hälfte des Speicherraums. Ein Effekt: die Adressen sind bei weitem nicht voll ausgenutzt. Partielle Adressdekodierung ist in Mikrocontrollersystemen weit verbreitet, bei denen geringe Kosten oberste Priorität haben. Der Nachteil, den man dafür in Kauf nehmen

Technische Informatik I

41

4. Struktur des Hauptspeichers A15

...

A10–A14 unbenutzt (d. h. werden nicht dekodiert)

...

A14

A0–A9 selektieren Abschnitt im RAM

A10 A9

A0

A0

... M1 1K x 8

A9

A0

CS1

...

A9

M2 1K x 8

CS2

Systemdatenbus

Abbildung 4.9.: Partielle Adressdekodierung

muss, ist, dass bei einem System mit partieller Adressdekodierung nie der volle Adressraum eines Mikroprozessors genutzt werden kann und die nachträgliche Erweiterung des Speichersystems sehr schwierig ist. Das stellt in der Regel jedoch kein Problem dar, da Mikrocontrollersysteme meist fest eingebaut werden (eingebettete Systeme) und deshalb nur extrem selten Speicheraufrüstungen benötigt werden.

4.3.3. Vollständige Adressdekodierung Ein Mikroprozessor verwendet vollständige Adressdekodierung, wenn jede adressierbare Stelle innerhalb der Speicherkomponenten jeweils einer einzelnen Adresse im Systemadressbus entspricht. Das heißt, dass alle Adressleitungen des Mikroprozessors ( A 0 – A 15 ) benutzt werden, um jede physische Speicherstelle anzusprechen. Vollständige Adressdekodierung stellt das Ideal dar, verlangt jedoch zu viel Dekodierlogik. Zum Beispiel kann ein in den Speicher abgebildetes Gerät („memory mapped“) in einem 68000-System nur ein Kontroll- oder Datenregister haben (die Auswahl erfolgt über A 1 ). Wenn die Komponente vollständig dekodiert wird, müssen die Adressleitungen A 2 – A 23 dekodiert werden. Wenn partielle Adressdekodierung verwendet wird, kann die Peripherie z. B. auf ein 32K-Wörterblock abgebildet werden, welcher somit die Dekodierung auf A 16 – A 23 beschränkt. Man erinnere sich, dass, wenn eine Zweiwort-Peripherie auf einen Block von 32 Wörtern abgebildet wird, die Speicherstellen sich 32K /2 = 16K Mal wiederholen.

42

Technische Informatik I

4.3. Speicherorganisation 0000 1K

M1 03FF 0400 M1 07FF 0800

M1 ...

...

...

M1 wird 32 Mal im Speicherraum 0000–7FFF wiederholt

M1

7BFF 7C00

M1 7FFF 8000 M2 83FF 8400

M2 ...

...

... F7FF F800

M2

M2

M2 wird 32 Mal im Speicherraum 8000-FFFF wiederholt

FBFF FC00 M2 FFFF

Abbildung 4.10.: Die Abbildung 4.9 entsprechende Speicherabbildung

4.3.4. Blockweise Adressdekodierung Die blockweise Adressdekodierung ist ein Kompromiss zwischen partieller und vollständiger Adressdekodierung. Sie vermeidet die ineffiziente Speicherausnutzung der partiellen Adressdekodierung, indem der Adressraum in eine Anzahl von Blöcken unterteilt wird. Diese Blöcke werden als Seiten (pages) bezeichnet. Bei einer typischen Anwendung der blockweisen Adresskodierung wird der 64K-Adressraum eines Mikroprozessors beispielsweise in 16 Blöcke zu je 4K aufgeteilt. Wir können dies unter Verwendung eines relativ kostengünstigen Bauteils tun, welches die vier hochwertigsten Adressleitungen ( A 12 – A 15 ) in 16 Leitungen dekodiert. Jede dieser 16 Leitungen ist mit einem der binären Zustände der vier Adressleitungen verknüpft. Die 16 Ausgänge dieses Adressdecoders können als Chipauswahl-Eingänge (CS) für Speicherkomponenten verwendet werden.

4.3.5. Struktur von Speichersystemen Um den Abschnitt über Speicherorganisation abzuschließen, werden wir uns eine Reihe von Speicherkomponenten ansehen, die mit einem Mikroprozessor verbunden werden. In diesem Fall werden wir den 68000 benutzen. Es gibt zwei Wege, Halbleiterspeicher anzuordnen: Zum einen die Verwendung von byteorganisierten Speicherkomponenten und zum anderen die Verwendung von bitorganisierten Komponenten. Zum Beispiel kann

Technische Informatik I

43

4. Struktur des Hauptspeichers ein 64KB-Speicherblock aus acht 8K × 8-Schaltkreisen oder aus acht 64K × 1-Schaltkreisen aufgebaut werden. D0–D15 Datenbus

D0–D7

D8–D15

Unteres Byte

Oberes Byte

Abbildung 4.11.: Beispiel eines byteorganisierten Speichers Beide Lösungen erfordern acht Chips, aber die 64K × 1-Lösung wird ausnahmslos bevorzugt. Warum ist dies der Fall? Weil das Laden des Datenbus auf eine Verbindung pro Schaltkreis reduziert wurde, während die 8K × 8-Bit-Lösung acht Verbindungen pro Schaltkreis erfordert. Damit können die Speicherchips in einem kleineren Gehäuse untergebracht werden. Die Verwendung von byteorganisierten Komponenten verschwendet jedoch weniger Energie, da weniger Speicherchips gleichzeitig ausgewählt werden müssen. Die Anzahl der Pins eines Chips stellt eine kritische Größe dar! Abbildung 4.11 und Abbildung 4.12 zeigen den Aufbau eines byteorganisierten bzw. eines bitorganisierten Speichers.

Abbildung 4.12.: Beispiel eines bitorganisierten Speichers

4.4. Dynamischer Speicher vs. Statischer Speicher Dynamischer Speicher mit wahlfreiem Lese-/Schreibzugriff (DRAM) oder einfach dynamischer Speicher ist die kompakteste und preiswerteste Form von heutzutage verfügbarem Halbleiterspeicher. Der gegenwärtige Industriestandard für dynamische RAMChips hat eine Kapazität von 512M × 1 Bits, größere Chips sind auch schon in Produktion. Dynamischer RAM ist nicht nur preiswerter (ausgedrückt in Bits/Chip) als statischer

44

Technische Informatik I

4.4. Dynamischer Speicher vs. Statischer Speicher Speicher. DRAM-Chips verbrauchen darüber hinaus noch weniger Energie als einige ihrer statischen Gegenstücke.2 Was ist nun der Unterschied zwischen statischem und dynamischen RAM. Die typische Antwort auf diese Frage: „Statischer RAM verfügt über keine interne Steuer-Elektronik, dynamischer RAM benötigt diese!“ Der Betrieb solcher Steuerelektronik ist sehr aufwendig und nicht unkritisch. Der statische RAM speichert Daten in Form eines Zustandes von zwei über Kreuz verbundener Transistoren (RS-Flipflop). Einer der Transistoren dieses Paares ist an, der andere aus. Der gewöhnliche statische Speicherchip erfordert vier weitere Transistoren pro Flipflop, was eine Gesamtmenge von sechs Transistoren pro Bit gespeicherter Information ergibt. Ein dynamischer Speicherchip enthält die gesamte Elektronik, die für den Zugriff auf eine bestimmte Zelle erforderlich ist, d. h. um eine logische Eins oder logische Null in einem Schreibzyklus zu schreiben oder die Inhalte in einem Lesezyklus zu lesen. Die meisten dynamischen Speicher garantieren, die Daten über eine Periode von 2000 Mikrosekunden (2 Millisekunden) zu halten, nachdem sie geschrieben wurden. Um die Daten länger als 2 Millisekunden zu halten, müssen die Daten in jeder Zelle des dynamischen Speichers innerhalb dieser Zeit periodisch neu geschrieben werden. Die Auffrischung von dynamischem Speicher ist in weiten Teilen dafür verantwortlich, dass die Verwendung von dynamischem Speicher so schwierig ist. In der Praxis frischt der Zugriff auf eine Speicherzelle sie auf, wie wir sehen werden. Weiterhin ist es möglich, eine Gruppe von 128, 256 oder 512 Zellen gleichzeitig aufzufrischen.

A8

1

16

VSS

D

2

15

CAS

W

3

14

Q

RAS

4

13

A6

A0

5

12

A3

A2

6

11

A4

A1

7

10

A5

8

9

A7

VCC

(262144x1)

Abbildung 4.13.: Interner Aufbau eines dynamischen 256K-RAM Abbildung 4.13 veranschaulicht den Aufbau eines dynamischen 256K × 1-Speicherchips, der in einem Gehäuse mit 16 Kontakten ausgeliefert wird. Dadurch, dass die Hersteller den Speicherchip mit 16 Pins untergebracht haben, besteht ein neues Problem für den Entwickler. Ein 256K-Adressraum wird durch 18 Adressleitungen (218 = 256K ) aufgespannt, sodass 16 Kontakte nicht in der Lage sind, als Adresseingänge, Datenleitungen, Energieversorgungsanschlüsse und Steuerleitungen zu dienen. Der offensichtliche 2 Diese Aussage gilt nicht für statischen CMOS-RAM, der einen sehr geringen Energieverbrauch hat.

Technische Informatik I

45

4. Struktur des Hauptspeichers Mangel an Kontakten kann überwunden werden, indem man Multiplexverarbeitung auf den Adressbus anwendet: Die Adresse wird als zwei aufeinanderfolgende 9-Bit-Werte in den Chip eingegeben. Dies erschwert die Verwendung von dynamischem Speicher natürlich noch weiter. Da die 256K × 1-Speicherkomponente nur ein einzelnes Bit in jeder der 256K adressierbaren Stellen enthält, sind sechzehn von diesen Chips nötig, um ein 16 Bit breites Speichermodul zu bilden. Ungleich statischem RAM, dessen Zugriffszeit gleich seiner Zyklenzeit ist, hat das dynamische RAM eine Zyklenzeit, die größer als seine Zugriffszeit ist. Dies liegt daran, dass bestimmte interne Operationen im DRAM stattfinden müssen, bevor ein weiterer Zugriff erfolgen kann. Ein typischer Chip hat beispielsweise eine Zugriffszeit von 100 ns und eine Zykluszeit von 150 ns. MSEL

A0 . . . A8

ZeilenAdressSpeicher

8

18-BitAdresse von der CPU

AdressMUX

A9 . . . A17

SpaltenAdressSpeicher

O0 . . . O8

DRAMAdresse

8

8 LE

AdressRefreshGenerator

CLR RSEL0 RSEL1

TC

BankAuswahlSpeicher

RAS1 RASDekoder

RAS2

RASI RAS3

Wählt eine von vier DRAMBänken aus

RFSH CASI

CASPuffer

CASO

CAS aller DRAMBänke

Abbildung 4.14.: Steuerung von dynamischen Speicher Einen Überblick über die Logik, die für die Steuerung eines dynamischen Speichers notwendig ist, wird in Abbildung 4.14 gezeigt. Die Steuerung des dynamischen Speichers muss das Adressenmultiplexing durchführen und die entsprechenden Steuersignale erzeugen. Es muss auch alle zwei Millisekunden 256 Zeilenauffrischzyklen durchführen. Um dies zu tun, muss eine Auffrischanforderung zum Prozessor gesendet werden und eine Genehmigung dafür von ihm empfangen werden. Wenn das Auffrischen genehmigt wurde, sendet der Controller eine Zeilenauffrischadresse zum Speicher und setzt

46

Technische Informatik I

4.5. Andere Formen von Halbleiterspeicher den Eingang RAS auf logisch Null. Der Auffrischadresszähler wird automatisch erhöht. Glücklicherweise ist es heutzutage möglich, den Großteil der benötigten Elektronik für die Implementierung einer Steuerung für dynamischen Speicher auf einem einzelnen Chip unterzubringen. Der dynamische Halbleiterspeicher leidet unter zwei für diese Art von Speicher eigentümlichen Schwächen. Wenn auf eine Speicherzelle zugegriffen und die Ladung zwischen den Elektroden des Kondensators verändert wird, benötigt der dynamische Speicher von der Energieversorgung kurzzeitig einen großen Strom, was einen Spannungsverlust entlang den Energieversorgungsleitungen verursacht. Dieser kann durch gezielte Anordnung der Schaltkreise des Speichersystems reduziert werden. Eine weitere Schwäche des dynamischen Speichers ist seine Empfindlichkeit gegenüber Alphateilchen. Der Chip ist in Kunststoff- oder Keramikmaterial eingekapselt, welcher winzige Mengen radioaktiven Materials enthalten kann. Eines der Produkte radioaktiven Zerfalls sind Alphateilchen (Heliumkerne), die hochgradig ionisierend wirken und somit die Daten in den Zellen zerstören, die sie passieren. Die Anzahl der Alphapartikel kann durch sorgfältige Qualitätskontrolle bei der Auswahl des Kapselmaterials reduziert, niemals jedoch ganz beseitigt werden. In den meisten Personalcomputern ist ein gelegentliches beschädigtes Bit lediglich ärgerlich, in den meisten professionellen Systemen können die Folgen weitaus ernster sein. Die praktische Lösung für dieses Problem sind fehlerkorrigierende Codes. Einem 16-BitDatenwort werden fünf Bits angehängt, um ein 21-Bit-Codewort zu schaffen. Wenn dieses Codewort aus dem dynamischen RAM zurückgelesen wird, ist es möglich, zu errechnen, welches Bit falsch ist, und den Fehler zu korrigieren. Da dynamisches RAM schwieriger zu verwenden ist als statisches RAM, tendiert man heute dazu, es in größeren Systemen wie Minicomputern oder Großrechnern zu verwenden. Jedoch wurde über die letzten Jahre hinweg dynamischer RAM von Herstellern vieler Personalcomputer übernommen. Nur Einplatinenmikrokontroller mit Speicher kleiner als 64KB, Systeme, die mehr Wert auf Verlässlichkeit als Kosten legen oder Hochgeschwindigkeitsrechner benutzen heutzutage statischen Speicher. Einige größere Mikroprozessorsysteme nutzen den teureren statischen Speicher, da er verlässlicher als dynamischer RAM ist.

4.5. Andere Formen von Halbleiterspeicher 4.5.1. ROM Halbleiter eignet sich besonders gut zur Produktion von sehr dichten, preiswerten NurLesen-Speichern. Es gibt mehre Arten von ROM: maskenprogrammiert, PROM, EPROM und EEPROM sind diejenigen, denen man am häufigsten begegnet. Im Allgemeinen ist Halbleiter-ROM, solange nicht anders angesagt, von der maskenprogrammierten Sorte, die während der Herstellung des Chips programmiert werden. Maskenprogrammierter ROM ist in Größen von 8K- bis 1M-Bits in 1K × 8 und 256 × 8 mit 24- oder 28-poligen Gehäusen verfügbar. Die Anwendung von ROM ist noch einfacher als die von statischem Halbleiter-RAM. Da der ROM niemals beschrieben wird, braucht ein ROM nicht mehr als die Adresse der Stelle, auf die zugegriffen wird, und ein Chipauswahlsignal um die Ausgangsleitungen des Datenbusses des Chips zu steuern.

Technische Informatik I

47

4. Struktur des Hauptspeichers Genauso wie andere Komponenten war der ROM für die Entwicklung von Personalcomputern und anderen preiswerten Rechnern verantwortlich. In den 1980er Jahren wurden oft Betriebssysteme und BASIC-Interpreter auf einem ROM ausgeliefert, wobei 8K bis 512K benötigt wurden. Heute sind es Urlader und BIOS, die in den ROM gespeichert werden. Der ROM lässt sich in der Regel in einem bis 4 Chips realisieren. Obwohl ROMs nicht neu programmiert oder verändert werden können ist es nicht sehr teuer, die ROMs zu entfernen, die die Systemsoftware enthalten. Sie werden durch eine neuere Version ersetzt. Eine andere Anwendung des ROM findet sich in bestimmten Mikroprozessor-basierten Controllern. Wenn ein Mikrocomputer einer bestimmten Aufgabe zugewiesen wurde, z. B. Software auf Lebensdauer des Gerätes festgelegt. Ein ROM stellt die kostengünstige und robuste Methode dar, solche Software zu speichern.

4.5.2. EPROM Der EPROM ist ein löschbarer und programmierbarer Nur-Lese-Speicher. Er teilt sich einige grundlegende Eigenschaften mit dem maskenprogrammierten ROM. Viele EPROMs können sogar in Steckplätze gesetzt werden, die für maskenprogrammierte ROMs ausgelegt sind, da die Schaltkreise pinkompatibel sind. Der EPROM kann durch den Nutzer mit Hilfe eines EPROM-Brenners programmiert werden. Die Daten im EPROM werden in Form von elektrostatischen Ladungen in hochgradig isolierten Leitern gespeichert. Die Ladungen bleiben über einen Zeitraum von über zehn Jahren bestehen, ohne zu entweichen. Der EPROM kostet ein Vielfaches eines maskenprogrammierten ROM der gleichen Dichte, hat aber den Vorteil, dass Kleinserienfertigungen von Mikroprozessorsystemen machbar sind, die ihre Betriebsystem in EPROM mit sich führen. Obwohl maskenprogrammierte ROMs relativ billig sind, verlangt der Hersteller normalerweise mehrere tausend Euro für das Einrichten der Maske. Der EPROM kann mehrmals benutzt werden, da die gespeicherten Daten durch Beleuchten des Siliziumchips mit ultraviolettem Licht gelöscht werden können. Der Siliziumchip befindet sich im Gehäuse unter einem Quarzfenster, das für ultraviolettes Licht durchlässig ist. Ultraviolettes Licht sorgt dafür, dass die Ladung durch die Isolierung versickert (ionisierende Strahlung). Da EPROMs programmiert, gelöscht und neu programmiert werden können, eignen sie sich für kleinere Projekte und Entwicklungsarbeiten in Laboren. Sobald ein Programm in einen EPROM geschrieben und Fehler bereinigt wurden, kann es später in ein maskenprogrammiertes ROM eingebunden werden, falls dies notwendig ist. Ein Fehler in einem EPROM kostet weniger als der gleiche Fehler in einem maskenprogrammierten ROM.

4.5.3. EEPROM Eine moderne Weiterentwicklung des EPROM ist der elektrisch löschbare und programmierbare ROM. Dieses EEPROM kann an Ort und Stelle programmiert werden und muss nicht aus dem System entnommen und ultraviolettem Licht ausgesetzt werden. Was ist nun aber der Unterschied zwischen RAM und EEPROM? Der EEPROM ist nicht-flüchtig wie das normale Halbleiter-RAM. Unglücklicherweise ist er viel teurer als HalbleiterRAM, erfordert häufig nicht-standard-konforme Energiequellen und hat eine relativ hohe Zugriffszeit. Der EEPROM findet sich in speziellen Anwendungen, wo Daten gehalten

48

Technische Informatik I

4.5. Andere Formen von Halbleiterspeicher werden müssen, wenn der Strom weg ist. Eine typische Anwendung ist ein Radioempfänger, der eine Anzahl von verschiedenen Frequenzen speichern und wiederherstellen kann, wenn der Strom wieder angelegt wird.

4.5.4. PROM Der programmierbare ROM kann durch den Nutzer programmiert werden wie der EPROM. Jedoch wird der PROM beschrieben, indem winzige metallische Verbindungen im Chip zerstört werden, in dem ein so hoher Strom durch sie geschickt wird, dass sie zerfließen. Eine durchgehende Verbindung stellt eine logische Eins dar und eine unterbrochene Verbindung demzufolge logisch Null. Klar ist somit, dass ein PROM einmal und nur genau einmal programmiert werden kann. Durch die Art, wie dies getan wird, besitzt der PROM eine sehr geringe Zugriffszeit (10–50 ns) und wird eher als logisches Element statt zur Speicherung eines Programms verwendet.

4.5.5. Pseudo-ROM Bis vor kurzem wurde die Mehrheit aller LSI-Chips durch einen NMOS-Prozess hergestellt und erforderte mehrere zig bis mehrere Hundert mA für normalen Betrieb. Eine andere Technik zur LSI-Chipherstellung nennt sich CMOS. Obwohl CMOS-Technologie schon seit längerem bekannt ist, waren die Kosten von CMOS bis in die 1980er Jahre für alle Geräte mit Ausnahme einiger Spezialgeräte sehr hoch. Wenn nicht auf den CMOS-Speicher zugegriffen wird, ist sein Stromverbrauch so gering, dass eine kleine Batterie, die ungefähr 2,8 V erzeugt, ausreicht, um die gespeicherten Daten zu halten, wenn die Hauptenergiequelle abgetrennt wird. Solch ein Speicher ist zweifellos Lese/Schreibspeicher, da es jedoch nicht-flüchtig ist, hat er einige Eigenschaften von ROM. Er wird deshalb auch als NVRAM (non-volatile RAM, nichtflüchtiger RAM) bezeichnet.

Technische Informatik I

49

5. Programm- und Datenstrukturen Die Auseinandersetzung mit maschinennaher Assemblerprogrammierung wird gern vermieden. Zwar ist in der Regel bekannt, dass Assemblerprogramme sehr schnell und flexibel sind, aber heutzutage programmiert man objektorientiert in C++, C#, Java oder, falls nötig, in C. Der Grund ist einfach: In Hochsprachen geschriebene Programme sind für einen Programmierer erheblich leichter lesbar und damit nachvollziehbarer. Das erleichtert die Wartung und Erweiterung bestehender Programme. Um diese Intuitivität zu erreichen, werden viele Prozesse und Mechanismen des Computers, die im Hintergrund stattfinden, ausgeblendet. Um jedoch zu verstehen, wie ein Computer funktioniert und aufgebaut ist, muss man genau diese Detailfragen in den Fokus rücken. Ein erster Schritt hierfür ist die Auseinandersetzung mit maschinennaher Programmierung und – zwangsläufig – mit dem Aufbau eines Rechners.

5.1. Datentransfer zwischen Hauptspeicher und CPU-Registern Zum Ausführen eines Befehls müssen sowohl der Befehl als auch erforderliche Operanden vom Hauptspeicher in die CPU und von der CPU in den Hauptspeicher transferiert werden. Dadurch werden zwei grundlegende Operationen für den Zugriff auf den Hauptspeicher benötigt: Lesen und Schreiben. Die Details der Hardwareimplementierung dieser beiden Operationen werden später noch behandelt. In diesem Kapitel soll es uns ausschließlich um den Blickwinkel des Programmierers gehen, sodass wir uns auf die logische Behandlung von Anweisungen und Operanden konzentrieren. Bestimmte Hardwarekomponenten, wie zum Beispiel CPURegister, werden nur soweit besprochen, wie es für das Verständnis der Ausführung von Maschinenanweisungen und Programmen nötig ist. Beim Datentransfer zwischen Hauptspeicher und CPU-Registern werden Speicherplätze durch Namen identifiziert. Zum Beispiel verwendet man frei wählbare Namen wie LOC, PLACE, A oder VAR2 für Speicherplätze und nummerierte Bezeichnungen wie R 0 oder R 5 für Register. Die Inhalte einer Speicherstelle oder eines Registers werden durch den in eckigen Klammern gefassten Namen dargestellt. Folglich steht der Ausdruck R 1 ← [LOC ] für die Überführung der Inhalte von Speicherstelle LOC in das Register R 1 .

5.2. Arithmetische und logische Operationen auf Daten Die Operation „Addiere zwei Zahlen“ ist eine grundlegende Funktion eines jeden Rechners. Der Befehl C=A+B in einer höheren Programmiersprache erfordert also folgende Aktionen: C ← [A] + [B ]

51

5. Programm- und Datenstrukturen Die Speicherplätze werden in der Regel mit großen Buchstaben bezeichnet. Der obige Befehl bedeutet: Die Inhalte von A und B werden in die CPU geladen und dort addiert. Das Resultat wird zurück in den Speicher (auf Adresse C) geschrieben. A und B bleiben dabei erhalten. Nehmen wir an, dass diese Aktion durch eine einzelne Maschinenanweisung ausgedrückt wird. Solch eine Instruktion muss drei Adressfelder umfassen, welche jeweils die Adresse von einem der drei Operanden aufnehmen müssen. Das Operationsfeld der Anweisung bestimmt, welche Addition ausgeführt werden soll. Diese Drei-AdressenAnweisung kann symbolisch durch ADD A,B,C dargestellt werden. (A und B sind Adressen der Quelloperanden, C enthält den Zieloperanden). Eine Anweisung dieses Typs hat die allgemeine Form Operation Quelle1, Quelle2, Ziel Wenn k Bits benötigt werden, um die Speicheradresse eines jeden Operanden auszudrücken, muss die kodierte Form der obigen Anweisung 3 · k Bits für Adressierungszwecke beinhalten, zusätzlich zu den Bits, die zur Kodierung der Operation selbst notwendig sind. Ein alternativer Ansatz nutzt eine Folge von Instruktionen, um die gleiche Aufgabe durchzuführen. Dabei hat jede Anweisung nur einen oder zwei Operanden. Viele Computer haben Zwei-Adressen-Anweisungen der Form Operation Quelle, Ziel in unserem Fall ADD A,B Hierbei wird B ← [A] + [B ] ausgeführt. Wenn die Summe berechnet wurde, wird das Ergebnis in den Hauptspeicher geschickt und an der Stelle B gespeichert. Hierbei wird der originale Inhalt dieser Position, hier B, überschrieben. Das bedeutet also, dass B sowohl Quelle als auch Ziel ist. Eine einzelne Zwei-Adressen-Anweisung reicht nicht aus, um unser ursprüngliches Problem zu lösen. Die Aufgabe ist, die Inhalte von A und B zu addieren, ohne sie zu zerstören und die Summe an die Speicheradresse C zu schreiben. Das Problem kann gelöst werden, in dem man eine weitere Zwei-Adressen-Anweisung nutzt, welche die Inhalte einer Speicheradresse an eine andere überführt. Solch eine Anweisung würde die Operation C ← [B ] durchführen. Hierbei bleibt der Inhalt von Speicherstelle B unverändert. Der Begriff Move (Bewegen), der sich für diese Operation durchgesetzt hat, ist irreführend, da es sich dabei um ein Kopieren (Copy) handelt. Jedoch ist der Name der Anweisung tief in der Rechnersprache verwurzelt. Die Operation C ← [A] + [B ] kann nun durch die folgenden Zwei-Adressen-Anweisungen durchgeführt werden: MOVE B,C ;Kopiere B nach C ADD A,C ;Addiere A zu C In allen bisher angegebenen Befehlen wurden die Quelloperanden zuerst angegeben, gefolgt von den Zieloperanden. Diese Reihenfolge wird in vielen Rechnern verwendet, inklusive unserer ersten Beispiel-Maschine Motorola 68000. Aber es gibt ebenso viele Rechner, in denen die Reihenfolge von Quellen und Zielen umgekehrt wurde, wie es bei unserem zweiten Beispiel (dem PowerPC) der Fall ist. In der Assemblersprache des PowerPC würde eine Drei-Adressen-Anweisung wie folgt geschrieben: Operation Ziel, Quelle1, Quelle2 Unglücklicherweise gibt es hierbei keine Konvention, die von allen Herstellern berücksichtigt wird. Tatsächlich ist es sogar so, dass im Befehlssatz einer Maschine die verschie-

52

Technische Informatik I

5.2. Arithmetische und logische Operationen auf Daten denen Assemblersprachkonventionen verschiedene Reihenfolgen für verschiedene Befehle benutzen. Wir werden hier immer die Quelloperanden zuerst angeben, es sei denn, wir beschäftigen uns gezielt mit den PowerPC-Anweisungen. Wir haben verschiedene Drei- und Zwei-Adressen-Anweisungen angesprochen. Eine andere Möglichkeit ist es, Maschinenbefehle mit nur einem Operanden zu verwenden. Wenn ein zweiter Operand benötigt wird, wie es im Falle einer Addition der Fall ist, wird dessen Quelle implizit verstanden. Das heißt, der zweite Operand befindet sich immer an einer bekannten Stelle, welche nicht explizit in dem Befehl angegeben werden muss. Ein CPU-Register, das normalerweise als Akkumulator bezeichnet wird, kann für diese Zwecke genutzt werden.1 Die Ein-Adress-Anweisung ADD A bedeutet dann: Addiere den Inhalt der Speicherstelle A zum Inhalt des Akkumulators und hinterlege die Summe im Akkumulator. Führen wir noch zwei weitere Ein-Adress-Anweisungen ein: LOAD A und STORE A. Die Ladeanweisung lädt den Inhalt der Speicherstelle A in den Akkumulator, und die Speicherungsanweisung kopiert umgekehrt den Inhalt des Akkumulators in die Speicherstelle A. Wenn man nur Ein-Adresse-Anweisungen verwendet, kann die Operation C ← [A] + [B ] durch die folgende Anweisungsfolge durchgeführt werden: LOAD ADD STORE

A B C

;Lade A in den Akku ;Addiere B zum Akku ;Speichere Akku in C

Man beachte, dass die Operanden, die im Operandenfeld angegeben werden, Quelle oder Ziel sein können. Das hängt vom Befehl ab. Im Lade-Befehl gibt die Adresse A einen Quelloperanden an und die Zieladresse ist per Definition der Akkumulator. Andererseits gibt C die Adressposition des Speicherungs-Befehls an, wobei die Quelle (wieder der Akkumulator) implizit bekannt ist. Viele Rechner haben eine gewisse Anzahl von Allgemeinregister (im Folgenden auch immer wieder als Mehrzweckregister bezeichnet), typischerweise zwischen 8 und 64, wobei auch in manchen Fällen mehr denkbar sind. Der Zugriff auf die Daten in diesen Registern ist viel schneller als der Zugriff auf Daten im Hauptspeicher, da die Register Bestandteile der CPU sind. Da die Anzahl der Register klein ist, werden nur wenige Bits zur Adressierung benötigt, um zu ermitteln, welches Register an einer Operation beteiligt ist. Bei 32 Registern sind nur fünf Bit notwendig. Dies ist viel weniger als die Anzahl der Bits, die zur Hauptspeicheradressierung nötig sind. Da die Nutzung von Registern eine schnellere Bearbeitung bei kürzeren Befehlen ermöglicht, werden Register zur temporären Speicherung von Daten während der Berechnung innerhalb der CPU verwendet. In vielen modernen Prozessoren wie dem PowerPC können Berechnungsbefehle direkt auf den Daten, die sich in der CPU befinden, ausgeführt werden. Sei R i ein Allgemeinregister. Die Befehlsfolge MOVE MOVE

A,R i R i ,A

und ADD

A,R i

1 Das Prinzip findet auch bei Registermaschinen Verwendung, welche in den Lehrveranstaltungen „Algo-

rithmen und Datenstrukturen“ sowie „Theoretische Informatik“ definiert werden.

Technische Informatik I

53

5. Programm- und Datenstrukturen sind Verallgemeinerungen der Lese-, Speicherungs- und Additionsbefehle des akkumulatorbasierten Falles, in dem das Register R i die Funktion des Akkumulators übernimmt (man beachte die Reihenfolge der Operanden). Wenn ein Prozessor mehrere Allgemeinregister besitzt, nutzen viele Anweisungen nur Operanden, die sich in den Registern befinden. Befehle wie z. B. ADD R i ,R j oder ADD R i ,R j ,R k sind von dieser Art. In beiden Befehlen sind die Quelloperanden die Inhalte der Register R i und R j . Im ersten Befehl dient R j auch als Zielregister, hingegen wird im zweiten Befehl ein drittes Register (R k ) als Ziel verwendet. Die Geschwindigkeit, mit der eine gegebene Berechnung ausgeführt wird, hängt von der Zeit ab, die zum Übertragen der Operanden zwischen den einzelnen Speicherpositionen und der ALU für die benötigte Operation nötig ist. Übertragungen, die den Hauptspeicher beinhalten, sind viel langsamer als Übertragungen innerhalb der CPU. Folglich erreicht man einen spürbaren Geschwindigkeitsanstieg, wenn mehrere Operationen in Folge mit Daten in den CPU-Registern durchgeführt werden. Die Daten müssen nicht aus dem oder in den Hauptspeicher kopiert werden. Wenn Compiler aus Hochsprachenquelltexten Maschinensprache erzeugen, ist es wichtig, dass sie die Häufigkeit, mit der Daten zwischen Hauptspeicher und CPU ausgetauscht werden, möglichst gering halten.

5.2.1. Befehlsausführung Die Ausführung eines Befehls ist eine Prozedur mit zwei grundlegenden Phasen. In der ersten Phase, der sogenannten Befehlsholphase (instruction fetch), wird der Befehl aus dem Hauptspeicher gelesen. Die Adresse hierfür befindet sich im Programmzähler (program counter) Der geholte Befehl wird nun im Befehlsregister der CPU hinterlegt. Zu Beginn der zweiten Phase, die man Befehls-Ausführungsphase (instruction execute) nennt, wird der Befehl im Befehlsregister untersucht, um zu bestimmen, welche Operation ausgeführt werden soll. Die so spezifizierte Operation wird anschließend durch die CPU ausgeführt. Dies kann beinhalten, dass Operanden aus dem Hauptspeicher gelesen werden, eine arithmetische oder logische Operation ausgeführt, ein Ergebnis in den Hauptspeicher geschrieben oder eine Kombination aus all diesen Grundoperationen ausgeführt wird. An einem bestimmten Punkt während dieser zweiphasigen Ausführung wird der Programmzähler auf die Adresse des nächsten zu holenden Befehls erhöht. Wenn die Ausführungsphase eines Befehls vollzogen ist, enthält der Programmzähler die Adresse des nächsten Befehls und die nächste Holphase kann beginnen.

5.2.2. Lineare Befehlsfolge In der vorausgegangenen Untersuchung von Befehlsformaten wurde die Beispielaufgabe C ← [A] + [B ] genutzt. Abbildung 5.1 auf der nächsten Seite zeigt ein mögliches Programmsegment für diese Aufgabe, wie es im Hauptspeicher eines Rechners vorkommen kann. Wir haben hier 2-Adress-Befehle vorliegen. Die drei Befehle des Programms sind in aufeinanderfolgenden Speicherstellen hinterlegt, deren Adresswerte sich entsprechend der Reihenfolge, in der die Anweisungen durchgeführt werden, erhöhen (mit dem Startwert i ). Die CPU verfügt über ein Register als Programmzähler, das die Hauptspeicheradresse des als nächsten auszuführenden Befehl enthält. Um die Ausführung eines Programms

54

Technische Informatik I

5.2. Arithmetische und logische Operationen auf Daten

Abbildung 5.1.: Lineare Befehlsfolge zu beginnen, muss die Adresse des ersten Befehls (in unserem Beispiel i ) in den Programmzähler geladen werden. Diese Adressinformation wird genutzt, um den auszuführenden Befehl von der adressierten Hauptspeicherstelle in das Befehlsregister der CPU zu laden und dann auszuführen. Dabei wird immer nur eine Anweisung gleichzeitig bearbeitet, in Reihenfolge der aufsteigenden Adressen. Dies nennt man lineare Befehlsfolge. Bei der Ausführung jedes Befehls wird der Programmzähler inkrementiert, um auf den nächsten Befehl zu zeigen. Das bedeutet, dass nach der Ausführung der Anweisung i + 2 sich der Wert i + 3 im Programmzähler befindet.

5.2.3. Sprünge Man stelle sich die Aufgabe vor, eine Liste von n Zahlen zu addieren. Das Programm in Abbildung 5.2 ist eine Verallgemeinerung des Programms aus Abbildung 5.1. Die Adressen der Speicherstellen, die die n Zahlen beinhalten, werden symbolisch als NU M 1, NU M 2, . . . , NU Mn bezeichnet und es wird jeweils eine Additionsanweisung benutzt, um jede Zahl zu dem aktuellen Inhalt des Registers R 0 zu addieren. Nachdem alle Zahlen addiert wurden, wird das Ergebnis an die Speicherstelle SU M geschrieben. Anstatt eine lange Liste von Additionsanweisungen zu benutzen, kann man eine einzelne Additionsanweisung in eine Programmschleife setzen, wie in Abbildung 5.3 dargestellt. Die Schleife veranlasst die wiederholte Ausführung einer linearen Befehlsfolge, so oft wie nötig. Sie startet an der Stelle LOOP und endet am Befehl Branch > 0. Bei jedem Durchlauf der Schleife wird die Adresse des nächsten Listeneintrags bestimmt und dieser Eintrag aus dem Speicher gelesen und zu R 0 hinzuaddiert. Die Adressierung eines Operanden kann, wie wir gesehen haben, auf viele verschiedene Weisen erfolgen. Hier konzentrieren wir uns auf die Erzeugung und Steuerung einer Programmschleife. Nehmen wir an, dass die Anzahl n der Listeneinträge in einer Speicherstelle N hinterlegt ist. Das Register R 1 wird als Zähler benutzt, um zu bestimmen, wie oft die Schleife durchlaufen wird. Folglich wird der Inhalt von N zu Beginn des Programms in das Register R 1 geladen. Dann wird im Körper der Schleife der Inhalt von R 1 durch der Befehl Decrement R1 jedes mal um den Wert 1 verringert. Das bedeutet, die Ausführung der

Technische Informatik I

55

5. Programm- und Datenstrukturen

Abbildung 5.2.: Programm zur Addition von n Zahlen Schleife muss wiederholt werden, solange das Ergebnis der Dekrementierung größer als Null ist.

Abbildung 5.3.: Addition von n Zahlen in einer Schleife Wir führen nun das Konzept der Sprunganweisung (branch instruction) ein. Dabei handelt es sich um einen Befehl, der eine neue Befehlsadresse in den Programmzähler lädt. Als Folge davon holt die CPU den nun auszuführenden Befehl von dieser, jetzt aktuellen, Adresse und führt ihn aus. Ein bedingter Sprung-Befehl (conditional branch) verursacht ebenfalls einen Sprung, aber nur falls eine bestimmte Bedingung erfüllt wurde. Ist diese Bedingung nicht erfüllt, wird der Programmzähler wie gewohnt erhöht und der nächstfolgende Befehl ausgeführt. Im Programm aus Abbildung 5.3 verursacht der Befehl Branch > 0

56

LOOP

; Sprung wenn größer als 0

Technische Informatik I

5.2. Arithmetische und logische Operationen auf Daten einen Sprung an die symbolische Programmadresse LOOP. Also wird damit der erforderliche Rücksprung an den Schleifenbeginn durchgeführt. Der Sprung wird aber nur dann ausgeführt, wenn das Ergebnis des unmittelbar vorausgegangenen Befehls (Verringere R 1 um 1) größer als Null ist. Das bedeutet, dass die Schleife wiederholt wird, solange noch Einträge aus der Liste zu R 0 hinzuzuaddieren sind. Am Ende des n -ten Durchlaufs der Schleife erzeugt die Dekrementierung einen Wert von 0 und somit erfolgt kein Sprung. Stattdessen wird der Move-Befehl aus dem Speicher geladen und ausgeführt, der dann das Ergebnis aus R 0 in die Speicherstelle SU M schreibt.

5.2.4. Zustandscodes Der Prozessor merkt sich wichtige Informationen über die Ergebnisse von verschiedenen Operationen, um sie bei darauffolgenden bedingten Sprung-Befehlen zu verwenden. Dies wird erreicht, indem die benötigten Informationen in einzelnen Bits aufgezeichnet werden, den Zustandsbits. In einigen Prozessoren sind diese Bits in einem speziellen Register (dem Zustands- oder Statusregister) zusammengefasst. Andererseits kann ein Befehl auch bestimmen, dass die Zustandsbits in ein bestimmtes Mehrzweckregister des Rechners geschrieben werden sollen. In jedem Fall werden einzelne Zustandsbits auf 1 gesetzt oder auf 0 zurückgesetzt, je nach dem Ausgang der durchgeführten Operation. Vier häufig benutzte Zustandsbits sind (sog. Flags): N (Negative) Wird 1, wenn das Ergebnis negativ ist, sonst 0 Z (Zero) Wird 1, wenn das Ergebnis gleich Null ist, sonst 0 V (oVerflow) Wird 1, wenn ein arithmetischer Überlauf auftrat, sonst 0 C (Carry) Wird 1, wenn das Ergebnis einen Übertrag hat, sonst 0 Die Bits N und Z können automatisch von arithmetischen oder logischen Operationen gesetzt werden. Häufiger jedoch werden sie durch Anweisungen gesetzt, die explizit spezifizieren, dass die Bits verändert werden sollen. Der letztere Ansatz ist wichtig bei Pipeline-Prozessoren, die später noch behandelt werden. Die Bits N und Z können auch durch Anweisungen für Datentransfer (Kopieren, Laden, Speichern) verändert werden. Dies ermöglicht es, einen bedingten Sprung basierend auf dem Vorzeichen oder Wert eines Operanden durchzuführen, der bereits verändert wurde. Einige Rechner bieten spezielle Testanweisungen, die einen Wert in einem Register oder dem Hauptspeicher untersuchen und die Zustandsbits entsprechend setzen. Das Bit V gibt an, ob ein Überlauf stattgefunden hat. Solch ein Überlauf tritt dann auf, wenn das Ergebnis einer arithmetischen Operation außerhalb des durch die verfügbaren Bits darstellbaren Wertebereichs liegt. Der Prozessor setzt dann dieses Bit, sodass der Programmierer auf Überlauf testen und zu einer entsprechenden Behandlungsroutine springen kann. Befehle wie „Sprung bei Überlauf“ dienen diesem Zweck. Das Bit C wird auf 1 gesetzt, wenn ein Übertrag vom höchstwertigen Bit während einer arithmetischen Operation erzeugt wurde. Dies macht es möglich, dass arithmetische Operationen auf Operanden, die länger als die verwendete Wortlänge sind, ausgeführt werden können. Der Befehl Branch > 0 ist ein einfaches Beispiel für einen Sprungbefehl, der ein oder mehrere der Zustandsbits testet. Er verursacht einen Sprung, wenn der Wert weder negativ noch gleich Null ist. Also genau dann, wenn N + Z = 0 ist. Viele weitere bedingte Sprünge werden normalerweise durch den Befehlssatz angeboten, sodass auf eine Vielzahl von Bedingungen getestet werden kann.

Technische Informatik I

57

5. Programm- und Datenstrukturen Kehren wir zu Abbildung 5.3 zurück. Der Zweck des Anweisungsblocks bei LOOP ist die Addition einer Zahl aus der Liste bei jedem Durchlauf der Schleife. Folglich muss sich der Additionsbefehl bei jedem Durchlauf auf eine andere Adresse beziehen. Wie wird diese Adresse nun bestimmt? Bis jetzt haben wir lediglich Befehle betrachtet, die die Adressen der Operanden explizit im Befehl enthielten. Dieser einfache Adressierungsmechanismus bedeutet, dass wir für den Zugriff auf verschiedene Listeneinträge das Adressenfeld des Additionsbefehls den Schleifendurchläufen verändern müssen (z. B. durch Addition von 1). Jedoch zeugt die Veränderung eines Programms zur eigenen Laufzeit von sehr schlechter Programmierpraxis und führt zu vielen Problemen (insbesondere bei der Fehlersuche). Ein besserer Ansatz ist die Einführung einer Adressierungsart, die es einem Befehl erlaubt, verschiedene Operanden anzugeben, ohne die Kodierung des Befehls verändern zu müssen. Der nächste Abschnitt befasst sich mit diesen Möglichkeiten.

5.3. Adressierungsarten Der Begriff Adressierungsarten bezieht sich auf Möglichkeiten, einen Operanden in einem Befehl zu spezifizieren. Bisher haben wir nur zwei Adresstypen verwendet: Registeradressen und absolute Adressen. Definieren wir daraus die folgenden Adressierungsarten: • Registeradressen (register addressing mode) – Der Operand ist der Inhalt eines Registers der CPU, der Name des Registers wird im Befehl angegeben. • Absolute Adressen (absolute addressing mode) – Der Operand ist eine Speicherstelle, dessen Adresse wird explizit im Befehl angegeben. Als nächstes betrachten wir weitere grundlegende Adressierungsarten und bestimmen, wie sie im Programm aus Abbildung 5.3 verwendet werden können.

5.3.1. Adressierung eines direkten Werts Bei dieser Adressierungsart wird ein direkter Wert adressiert (immediate addressing mode); der Operand wird direkt im Befehl angegeben. Dieser Modus wird benutzt, um Adresskonstanten und Datenkonstanten direkt im Befehl anzugeben. Zum Beispiel platziert die Anweisung MOVE

200direkt ,R 0

den Wert 200 ins Register R 0 . Diese Adressierungsart wird benutzt, um den Wert eines Operanden anzugeben. Er macht keinen Sinn als Zieloperand, da er keine Stelle angibt, an die ein Operand gesichert werden könnte. Bei der folgenden Adressierungsart gibt der Befehl nicht den Operanden oder seine Adresse explizit an. Stattdessen wird die Information gegeben, aus der die Speicheradresse des Operanden abgeleitet werden kann. Wir bezeichnen diese Adresse als eigentliche Adresse (effective address) des Operanden.

5.3.2. Indirekte Adressierung Bei der indirekten Adressierung ist die eigentliche Adresse des Operanden der Inhalt eines Registers oder einer Speicherstelle, dessen Adresse im Befehl angegeben wird. Wir adressieren indirekt, indem wir im Befehl den Namen des Registers oder die Adresse

58

Technische Informatik I

5.3. Adressierungsarten in Klammern schreiben, wie in Abbildung 5.4 dargestellt. Um die Additionsanweisung im Teil a) der Abbildung auszuführen, beginnt die CPU mit dem Laden des Inhalts von Adresse A. Da indirekte Adressierung angegeben wurde, ist der Wert B an dieser Stelle, nicht der Operand, sondern dessen Adresse. Um an den Operanden zu kommen, stellt die CPU eine neue Leseanfrage an den Hauptspeicher, um dieses Mal die Inhalte der Adresse B auszulesen. Dieser Wert ist dann der gewünschte Operand, den die CPU zum Inhalt des Registers R 0 addiert. Ähnlich wird in Abbildung 5.4b indirekt durch das Register R 1 auf den Operanden zugegriffen.

Abbildung 5.4.: Indirekte Adressierung Das Register oder die Adresse, welche(s) die Adresse des Operanden enthält, wird Zeiger genannt. Indirekte Adressierung ist ein wichtiges und mächtiges Konzept der Programmierung. Man stelle sich als Analogie eine Schatzsuche vor: In den Anweisungen für die Suche wird man aufgefordert, ein Haus an einer bestimmten Adresse aufzusuchen. Statt dort direkt den Schatz zu finden, stößt man lediglich auf eine Notiz, die einem eine weitere Adresse nennt, wo man den Schatz finden wird. In dem man die Notiz verändert, ändert man auch die (vermeintliche) Position des Schatzes, aber die Anweisungen für die Suche bleiben die gleichen. Die Notiz zu verändern entspricht der Änderung eines Zeigers in einem Rechnerprogramm. Indem wir zum Beispiel den Inhalt der Adresse A in Abbildung 5.4 verändern (oder des Registers R 1 ), würde der gleiche Additionsbefehl einen anderen Operanden zum Register R 0 hinzuaddieren. Kehren wir nun zum Programm zur Addition einer Liste von Zahlen aus Abbildung 5.3 zurück. Indirekte Adressierung kann verwendet werden, um auf aufeinanderfolgende Zahlen der Liste zurückzugreifen, wodurch das folgende Programm entsteht:

Abbildung 5.5.: Addition der Liste mithilfe von indirekter Adressierung Das Register R 2 wird als Zeiger auf die Zahlen der Liste verwendet, soll heißen, auf die Operanden wird indirekt über R 2 zugegriffen. Der Initialisierungsteil des Programms kopiert den Zählerwert n von Speicherstelle N ins Register R 1 und nutzt die Adressierung

Technische Informatik I

59

5. Programm- und Datenstrukturen mit direktem Wert, um die Adresse der ersten Zahl der Liste (NUM1) in das Register R 2 zu schreiben. Dann setzt er R 0 auf 0 zurück. Die ersten beiden Befehle der Schleife in Abbildung 5.5 implementieren den unbestimmten Befehlsblock von LOOP aus Abbildung 5.3. Beim ersten Durchlauf der Schleife in Abbildung 5.5 holt der Additionsbefehl den Operanden an der Stelle NUM1 in die CPU und addiert ihn zu R 0 . Die Inkrement-Anweisung addiert dann 1 zum Inhalt des Zeigers R 2 , sodass er die Adresse von NUM2 enthält, wenn der Additionsbefehl im zweiten Durchlauf der Schleife ausgeführt wird.

5.3.3. Indexadressierung Die Indexadressierung ermöglicht eine weitere Möglichkeit des Zugriffs auf Operanden. Die eigentliche Adresse des Operanden wird erzeugt, indem man einen konstanten Wert zum Inhalt eines Registers addiert. Das benutzte Register kann entweder ein für diesen Zweck vorhandenes Spezialregister oder (was weit häufiger der Fall ist) eins aus einer Menge von Allgemeinregistern in der CPU sein. Es wird dann als Indexregister bezeichnet. Wir geben ein Indexregister symbolisch als X (R)

an, wobei X eine Konstante und R der Name des verwendeten Registers ist. Die eigentliche Adresse des Operanden ergibt sich aus A eff = X + [R]

Beim Generieren der eigentlichen Adresse verändern sich die Inhalte des Indexregisters nicht. In einem Assemblerprogramm kann die Konstante X explizit als Zahl angegeben werden. Abbildung 5.6 illustriert zwei Möglichkeiten für die Verwendung der Indexadressierung. Im Teil a) enthält das Indexregister R 1 die Adresse einer Speicherstelle und der Wert X definiert einen Versatz (offset) von dieser Adresse zu der Stelle, an welcher der Operand gefunden werden kann. Eine alternative Verwendung wird in Teil b) der Abbildung dargestellt. Hier gehört die Konstante X zu einer Speicheradresse und die Inhalte des Indexregisters bilden den Offset zum Operanden. In jedem Fall ist die errechnete Adresse die Summe der beiden Werte. Einer davon ist explizit angegeben und der andere ist in einem Register gespeichert. Um den Nutzen der Indexadressierung zu erkennen, denke man sich ein einfaches Beispiel, welches eine Liste von Klausurergebnissen von Studenten umfasst, die an einer Lehrveranstaltung teilgenommen haben. Nehmen wir an, dass die Liste wie in Abbildung 5.7 strukturiert ist. Ein Tupel aus vier Worten wird zur Speicherung der relevanten Daten für jeden Studenten verwendet. Die vier Worte bestehen aus der Matrikelnummer des Studenten, gefolgt von den Punkten, die der Student in drei Klausuren erreicht hat. Die Teilnehmergruppe umfasst n Studenten, wobei der Wert von n am Anfang der Liste steht. Nehmen wir an, dass wir die Summe der Ergebnisse der zweiten und die Summe der Ergebnisse der dritten Klausur berechnen wollen. Ein mögliches Programm für die Aufgabe zeigt Abbildung 5.8. Das Programm nutzt die Indexadressierung aus Abbildung 5.6a), um auf die Ergebnisse jedes einzelnen Studenten zuzugreifen. Das Register R 0 wird als Indexregister benutzt und anfangs so gesetzt, dass es auf die Matrikelnummer des ersten Studenteneintrags

60

Technische Informatik I

5.3. Adressierungsarten

Abbildung 5.6.: Zwei Möglichkeiten zur Verwendung der Indexadressierung zeigt. Der Hauptteil des Programms besteht aus einer Schleife, die sukzessiv auf die Studenteneinträge zugreift, indem sie am Ende jeder Iteration 4 zu R 0 addiert. Register R 1 dient als Schleifenzähler. Es wird mit der Anzahl der Studenten initialisiert. Die Register R 2 und R 3 dienen als Akkumulatoren für die Summen der Ergebnisse der beiden Klausuren. Die Indexadressierung bietet einen einfachen Weg, auf das dritte und vierte Wort jedes Studenteneintrags zuzugreifen. Erneut soll hervorgehoben werden, dass der Inhalt des Indexregisters R 0 sich nicht ändert, wenn auf diese Worte zugegriffen wird. In diesem Programm werden die Inhalte von R 0 nur durch die letzte Additionsanweisung in der Schleife verändert, um den Zeiger von einem Studenteneintrag zum nächsten zu bewegen. Nach dem letzten Durchlauf der Schleife werden die resultierenden Summen in den Speicherpositionen SUM2 und SUM3 hinterlegt. Im Allgemeinen verschafft die Indexadressierung Zugriff auf einen Operanden, dessen Position relativ zu einem Referenzpunkt (in diesem Fall die Matrikelnummer) innerhalb der Datenstruktur definiert ist, in welcher sich der Operand befindet. Jede der vier Worte langen Studenteneinträge aus Abbildung 5.7 ist ein Beispiel für eine solche Datenstruktur. Die Position der in der Klausur erreichten Punkte in jeder Klausur wird durch die Distanz zum Beginn des Eintrags bestimmt, welcher die Matrikelnummer ist.

5.3.4. Indexadressierung mit Verschiebung Wir haben die wichtigsten Formen der Indexadressierung eingeführt. Es existieren mehrere mögliche Variationen dieser Adressierungsart. Zum Beispiel kann der Inhalt eines zweiten Registers als Indexkonstante X verwendet werden. In diesem Fall könnten wir die Indexadressierung als (R i , R j )

Technische Informatik I

61

5. Programm- und Datenstrukturen

Abbildung 5.7.: Beispiel zur Indexadressierung: Liste mit Klausurergebnissen

Abbildung 5.8.: Beispiel zur Indexadressierung: Programm zur Berechnung von Klausurergebnissen

schreiben. Die errechnete Adresse ist die Summe der Inhalte der beiden Register R i und R j . Diese Form der Indexadressierung bietet eine höhere Flexibilität beim Zugriff auf die Operanden, da beide Komponenten der effektiven Adresse geändert werden können. Eine weitere Version der Indexadressierung benutzt zwei Register plus eine Konstante, in der Form X (R i , R j )

Sie wird auch als Indexadressierung mit Verschiebung (index addressing with displacement) bezeichnet. In diesem Fall ist die eigentliche Adresse die Summe der Konstanten X und der Inhalte der Register R i und R j . Der 68000 und der PowerPC unterstützen diese Methode, bei der es immer das Ziel ist, die Adressierung so flexibel wie möglich zu gestalten und dadurch die Programmierung zu unterstützen. Bisher haben wir die fünf grundlegenden Adressierungsarten behandelt, die in den meisten Rechnern vorkommen. Obwohl diese Arten für die allgemeine Berechnung durchaus genügen, bieten viele Rechner zusätzliche Arten an, die bei bestimmten Programmieraufgaben hilfreich sind. Die zwei Arten, die im Folgenden beschrieben werden, sind sehr nützlich für den Zugriff auf aufeinanderfolgende Datenelemente im Speicher.

62

Technische Informatik I

5.4. Assemblersprache

5.3.5. Autoinkrement-Adressierung (postincrement) Bei dieser Adressierungsart ist die eigentliche Adresse des Operanden der Inhalt eines Registers, das im Befehl angegeben ist. Nachdem auf den Operanden zugegriffen wurde, wird der Inhalt des Registers um eins erhöht. Wir symbolisieren diese Adressierungsart, in dem wir das angegebene Register in Klammern schreiben, um zu zeigen, dass der Inhalt des Registers zur Adressierung genutzt wird, gefolgt von einem Pluszeichen, um anzugeben, dass die Inhalte des Registers nach dem Zugriff auf den Operanden erhöht werden. So wird z. B. bei Verwendung des Registers R 4 der folgende Ausdruck geschrieben: (R 4 )+

Die Autoinkrement-Adressierung macht es möglich, auf die Inkrement-Anweisung in Abbildung 5.5 zu verzichten. Die gewünschte Operation kann mit der vorausgehenden Addition kombiniert werden und das Programm kann wie in Abbildung 5.9 neu geschrieben werden.

Abbildung 5.9.: Listenaddition mit Autoinkrement und indirekter Adressierung

5.3.6. Autodekrement-Adressierung (predecrement) Bei der Autodekrement-Adressierung wird der Inhalt des im Befehl angegebenen Registers dekrementiert. Dieser Inhalt ist dann die eigentliche Adresse des Operanden. Um mit einem Autodekrement zu adressieren, wird das Register in Klammern geschrieben und ein Minus-Zeichen vorangestellt. Dieses Zeichen bedeutet, dass der Inhalt des Registers zunächst dekrementiert und dann zur Adressierung genutzt wird. Bei Verwendung von Register R 4 sieht der Ausdruck folgendermaßen aus: −(R 4 )

Um das Lesen zu erleichtern, wurden die Befehle in diesem Abschnitt rechnerunabhängig beschrieben. Beim Schreiben realer Programme ist es jedoch notwendig, bestimmte symbolische Repräsentationen für Befehle und Adressierungsarten zu verwenden und sich streng an die Regeln der Assemblersprache des Rechners zu halten. Wir werden uns nun einen allgemeinen Überblick über Assemblersprache im nächsten Abschnitt verschaffen.

5.4. Assemblersprache Maschinenbefehle werden durch Muster aus Nullen und Einsen dargestellt. Solche Muster sind unbequem in der sprachlichen Kommunikation oder beim Schreiben von Programmen. Man verwendet stattdessen Symbole, um diese Muster darzustellen. Beispielsweise verwenden wir die Symbole MOVE und ADD zur Darstellung der Bitmuster für

Technische Informatik I

63

5. Programm- und Datenstrukturen Kopier- und Additionsbefehle. Aus derselben Motivation heraus verwenden wir die Notation R 3 , wenn wir uns auf das dritte Register beziehen. Die vollständige Menge solcher symbolischen Namen und Regeln für ihre Verwendung zur Schaffung einer Programmiersprache werden allgemein als Assemblersprache bezeichnet. Die symbolischen Namen heißen mnemonische Symbole, die Menge der Regeln zur Angabe von vollständigen Anweisungen und Programmen nennt man Syntax der Sprache. Programme, die in Assemblersprache geschrieben wurden, können durch einen Assembler automatisch in eine Folge von Maschinenanweisungen übersetzt werden. Der Assembler selbst liegt, wie jedes andere Programm, als eine Folge von Maschinenanweisungen im Hauptspeicher des Rechners vor. Ein Benutzerprogramm wird normalerweise durch ein Terminal in den Rechner eingegeben, wo es entweder in den Hauptspeicher oder auf einen Magnetspeicher gespeichert wird. Zu diesem Zeitpunkt ist das Programm nichts anderes als eine Menge von Zeilen voller alphanumerischer Zeichen. Wenn der Assembler ausgeführt wird, liest und analysiert er das Benutzerprogramm und erzeugt daraus das gewünschte Maschinenprogramm. Das beinhaltet Folgen von Nullen und Einsen, die Maschinenbefehle darstellen und durch den Rechner ausgeführt werden können. Das Assemblerprogramm in der ursprünglichen alphanumerischen Form wird Quellprogramm genannt und das übersetzte Assemblersprachenprogramm bezeichnet man als Objektprogramm. Wir werden uns später noch damit auseinandersetzen, wie der Assembler arbeitet. Zuerst sollen einige Aspekte der Assemblersprache betrachtet werden. Die Syntax der Assemblersprache könnte es erfordern, dass wir die Kopieranweisung folgendermaßen schreiben: MOVE

R 0 ,SUM

Das mnemonische Symbol MOVE repräsentiert die Operation „Kopiere“, die mit dem Befehl ausgelöst werden soll. Der Assembler übersetzt dieses Symbol in einen Binärcode, den der Rechner versteht. Dieser Binärcode wird als Befehlscode bezeichnet, da er die durch das mnemonische Symbol bezeichnete Operation angibt. Beim Editieren einer Programmzeile folgt dem Symbol des Befehlscodes mindestens ein Leerzeichen (und/oder ein Tabulatorensprung). Anschließend werden Information über die Operanden gegeben. In unserem Beispiel ist der Quelloperand im Register R 0 . Dieser Information folgt die Angabe des Zieloperanden, der durch ein Komma vom Quelloperanden getrennt wird. Der Zieloperand ist die Speicherstelle, deren Adresse durch den Namen SUM dargestellt wird. Da es viele mögliche Adressierungsarten zur Angabe von Operandenpositionen gibt, muss die Syntax der Assemblersprache verdeutlichen, welche Adressierungsart verwendet werden soll. Dies kann zum Beispiel durch einen numerischen Wert oder einen Namen für die Angabe einer absoluten Adressierung geschehen, wie es bei SUM im obigen Beispiel der Fall ist. Das Rautezeichen (#) wird üblicherweise zur Angabe eines direkten Wertes benutzt. So wird durch den Befehl ADD

#5,R 3

der Zahlenwert 5 zum Inhalt des Registers R 3 addiert und das Ergebnis wieder in R 3 hinterlegt. Das Rautezeichen ist nicht der einzige Weg, einen direkten Wert anzugeben. In manchen Fällen wird die gewünschte Adressierungsart durch den Befehlscode deutlich. Die Assemblersprache kann verschiedene Anweisungscodes für verschiedene Adressierungsarten beinhalten. Zum Beispiel kann der Additionsbefehl auch wie folgt geschrieben werden:

64

Technische Informatik I

5.4. Assemblersprache ADDI

#5,R 3

Das mnemonische Symbol ADDI legt fest, dass der Quelloperand direkt (immediate) angegeben wird. Die indirekte Adressierung wird normalerweise durch Klammern um den Namen oder das Symbol, welches den Zeiger auf den Operanden bestimmt, angegeben. Soll zum Beispiel der Zahlenwert 5 in einer Speicherstelle hinterlegt werden, deren Adresse im Register R 2 angegeben ist, kann die Anweisung so aussehen: MOVE #5,(R 2 ) oder MOVEI

5,(R 2 )

5.4.1. Assemblerdirektiven Um einen zusätzlichen Mechanismus für die Darstellung von Befehlen in einem Programm zu bieten, erlaubt die Assemblersprache dem Programmierer, weitere Informationen anzugeben, die für die Übersetzung des Quellprogramms in das Objektprogramm nötig sind. Wie bereits erwähnt, müssen allen Namen, die im Programm benutzt werden, numerische Werte zugewiesen werden. Nehmen wir an, dass der Name SUM benutzt wird, um den Wert 200 zu repräsentieren. Dies kann dem Assemblerprogramm durch einen Ausdruck der folgenden Form bekannt gemacht werden. SUM EQU 200 Dies ist kein Befehl, der während der Laufzeit des Objektprogramms ausgeführt wird, sondern wird in der Tat nicht einmal im Objektprogramm erscheinen. Es handelt sich lediglich um eine Information, die dem Assembler mitteilt, dass der Name SUM durch den Wert 200 ersetzt werden soll, wann immer er im Programm auftritt (EQU steht für equals). Solche Ausdrücke, sogenannte Assemblerdirektiven, werden vom Assembler benutzt, wenn er ein Quellprogramm übersetzt. Um die Verwendung von Assemblersprache weiter zu verdeutlichen, erinnern wir uns an das Programm aus Abbildung 5.5. Um dieses Programm in einem Rechner laufen zu lassen, muss sein Quellcode in der benötigten Assemblersprache geschrieben werden, wobei alle notwendigen Informationen zur Enderzeugung des entsprechenden Objektprogramms mit angegeben werden müssen. Nehmen wir an, dass jeder Befehl und jedes Datum ein Speicherwort belegt (dies ist eine sehr vereinfachte Annahme, aber dadurch bleibt das Beispiel überschaubar). Nehmen wir ebenso an, dass das Objektprogramm wie in Abbildung 5.10 dargestellt in den Hauptspeicher geladen wird. Die Abbildung zeigt die Speicheradressen auf, an denen sich die Maschinenanweisungen und die benötigten Daten befinden, nachdem das Objektprogramm in den Hauptspeicher geladen wurde. Wenn der Assembler ein Objektprogramm dementsprechend erzeugen soll, muss er wissen • wie die Namen interpretiert werden, • wo das Objektprogramm im Speicher abgelegt wird und • wie viele Speicherstellen zur Speicherung der Daten verwendet werden. Um diese Anforderungen zu erfüllen, kann das Quellprogramm wie in Abbildung 5.11 geschrieben werden. Das Programm beginnt mit den Assemblerdirektiven. Den EQUBefehl haben wir bereits besprochen. Der zweite Befehl (ORIGIN) sagt dem Assembler,

Technische Informatik I

65

5. Programm- und Datenstrukturen

Abbildung 5.10.: Programm zur Hauptspeicher

indirekten

Adressierung

(Abbildung

5.5)

im

an welcher Stelle er den folgenden Datenblock in den Hauptspeicher legen soll. In diesem Fall ist dies die Adresse 201. Da diese Stelle mit dem Wert 300 geladen werden soll (das ist die Anzahl der Listeneinträge), wird ein DATA-Befehl benutzt, um den Assembler dazu zu veranlassen. Dadurch wird der Wert 300 an Adresse 201 in den Speicher geschrieben. Diese Adresse ist auch durch den Namen N bekannt. Jeder Ausdruck, der einen Befehl oder einen Datenwert für eine Speicherstelle enthält, kann mit einem Bezeichner versehen werden. Dem Bezeichner wird der Wert der Adresse dieser Speicherstelle zugewiesen. Da die DATA-Anweisung den Bezeichner N bekommen hat, wird dem Namen N der Wert 201 zugewiesen. Wann immer N im Rest des Programms auftritt, wird er durch diesen Wert ersetzt. Der nächste Befehl verursacht, dass die nächste Adresse (202) als NUM1 bekannt wird, und dass 300 nachfolgende Worte ab dieser Adresse zur Datenspeicherung reserviert werden. Dieser Befehl lädt keine Daten in die Speicherstellen. Dies kann durch eine Eingabeprozedur erfolgen, wie später beschrieben werden wird. Der zweite ORIGIN-Befehl gibt an, dass die Befehle im Objektprogramm ab Adresse 100 in den Speicher geladen werden sollen. Die Befehle des Quellprogramms werden in Assemblersprache geschrieben, wobei die entsprechenden mnemonischen Symbole und die Syntax verwendet werden. Der letzte Befehl im Quellprogramm ist der Assemblerbefehl END, welcher dem Assembler das Ende des Quellprogramms kennzeichnet. Der ENDBefehl beinhaltet den Bezeichner START, welcher die Adresse angibt, an der die Ausführung des Programms begonnen werden soll. Dies ist für den Lader eines Programms wichtig, was uns aber im Moment noch nicht interessieren soll. Wir haben alle Befehle aus Abbildung 5.11 erklärt, mit Ausnahme von RETURN. Dies ist ein Assemblerbefehl, der angibt, an welchem Punkt die Ausführung des Programms beendet werden soll. Hier wird durch den Assembler eine besondere Maschinenanweisung eingeführt, welche die Kontrolle an das Betriebssystem des Rechners zurückgibt.

66

Technische Informatik I

5.5. Assemblierung und Ausführung von Programmen Sprungmarke SUM N NUM1 START

LOOP

Operation EQU ORIGIN DATA RESERVE ORIGIN MOVE MOVE CLR ADD INC DEC BGTZ MOVE RETURN END

Operanden 200 201 300 300 100 N,R1 #NUM1,R2 R0 (R2),R0 R2 R1 LOOP R0,SUM START

Abbildung 5.11.: Assembler-Code zu Abbildung 5.5 Die meisten Assemblersprachen fordern, dass Ausdrücke in einem Quellprogramm in dieser Form geschrieben werden: Bezeichner

Befehl

Operand(en)

Kommentar

Diese vier Felder werden durch entsprechende Begrenzerzeichen von einander getrennt, normalerweise sind dies ein oder mehrere Leerzeichen. Der Bezeichner ist ein Name, der mit der Adresse der Maschinenanweisung, die aus dem Ausdruck geschaffen wurde, verknüpft wird. Bezeichner können auch mit Adressen von Daten verknüpft werden. In Abbildung 5.11 gibt es fünf Bezeichner: SUM, N, NUM1, START und LOOP. Das Befehlsfeld beinhaltet das mnemonische Symbol für den gewünschten Befehl oder die Assemblerdirektive. Das Operandenfeld enthält einen oder mehrer Operanden, abhängig von der Art des Befehls. Das Kommentarfeld wird von Assemblerprogrammen ignoriert. Es dient zu Dokumentationszwecken die das Lesen des Programms für den Programmierer vereinfachen sollen. Die Grundlagen der Assemblersprache, die bis hierhin vorgestellt wurden, unterscheiden sich in Details von einer Rechnerarchitektur zur anderen.

5.5. Assemblierung und Ausführung von Programmen Ein Quellprogramm, das in Assemblersprache vorliegt, muss in ein Objektprogramm im Maschinencode assembliert werden, bevor es ausgeführt werden kann. Dies geschieht durch den Assembler, der alle Namen und Symbole durch die numerischen Werte, die sie repräsentieren, ersetzt. Symbole, die Befehle und Adressierungsarten beschreiben, werden durch die den Maschinenanweisungen entsprechenden Binärcodes ersetzt. Der Assembler weist Befehlen und Datenblöcken Adressen zu, die mit der durch ORIGIN angegebenen Adresse beginnen. Der Assembler fügt ebenso Konstanten ein (DATA) und reserviert Speicherplatz (RESERVE).

Technische Informatik I

67

5. Programm- und Datenstrukturen Eine wichtige Funktion des Assemblers dient dazu, die Werte für die Namen zu bestimmen. In den Fällen, in denen der Wert eines Namens durch EQU festgelegt wurde, ist dies eine einfache Aufgabe. In anderen Fällen, in denen ein Name als Bezeichner für einen Befehl verwendet wird, muss der Wert für diesen Namen durch die Position des Befehls im assemblierten Programm bestimmt werden. In Abbildung 5.10 repräsentieren die Namen N und NUM1 zum Beispiel die Werte 201 und 202, welche die Speicheradressen für die Gesamtanzahl der Daten und das erste Datum enthalten. Der Wert 201 für N wird durch den ORIGIN-Befehl aus Abbildung 5.11 bestimmt. Da der Assembler aufeinanderfolgenden Ausdrücken im Quellprogramm auch aufeinanderfolgende Adressen zuweist, wird NUM1 mit der Adresse 201 verknüpft. In einigen Fällen ersetzt der Assembler den Namen nicht direkt durch die eigentliche Adresse. Bei einer Sprunganweisung zum Beispiel wird der Name, der das Ziel des Sprungs bestimmt, nicht durch die eigentliche Adresse ersetzt. Wie wir später noch sehen werden, wird eine Sprunganweisung üblicherweise so im Maschinencode implementiert, dass die Distanz zwischen der aktuellen Anweisung und dem Sprungziel angegeben wird. Der Assembler berechnet diese Distanz (Sprungversatz oder Sprung-Offset genannt) und setzt sie in den Maschinenbefehl ein. Wenn der Assembler ein Quellprogramm übersetzt, merkt er sich alle symbolischen Namen und die dazugehörigen Zahlenwerte in einer Symboltabelle. Wenn ein solcher Name ein zweites Mal auftritt, wird er durch den Wert aus der Tabelle ersetzt. Ein Problem tritt auf, falls ein Name als Operand verwendet wird, bevor ihm ein Wert zugewiesen wird. Dies tritt zum Beispiel auf, wenn ein Sprung nach vorne notwendig ist. Der Assembler ist nicht in der Lage, das Sprungziel zu bestimmen, da der Name bisher noch nicht in der Symboltabelle aufgezeichnet wurde. Eine einfache Lösung besteht darin, denn Assembler zweimal durch das Quellprogramm laufen zu lassen. Im ersten Durchlauf erzeugt er die vollständige Symboltabelle. Am Ende des Durchlaufs wurden allen symbolischen Namen Zahlenwerte zugewiesen. Der Assembler läuft dann ein zweites Mal durch das Quellprogramm und ersetzt alle symbolischen Namen durch die Werte aus der Symboltabelle. Solch einen Assembler nennt man Zwei-Schritt-Assembler (Two-Pass-Assembler). Der Assembler speichert – wie die meisten Anwendungsprogramme, die zur Systemumgebung gehören – das Objektprogramm auf einem Speichermedium. Zur Ausführung muss das Objektprogramm in den Hauptspeicher des Rechners geladen werden. Damit dies geschehen kann, muss sich ein anderes Programm, Lader (Loader) genannt, bereits im Hauptspeicher befinden. Der Lader führt eine Folge von Eingabeoperationen aus, die nötig sind, um das Maschinensprachprogramm vom Magnetplattenspeicher an eine bestimmte Stelle in den Hauptspeicher zu laden. Der Lader muss die Länge des Programms und die Zieladresse im Hauptspeicher kennen. Diese Informationen werden durch den Assembler in den Kopf des Objektprogramms geschrieben. Nach dem der Objektcode geladen wurde, startet der Lader die Ausführung des Objektprogramms, in dem er zum ersten Befehl des Programms springt. Man erinnere sich, dass die Adresse dieses Befehls im Assemblersprachprogramm als Operand des END-Befehls eingefügt wurde. Der Assembler hinterlegt diese Adresse im Header, der dem Objektcode auf dem Speichermedium vorausgeht. Wenn das Objektprogramm ausgeführt wird, arbeitet es bis zur Fertigstellung, solange keine logischen Fehler im Programm auftreten. Der Benutzer soll in der Lage sein, logische Fehler schnell zu finden. Der Assembler allein kann nur Syntaxfehler erkennen und melden. Um dem Nutzer bei der Suche nach anderen Programmierfehlern zu helfen, enthält die Systemsoftware normalerweise ein Debugger-Programm.

68

Technische Informatik I

5.6. Zahlennotation Dieser Debugger ermöglicht dem Nutzer die Ausführung des Objektprogramms an bestimmten Stellen zu unterbrechen, die von Interesse sind. Die Inhalte von verschiedenen Prozessorregistern und Speicherstellen können dann untersuchet werden.

5.6. Zahlennotation Wenn man mit Zahlenwerten arbeitet, ist es oft bequemer, die vertraute Dezimaldarstellung zu verwenden. Natürlich sind diese Werte im Rechner als Binärzahlen gespeichert. In einigen Situationen ist es angenehmer, die Binärmuster direkt einzugeben. Die meisten Assembler erlauben die Angabe von numerischen Werten auf verschiedenen Wegen. Dabei werden Regeln der Assemblersyntax verwendet. Man stelle sich zum Beispiel die Zahl 93 vor, die durch die 8-Bit-Binärziffer 01011101 dargestellt werden kann. Wenn dieser Wert als direkter Operandenwert benutzt werden soll, kann er als Dezimalzahl angegeben werden, wie es im Befehl ADD

#93,R1

der Fall ist. Soll er in binärer Darstellung angegeben werden, muss der Zahl ein Prozentzeichen vorangestellt werden: ADD

#%01011101,R1

Hexadezimalzahlen (kurz Hexzahlen) können kompakter als Binärzahlen geschrieben werden, bei der vier Bit durch eine einfach Hexziffer dargestellt werden. Hier bleibt nur zu bemerken, dass eine solche Hexzahl in der Assemblersyntax durch ein Dollarzeichen identifiziert wird, sodass man folgendes schreiben würde: ADD

#$51,R1

5.7. Grundlegende Operation für Ein- und Ausgabe In den bisherigen Abschnitten dieses Kapitels haben wir die grundlegenden Ideen für die Entwicklung von Assemblersprachprogrammen erläutert. Wir haben angenommen, dass die Daten, auf denen diese Programme arbeiten, sich bereits im Hauptspeicher befinden. Wir werden nun untersuchen, wie die Daten zwischen dem Hauptspeicher des Rechners und der Außenwelt übertragen werden. Ein- und Ausgabeoperationen sind essentiell und die Art, wie sie ausgeführt werden, hat direkte Auswirkungen auf die Leistung des Rechners. Man stelle sich die Aufgabe vor, die es erfordert, dass Zeichen von der Tastatur eines Terminals gelesen werden und Zeichen auf dem Monitor desselben Terminals dargestellt werden. Eine einfache Art, solche Ein-/Ausgabeaufgaben zu erledigen, ist eine programmgesteuerte Ein-/Ausgabe. Das Terminal (bestehend aus einer Tastatur und einem Display) ist ein einfaches E/A-Gerät, das wir für die Veranschaulichung der Konzepte von programmgesteuerter E/A benutzen können. Die Datentransferrate von der Tastatur zum Rechner ist durch die Geschwindigkeit des Benutzers begrenzt, der kaum mehr als einige Zeichen pro Sekunde schaffen wird. Die Rate der Ausgabeübertragungen vom Rechner zum Display ist viel höher und wird durch die Geschwindigkeit der Verbindung begrenzt (sprich: wie viele Zeichen können pro Sekunde übertragen werden). Dies ist aber immer noch viel langsamer als die

Technische Informatik I

69

5. Programm- und Datenstrukturen Geschwindigkeit eines Prozessors, der Millionen von Befehlen pro Sekunde ausführen kann. Der Unterschied zwischen dem Prozessor und dem Terminal schafft die Notwendigkeit einer Synchronisation der Operationen der beiden Komponenten. Eine Lösung dieses Problems könnte wie folgt aussehen: Bei der Ausgabe sendet die CPU ein Zeichen und wartet dann auf ein Signal des Terminals, dass das Zeichen dargestellt wurde. Es sendet dann ein zweites Zeichen und so weiter. Die Eingabe wird von der Tastatur auf ähnliche Weise gesendet. Die CPU wartet auf ein Signal, dass eine Taste gedrückt wurde und dass somit der Zeichencode an einer bestimmten Stelle verfügbar ist. Anschließend macht sich die CPU auf den Weg, diesen Code auszulesen. Die Tastatur und das Display sind getrennte Geräte, die zusammen das Terminal bilden. Die Aktion eines Tastendrucks verursacht nicht automatisch die Darstellung des dazugehörigen Zeichens auf dem Display. In Wirklichkeit überträgt ein Block von Anweisungen im E/A-Programm ein Zeichen in die CPU und ein anderer Anweisungsblock stellt das Zeichen auf dem Display dar. Dieser Prozess des Sendens eines empfangenen Zeichen an das Display (Echo genannt) erlaubt eine Art der Fehlererkennung. Wenn der Benutzer nicht das korrekte Zeichen auf dem Bildschirm sieht, ist es sofort offensichtlich, dass während des Vorgangs ein Fehler aufgetreten ist. Stellen wir uns nun vor, einen Zeichencode von der Tastatur zur CPU zu übertragen. Ein Tastendruck hinterlegt den entsprechenden Zeichencode in einem 8-Bit-Pufferregister, das mit der Tastatur verbunden ist. Nennen wir dieses Register DATAIN. Um die CPU zu informieren, dass ein gültiges Zeichen in DATAIN vorliegt, muss ein Synchronisationsbit (SIN) auf 1 gesetzt werden. Ein Programm überwacht SIN (aus diesem Grunde spricht man von programmgesteuerter E/A) und liest bei SIN=1 den Inhalt von DATAIN. Wenn das Zeichen zur CPU übertragen wird, wird SIN automatisch wieder auf 0 zurückgesetzt. Wenn ein zweites Zeichen an der Tastatur eingegeben wird, wird SIN wieder auf 1 gesetzt und der Vorgang wiederholt sich. Ein analoger Vorgang findet statt, wenn Zeichen von der CPU zum Display übertragen werden. Ein Pufferregister (DATAOUT) und ein Synchronisationsbit (SOUT) werden für diese Übertragung verwendet. Wenn SOUT gleich 1 ist, ist das Display bereit, ein Zeichen zu empfangen. Die CPU überwacht mittels Software SOUT und wenn es gleich 1 ist, sendet sie einen Zeichencode zu DATAOUT. Der Transfer eines Zeichens nach DATAOUT setzt SOUT auf 0 zurück. Wenn das Zeichen dargestellt wurde und das Display bereit ist, ein zweites Zeichen zu empfangen, wird SOUT wieder auf 1 gesetzt. Die Pufferregister DATAIN und DATAOUT sowie die Synchronisationsbits SIN und SOUT sind Teil einer Schaltung, die unter dem Begriff Geräteschnittstelle bekannt ist, welche das Terminal mit dem Rechner verbindet. Die Schaltung ist über einen Bus mit der CPU verbunden, wie in Abbildung 5.12 dargestellt. Um solche E/A-Übertragungen durchzuführen, sind Maschinenanweisungen nötig, die den Zustand der Steuerbits überwachen und die Daten zwischen CPU und E/A-Gerät übertragen. Diese Anweisungen ähneln vom Format her denen, die Daten zwischen CPU und Hauptspeicher übertragen. Zum Beispiel kann die CPU das Tastatursteuerbit SIN überwachen und ein Zeichen von DATAIN in das Register R 1 übertragen, wenn sie die folgenden Anweisungen ausführt: READWAIT

70

Sprung zu READWAIT, wenn SI N = 0 Eingabe von DATAIN zu R 1

Technische Informatik I

5.7. Grundlegende Operation für Ein- und Ausgabe

BUS CPU

DATAIN SIN Tastatur

DATAOUT SOUT Display

Terminal

Abbildung 5.12.: Kommunikationsarchitektur eines Terminals Die Sprunganweisung wird normalerweise durch zwei Maschinenbefehle implementiert. Die erste testet das Steuerbit und die zweite führt den Sprung aus. Obwohl sich die Details von Rechner zu Rechner unterscheiden können, ist die Hauptidee, dass die CPU das Kontrollbit überwacht, indem sie eine kurze Warteschleife durchläuft und zur Übertragung des Zeichens voranschreitet, wenn SIN nach einem Tastendruck auf 1 gesetzt wurde. Zusätzlich zur Übertragung eines Zeichens von DATAIN zu R 1 setzt die Eingabeanweisung SIN wieder auf 0 zurück. Eine analoge Folge von Operationen wird zur Übertragung von Ausgabe zum Display benutzt. Ein Beispiel ist WRITEWAIT

Sprung zu WRITEWAIT, wenn SOU T = 0 Ausgabe von R 1 zu DATAOUT

Wieder wird der Sprung im Normalfall durch zwei Maschinenbefehlen implementiert. Die Warteschleife wird wiederholt ausgeführt, bis das Steuerbit SOUT durch das Terminal auf 1 gesetzt wird, wenn dieses bereit ist, ein Zeichen anzuzeigen. Die Ausgabeoperation überträgt ein Zeichen von R 1 nach DATAOUT und setzt SOUT auf 0 zurück. Wir nehmen an, dass der Anfangszustand von SIN 0 ist und von SOUT 1. Diese Initialisierung wird normalerweise durch die Schaltkreise des Gerätes vorgenommen, wenn die Geräte in Betrieb genommen werden, bevor die Programmausführung beginnt. Bis jetzt haben wir angenommen, dass die Adressen, die von der CPU zum Zugriff auf Befehle und Operanden genutzt werden, sich immer auf Hauptspeicherstellen beziehen. Alternativ benutzen viele Rechner das Konzept des memory mapped I/O, bei dem ein reservierter Adressbereich benutzt wird, um sich auf die Register von Peripheriegeräten zu beziehen, wie z. B. DATAIN und DATAOUT. Damit braucht man keine besonderen Anweisungen, um auf die Inhalte dieser Register zuzugreifen. Daten können mit denselben Anweisungen zwischen diesen Registern und der CPU ausgetauscht werden, die wir bereits besprochen haben, wie MOVE, LOAD und STORE. Es entsteht also der Eindruck, als ob DATAIN oder DATAOUT Speicheradressen wären. Außerhalb des Prozessors sitzen Logikschaltkreise, die erkennen, dass es sich bei diesen Adressen in Wirklichkeit um Geräteregister handelt und lenken die Daten dorthin. Dazu werden die Adressen entsprechend dekodiert, sodass statt des Speichers E/A-Register verwendet werden. Mit der folgenden Anweisung kann der Inhalt des Tastaturpufferregisters DATAIN in das Register R 1 der CPU übertragen werden. MOVE

DATAIN,R 1

Technische Informatik I

71

5. Programm- und Datenstrukturen Genauso können die Inhalte von R 1 nach DATAOUT kopiert werden. Die Steuerbits SIN und SOUT werden automatisch zurückgesetzt, wenn auf die Pufferregister DATAIN und DATAOUT zugegriffen wird. Diese beiden Befehle implementieren die allgemeinen Ein- und Ausgabeanweisungen. Wir haben festgestellt, dass die beiden Datenpuffer aus Abbildung 5.12 adressiert werden können, als ob sie zwei Speicheradressen wären. Es ist möglich, die Zustandsbits SIN und SOUT auf die gleiche Weise zu behandeln, indem man ihnen Adressen zuweist. Für ein Gerät wie ein Terminal ist es jedoch weitaus üblicher, SIN und SOUT in einem einzelnen Register, dem Gerätezustandsregister, (ähnlich dem Zustandsregister einer CPU) zusammenzufassen. Nehmen wir an, dass die Bits b3 und b4 im Register IOSTATUS zu SIN und SOUT gehören. Die Leseanweisung kann so implementiert werden: READWAIT

Testbit #3,IOSTATUS Branch=0 READWAIT MOVE DATAIN,R 1 Die Schreibanweisung würde so aussehen: WRITEWAIT

Testbit #4,IOSTATUS Branch=0 WRITEWAIT MOVE R 1 ,DATAOUT Die Testbit-Anweisung überprüft den Zustand eines der Bits in IOSTATUS, wobei das zu testende Bit als erster Operand angegeben wird. Wenn das zu testende Bit gleich 0 ist, dann ist die Bedingung der Sprunganweisung erfüllt und ein Sprung zum Beginn der Warteschleife wird durchgeführt. Wenn das Gerät bereit ist, d. h. wenn das getestete Bit gleich 1 wird, werden die Daten aus dem Eingabepuffer gelesen oder in den Ausgabepuffer geschrieben.

5.8. Stack (Stapelspeicher) Nachdem wir bis hierhin die Programmstruktur behandelt haben, soll es uns nun um Datenstrukturen gehen. Daten, mit denen in einem Programm gearbeitet wird, können auf verschiedene Weise organisiert sein. Wir sind bereits Daten begegnet, die als Listen strukturiert waren. Betrachten wir nun eine weitere wichtige Datenstruktur, die als Stapel oder Stack bekannt ist. Im Deutschen verwendet man auch den Begriff Kellerspeicher, im Folgenden verwenden wir Stapel und Stack gleichbedeutend. Ein Stack ist eine Liste von Datenelementen, normalerweise Worte oder Bytes, mit der Beschränkung, dass Elemente nur an einem Ende der Liste hinzugefügt oder entfernt werden können. Dieses Ende bezeichnet man als Spitze des Stapels (top), das andere Ende nennt man Boden (bottom). Man stelle sich einen Tablettstapel in einer Mensa vor. Die hungrigen Gäste entnehmen jeweils ein Tablett von der Spitze des Stapels und saubere Tabletts werden zum Stapel hinzugefügt, indem sie einfach oben drauf gestellt werden. Eine andere Beschreibung für diesen Speichermechanismus ist Last In, First Out (Zuletzt rein, zuerst raus) oder kurz LIFO: Das letzte Element, welches hinzugefügt wurde, ist das erste, was entnommen wird, wenn die Daten gelesen werden. Im Englischen haben sich die Begriffe push für das Hinzufügen von Elementen und pop für das Entfernen des obersten Elementes durchgesetzt.

72

Technische Informatik I

5.8. Stack (Stapelspeicher) Die Daten im Hauptspeicher eines Rechners können als Stapel organisiert werden, wobei aufeinanderfolgende Elemente auch auf aufeinanderfolgende Adressen gespeichert werden. Nehmen wir an, dass das erste Element in der Stelle BOTTOM platziert wird und dass beim Hinzufügen neuer Elemente diese auf den vorhergehenden Adressen gelegt werden. Wir benutzen im weiteren Verlauf der Erklärung einen Stapel, der in Richtung der kleiner werdenden Adressen wächst, da dies die übliche Konvention ist (im Gegensatz zu den bisherigen Listen). 0



StackZeigerRegister

-28 17 739

SP



Stack

Aktuelle Stapelspitze

43

Bodenelement



BOTTOM

k

2 -1

Abbildung 5.13.: Stack im Hauptspeicher Abbildung 5.13 zeigt einen Stapel mit Daten im Hauptspeicher eines Rechners. Er enthält die Zahlenwerte 43 am Boden und –28 an der Spitze. Ein CPU-Register wird dazu benutzt, die Adresse des jeweiligen obersten Elements des Stapels zu speichern. Dieses Register nennt man stack pointer (Stapelzeiger). Es könnte eines der Allgemeinregister sein oder ein speziell für diese Aufgabe angelegtes Register. Das Hinzufügen eines Elementes kann nun mit den folgenden Anweisungen geschehen, wobei SP den Stapelzeiger darstellt: Dekrementiere SP MOVE ELEMENT,(SP) Diese Anweisungen kopieren das Wort von Speicherstelle ELEMENT auf die Spitze des Stacks, nach dem der Zeiger dekrementiert wurde (wodurch der Stapel wächst). Das Herunterholen eines Elementes geschieht dann mit: MOVE (SP),ELEMENT INKREMENTIERE SP Diese Anweisungen kopieren den Wert der Stapelspitze in die Speicherstelle ELEMENT und erhöhen dann das Zeigerregister, sodass es auf die neue Spitze zeigt. Abbildung 5.14 zeigt die Auswirkungen aller Operationen auf dem Stack aus Abbildung 5.13. Da Stapeloperationen sehr häufig vorkommen, bieten einige Prozessoren spezielle Befehle für das Drauflegen und Herunterholen von Elementen. Wenn der Prozessor Adressierungsarten mit Autoinkrement und Autodekrement beherrscht, können die Operationen ebenfalls mit jeweils nur einem Befehl durchgeführt werden: MOVE ELEMENT,-(SP) (für das Drauflegen) MOVE (SP)+,ELEMENT (für das Herunterholen)

Technische Informatik I

73

5. Programm- und Datenstrukturen

SP Alter SP

19 -28 17 739

Alter SP SP

-28 17 739

Stack

43 NewItem

19

Item

43 NewItem

19

Item

-28

(a) Nach Push von NewItem

(b) Nach Pop von Item

Abbildung 5.14.: Auswirkungen der Stack-Operationen Wenn ein Stack in einem Programm benutzt wird, kann er einer festen Anzahl von Speicherstellen im Hauptspeicher zugewiesen werden. In diesem Fall muss man das Drauflegen von Elementen vermeiden, wenn der Stapel seine maximale Größe erreicht hat. Genauso sollte man aufpassen, dass man kein Element von einem leeren Stapel herunterholt, da diese Operation zwar syntaktisch korrekt wäre, aber in einem Programmfehler enden würde. Man stelle sich einen Stapel vor, der von Adresse 2000 (BOTTOM) bis zu Adresse 1500 läuft (und nicht tiefer). Der Stackpointer wird anfangs auf 2001 gesetzt (da er vor dem Herauflegen des ersten Elementes ohnehin erhöht wird). Um zu verhindern, dass aus einem leeren Stack gelesen oder in einen vollen Stack geschrieben wird, ersetzt man die einfache einzelne Anweisung durch Anweisungsfolgen, wie sie in Abbildung 5.15 und Abbildung 5.16 dargestellt sind. Die Vergleichsanweisungen subtrahieren den ersten Operanden vom zweiten und setzen die Zustandsbits entsprechend dem Ergebnis. Der Wert beider Operanden bleibt unverändert. SAFEPOP

Compare

#2000,SP

Branch > 0

EMPTYERROR

Move

(SP)+,ITEM

Überprüfen, ob der Stackpointer einen Wert größer 2000 enthält Falls ja, ist der Stack leer. Sonst oberstes Element an die Speicherstelle ITEM kopieren

Abbildung 5.15.: Sichere Pop-Operation

5.9. Queues (Warteschlangen) Eine andere nützliche Datenstruktur, die dem Stapel sehr ähnlich ist, ist die Warteschlange (Queue). Daten werden auf Grundlage des „First In, First Out“-Prinzips (FIFO) in eine Warteschlange geschrieben oder aus ihr gelesen. Als Analogie stelle man sich eine belie-

74

Technische Informatik I

5.10. Subroutinen (Unterprogramme) SAFEPUSH

Compare

#1500,SP

Branch > 0

FULLERROR

Move

NEWITEM,-(SP)

Überprüfen, ob der Stackpointer einen Wert kleiner/gleich 1500 enthält Falls ja, ist der Stack voll. Sonst Element von der Speicherstelle NEWITEM auf den Stack legen

Abbildung 5.16.: Sichere Push-Operation bige Warteschlange vor, wobei sich vielleicht die in der Magdeburger Mensa zwischen 12:30 Uhr und 13:15 Uhr am besten eignet. Nehmen wir an, dass die Queue in Richtung der ansteigenden Adressen wächst, sodass die Daten am Ende (höhere Adressen) hinzugefügt werden und vom Beginn (niedrige Adressen) gelesen werden. Es gibt zwei wesentliche Unterschiede zwischen der Implementierung eines Stapels und einer Warteschlange. Der eine Unterschied bezieht sich auf die Anzahl der Zeiger, die man benötigt. Beide Datenstrukturen werden durch eine Startposition und eine Endposition in ihrer Ausdehnung definiert. Beim Hinzufügen oder Entnehmen von Daten bleibt beim Stapel das eine Ende, der Boden, jedoch unverändert, während das andere Ende steigt und fällt. Es genügt folglich ein einzelner Zeiger. Bei der Queue hingegen hat man zwei Zeiger: Einer für die Start- und einer für die Endposition. Es können Daten am Ende hinzugefügt werden, was zu einer veränderten Endposition führt, als auch Daten entnommen werden, was zu einer anderen Startposition führt. Der andere Unterschied zwischen Stapel und Queue ist, dass sich die Queue ohne weitere Steuerung kontinuierlich durch den gesamten Speicher eines Rechners bewegen würde, von einem Ende zum anderen. Eine Möglichkeit, die Queue in einem bestimmten Teil des Speichers zu begrenzen, ist die Verwendung eines Ringpuffers (circular buffer). Nehmen wir an, dass die Speicheradressen von ANFANG bis ENDE der Queue zugewiesen wurden. Der erste Eintrag der Queue wird an die Stelle ANFANG geschrieben und folgende Einträge werden an die Queue angehängt, indem sie an die nachfolgend größeren Adressen geschrieben werden. Sobald die Queue ENDE erreicht, muss Platz am Anfang geschaffen worden sein, da einige Elemente bereits aus der Queue entfernt wurden. Folglich muss der Queue-Ende-Zeiger auf den Wert ANFANG zurückgesetzt werden und der Vorgang geht weiter.

5.10. Subroutinen (Unterprogramme) Aufgaben, und damit Befehlsfolgen, die in verschiedenen Programmen identisch sind, lassen sich aus diesen Programmen herausnehmen und separat codieren. Das Ergebnis dieser Auslagerung ist ein Unterprogramm (subroutine). Es ist möglich, den Befehlsblock, der die Subroutine bildet, bei Bedarf in ein Programm einzubinden. Man spart Speicherplatz, wenn man nur eine Kopie der Befehle für die Subroutine im Hauptspeicher hinterlegt und jedes Programm, das Gebrauch von dieser Subroutine machen will, zu ihrer Startposition springt. Wenn ein Programm zu einer Subroutine springt, sagt man, dass es diese Subroutine aufruft. Der Befehl, der diese Sprungoperation ausführt, nennt man Subroutinenaufruf.

Technische Informatik I

75

5. Programm- und Datenstrukturen Nachdem eine Subroutine ausgeführt wurde, muss das aufrufende Programm die Ausführung direkt nach dem Befehl fortsetzen, mit der die Subroutine aufgerufen wurde. Man sagt, dass von der Subroutine zurückgesprungen wird. Da die Subroutine von verschiedenen Stellen des aufrufenden Programms heraus aufgerufen werden kann, müssen Vorkehrungen getroffen werden, dass zur entsprechenden Adresse zurückgekehrt wird. Die Adresse, an der das aufrufende Programm seine Ausführung wieder aufnimmt, ist die Adresse, auf die der Programmzähler während der Ausführung des Subroutinenaufrufes zeigte. Folglich muss der Inhalt des Programmzählers während des Aufrufes gesichert werden, um einen korrekten Rücksprung zum aufrufenden Programm gewährleisten zu können. Die Weise, in der ein Rechner den Aufruf und Rücksprung von einer Subroutine realisiert, nennt man Unterprogrammaufrufmethode (subroutine linkage method). Die einfachste Unterprogrammaufraufmethode besteht aus dem Sichern der Rücksprungadresse an eine bestimmte Position, die unter anderem ein für diese Aufgabe existierendes Register sein kann. Solch ein Register nennt man Linkregister. Von der Subroutine wird dann zum aufrufenden Programm zurückgekehrt, in dem zur Adresse gesprungen wird, die im Linkregister steht (indirekter Sprung). In diesem Fall ist der Subroutinenaufruf eine spezielle Sprunganweisung, welche die folgenden Operationen durchführt: • Sichere den Inhalt des Programmzählers im Linkregister. • Springe zur Zieladresse, die im Befehl angegeben ist. Die Rücksprunganweisung führt diese Operation aus: • Springe zur Adresse, die sich im Linkregister befindet. Abbildung 5.17 veranschaulicht den Vorgang.

Programmaufruf

SpeicherStelle



SpeicherStelle

1000



Aufruf_Subroutine SUB Nächster Befehl

Erster Befehl



200 201

Subroutine SUB

Rücksprung_aus_Subroutine

1000

PC

201

Link Aufruf

201 Rücksprung

Abbildung 5.17.: Aufruf einer Subroutine und Rücksprung Eine übliche Programmierpraxis, die sogenannte Subroutinenverschachtelung, ist, dass eine Subroutine eine andere aufruft. In diesem Fall würde die Rücksprungadresse des zweiten Aufrufs ebenfalls im Linkregister gesichert werden und somit den vorherigen Inhalt

76

Technische Informatik I

5.10. Subroutinen (Unterprogramme) des Registers überschreiben. Folglich müssen die Inhalte des Linkregisters vor dem Aufruf einer anderen Subroutine an anderer Stelle hinterlegt werden. Anderenfalls wäre die Rücksprungadresse der ersten Subroutine verloren. Subroutinenverschachtelung kann in beliebiger Tiefe erfolgen. Irgendwann beendet die letzte Subroutine ihre Berechnung und kehrt zur Subroutine zurück, durch die sie aufgerufen wurde. Die Rücksprungadresse für diesen ersten Rücksprung ist die letzte, die während des verschachtelten Aufrufs erzeugt wurde. Das bedeutet, dass Rücksprungadressen dem LIFO-Prinzip entsprechend erzeugt und verwendet werden. Dies wiederum legt es nahe, dass die Rücksprungadressen, die mit Subroutinenaufrufen verbunden sind, auf einem Stapelspeicher abgelegt werden sollten. Einige Prozessoren tun dies automatisch als eine der Operationen, die bei Subroutinenaufruf durchgeführt werden. Einem bestimmten Register wird der dabei verwendete Stapelzeiger zugewiesen. Den Stapel selbst nennt man in diesem Fall Prozessorstack. Hierbei legt der Rechner im Falle eines Unterprogrammaufrufs den Inhalt des Programmzählers auf den Prozessorstack ab und lädt die Subroutinenadresse in den Programmzähler. Die Rücksprunganweisung holt die Adressen dann vom Stapelspeicher in den Programmzähler herunter. Der 68000-Prozessor benutzt diesen Ansatz. Der PowerPC hingegen sichert bei Subroutinenaufruf die Rücksprungadresse im Linkregister. Hier muss der Programmierer dafür sorgen, dass der Inhalt des Linkregisters vor dem Aufruf einer weiteren Subroutine gesichert wird.

5.10.1. Parameterübergabe Wenn eine Subroutine aufgerufen wird, muss das Programm in der Lage sein, Parameter an die Subroutine zu übergeben. Dies bedeutet, dass die Operanden oder ihre Adressen in der Subroutine verwendet werden. Später gibt die Subroutine andere Parameter zurück, wobei es sich um die Ergebnisse handelt. Den Austausch dieser Informationen zwischen dem aufrufenden Programm und der Subroutine bezeichnet man als Parameterübergabe. Parameterübergabe kann auf verschiedene Arten erfolgen. Die Parameter können in Registern oder festen Speicherpositionen abgelegt werden, von wo aus sie auch von der Subroutine aus erreicht werden können. Alternativ dazu können die Parameter auch auf einem Stapelspeicher abgelegt werden. Dies kann möglicherweise auch der Prozessorstack sein, auf dem normalerweise die Rücksprungadressen abgelegt werden. Die Übergabe von Parametern durch CPU-Register ist sehr einfach und effizient. Sind jedoch viele Parameter im Spiel, besteht die Möglichkeit, dass nicht genügend Allgemeinregister zu diesem Zweck zur Verfügung stehen. Und das aufrufende Programm kann auf Informationen in einigen Registern angewiesen sein, die nach der Rückkehr aus den Subroutinen noch verwendet werden sollen, wodurch diese Register nicht für die Parameterübergabe zur Verfügung stehen. Die Verwendung des Stapels hingegen ist hochflexibel: Ein Stapelspeicher kann eine große Anzahl von Parametern behandeln. Das folgende Beispiel veranschaulicht diesen Ansatz. Abbildung 5.18 zeigt das Programm aus Abbildung 5.9 als Subroutine LISTADD, die aus jedem anderen Programm heraus aufgerufen werden kann, um eine Liste von Zahlen zu addieren. Die Parameter, die an die Subroutine übergeben werden sind die Adresse der Liste und die Anzahl der Einträge. Die Subroutine führt die Addition aus und gibt die berechnete Summe zurück. Die Parameter werden über den Prozessorstack übergeben, auf den das Register SP zeigt. Nehmen wir an, dass vor dem Aufruf der Subroutine die

Technische Informatik I

77

5. Programm- und Datenstrukturen

Ebene 3

[R2] [R1] [R0]

Ebene 2

Rücksprungadresse n Liste

Ebene 1

Abbildung 5.18.: Parameterübergabe über den Stack Spitze des Stacks auf Ebene 1 ist, wie in Abbildung 5.18. Das aufrufende Programm legt die Adresse LIST und den Wert n auf den Stack und ruft die Subroutine LISTADD auf. Die Subroutinenaufrufanweisung legt auch noch die Rücksprungadresse auf den Stack an. Die Stackspitze ist nun auf Ebene 2. Die Subroutine verwendet drei Register. Da diese Register gültige Daten enthalten können, die zum aufrufenden Programm gehören, sollten ihre Inhalte durch Ablegen auf den Stack gesichert werden. Wir haben einen einzelnen Befehl MOVE_MULTIPLE benutzt, um die Inhalte der Register R 0 bis R 2 auf dem Stack zu sichern. Viele Prozessoren haben solch einen Befehl. Die Spitze des Stacks ist nun auf Ebene 3. Die Subroutine greift mit Indexadressierung auf die Parameter n und LIST vom Stack zu. Der Wert n wird in R 1 als Anfangswert des Zählers geladen. Die Adresse LIST wird in R 2 geladen, wo sie als Zeiger auf die Listeneinträge verwendet wird. Am Ende der Berechnung enthält das Register R 0 die Summe. Bevor die Subroutine zum aufrufenden Programm zurückspringt, werden die Inhalte von R 0 auf dem Stack hinterlegt. Sie ersetzen dort den Parameter LIST, der nicht länger benötigt wird. Anschließend werden die Inhalte der drei Register, die von der Subroutine verwendet wurden, vom Stack zurückgelesen. Nun ist das Element an der Spitze des Stacks wieder auf Ebene 2. Nachdem die Subroutine zurückgesprungen ist, sichert das aufrufende Programm die Inhalte in der Stelle SUM und verringert die Stackspitze auf das ursprüngliche Niveau. Machen wir ein paar Kommentare zu diesem Beispiel. Erstens beachte man die Art der zwei Parameter LIST und n , die an die Subroutine übergeben werden. Der Zweck der Subroutine ist die Addition einer Liste von Zahlen. Anstatt die eigentlichen Listeneinträge zu übergeben, übergibt das aufrufende Programm einen Zeiger, welcher die Adresse der Liste im Speicher enthält. Diese Technik nennt man Referenzübergabe (passing by reference). Der zweite Parameter wird per Wertübergabe (passing by value) übergeben, d. h. es wird tatsächlich die eigentliche Anzahl von Einträgen an die Subroutine übergeben. Zweitens beobachte man, wie der Platz des Stapelspeichers verwendet wird. Während der Ausführung der Subroutine enthalten sechs Stellen des Stapels Einträge, die von der Subroutine benötigt werden. Diese Stellen bilden einen privaten Arbeitsbereich für die Subroutine, der zum Zeitpunkt des Starts der Subroutine geschaffen und der wieder freigegeben wird, wenn die Subroutine zurückspringt. Dieser Platz wird häufig als Stackrahmen (stack frame) bezeichnet. Wenn die Subroutine mehr Platz für ihre lokalen Variablen

78

Technische Informatik I

5.10. Subroutinen (Unterprogramme) benötigt, kann sie sich den Platz besorgen, in dem sie die Spitze des Stapelspeichers anhebt. Compiler für Hochsprachen verwenden die hier angesprochenen Ideen, um Subroutinenaufrufe zu realisieren. Eine detaillierte Auseinandersetzung mit dem Thema würde den Rahmen dieses Skriptes sprengen. Wir haben Beispiele zur Veranschaulichung geboten. Dies soll als sprachliche Beschreibung der Ausführung von Befehlen auf der Assemblerebene genügen.

Technische Informatik I

79

Teil II.

Rechnerentwurf

81

6. Kombinatorische Schaltnetze Ein kombinatorisches Schaltnetz ist die reale Umsetzung einer logischen oder mathematischen Funktion. Das kleinste Element eines solchen Schaltnetzes ist ein Gatter, eine Box (d. h. eine Schaltung) mit einem oder mehreren Eingängen und einem Ausgang, wobei der Ausgang das Ergebnis der Anwendung einer Funktion (realisiert durch das betreffende Gatter) auf die Eingangsbelegungen darstellt. Eingänge und Ausgang sind dabei zweiwertig, also 1 oder 0.

Abbildung 6.1.: Beispiel für die Berechnung von y = f (x) = 2x 2 + x + 1 mit Gattern In Abbildung 6.1 ist ein Beispiel dargestellt, wie man eine mathematische Funktion mit einem kombinatorischen Schaltnetz berechnen kann. Aus diesem Beispiel sind folgende Konventionen umgesetzt: 1. Schaltnetze werden in Richtung der Pfeilorientierung gelesen. 2. Soll eine Variable an mehreren Eingängen angelegt werden (d. h. mit mehreren Eingängen verknüpft werden), dann wird dies durch einen Verknüpfungspunkt symbolisiert – im Gegensatz dazu, wenn sich zwei Leitungen einfach im Design kreuzen, ohne miteinander verknüpft zu sein. In sequentiellen Schaltungen hängen Ausgänge unter Umständen auch von früheren Ausgängen ab. Man braucht also so etwas wie Taktung, um die Schaltungen zu synchronisieren. Dazu später mehr.

6.1. Die Basisgatter Es gibt eine handvoll logischer Gatter, aus denen sich sämtliche kombinatorische Schaltnetze zusammensetzen lassen. Die grundlegenden davon (Abbildung 6.2) werden nachfolgend beschrieben: Die Gatter NICHT (NOT), UND (AND) und ODER (OR) repräsentieren die drei Grundoperationen in der zweiwertigen Logik (boolesche Algebra). Mithilfe dieser drei Funktionen können alle anderen dargestellt werden. Die Gatter NAND (Not AND) und NOR (Not OR), die man auch als Reihenschaltung von UND und NICHT bzw. ODER und NICHT realisieren könnte, sind schneller und billiger als UND- bzw. ODER-Gatter. Ein

83

6. Kombinatorische Schaltnetze

Abbildung 6.2.: Symbole der logischen Gatter weiterer Vorteil von NAND und NOR ist es, nur unter Verwendung einer dieser Gatterarten alle anderen Gattertypen realisieren zu können. Als letztes gibt es noch das Gatter XOR (gesprochen EX-OR, eXclusive OR); es entspricht einem “Entweder . . . oder . . .” und ist ein vielfach verwendetes logisches Element beim Vergleichen. Die Kreise am Ausgang von NICHT, NAND und NOR werden Inversionsblasen (inversion bubbles) genannt. Sie können zwischen Ein- oder Ausgangsleitung und Gattersymbol auftauchen. Ihre Funktion entspricht der eines NICHT-Gatters. Die Funktion eines Gatters, also das Verhältnis von Eingangsvariablen zu Ausgangsvariablen, wird üblicherweise mit Wahrheitstabellen dargestellt. In ihnen wird für jede mögliche Kombination von Eingaben (links des Doppelstrichs) die entsprechenden Ausgaben (jeweils unterhalb einer genannten Funktion, die sich aus den Eingaben kombiniert) angegeben. Tabelle 6.1 ist eine solche Wahrheitstabelle; sie beschreibt die Basisgatter vollständig. Wie man sieht, lässt sich NAND als „Nicht beide“ und NOR als „Weder noch“ beschreiben. A

B

NICHT(A)

UND(A, B)

ODER(A,B)

NAND(A, B)

NOR(A,B)

XOR(A,B)

0

0

1

0

0

1

1

0

0

1

1

0

1

1

0

1

1

0

0

0

1

1

0

1

1

1

0

1

1

0

0

0

Tabelle 6.1.: Wahrheitstabelle der in Abbildung 6.2 dargestellten Gatter Wir können diese Gatter auch als Formel darstellen: • NICHT(A) = ¬A = A • UND (A, B ) = A ∧ B = A · B = AB • ODER (A, B ) = A ∨ B = A + B • XOR (A, B ) = A ⊕ B In dieser Lehrveranstaltung verwenden wir die jeweils am Zeilenende notierte Form. In der Sprache der elektrischen Lehre ausgedrückt, ist ein UND-Gatter die Serienschaltung zweier Schalter und ein ODER-Gatter die Parallelschaltung (Abbildung 6.3). NICHT und XOR lassen sich als Öffner bzw. als Wechselschaltung implementieren (Abbildung 6.4).

84

Technische Informatik I

6.2. Beispiele für kombinatorische Schaltnetze

Abbildung 6.3.: Realisierung von UND und ODER mit elektrischen Schaltern

Abbildung 6.4.: Realisierung von NICHT und XOR mit elektrischen Schaltern Gatter mit zwei Eingängen lassen sich zur Steuerung des Informationsflusses einsetzen, wobei einer der beiden Eingänge die Funktion einer Steuerungsvariablen übernimmt. Dessen Belegung (0 oder 1) entscheidet über den Durchlass oder auch die Komplementierung des anderen Eingangssignal (Abbildung 6.5).

Abbildung 6.5.: Gatter als Steuerungseinheiten eines Informationsflusses X. Das Signal C steuert, ob und wie X weitergeleitet wird

6.2. Beispiele für kombinatorische Schaltnetze In diesem Abschnitt werden drei Beispiele für einfache kombinatorische Schaltnetze gegeben, wie sie tatsächlich später anzutreffen sind.

6.2.1. Beispiel 1: Mehrheitsentscheider In Abbildung 6.6 wird eine Schaltung mit drei Eingangsvariablen dargestellt, die aus drei UND-Gattern und einem ODER-Gatter besteht. Man kann die drei Eingangsvariablen A, B und C, die jeweils die Werte 0 oder 1 annehmen können, auf 23 = 8 verschiedene Weisen belegen. Um die Ausgabe F (Funktionswert) besser berechnen zu können, wurden für die Stellen P, Q und R jeweils zusätzlich Zwischenzustände ermittelt, die in der Wahrheitstabelle (Tabelle 6.2) mit aufgeführt werden. Daraus ergibt sich die Ausgabefunktion F = P + Q + R = AB + BC + AC . F ist wahr (= 1, Strom fließt), wenn zwei der Eingänge auf wahr stehen. Solange aber nur maximal ein Eingang wahr ist, ist der Ausgang falsch (= 0, kein Strom fließt). Die Ausgabe hat also denselben Wert wie die Mehrheit der Eingaben.

Technische Informatik I

85

6. Kombinatorische Schaltnetze

Abbildung 6.6.: Kombinatorisches Schaltnetz: Mehrheitsentscheider A

B

C

P = AB

Q = BC

R = AC

F = P +Q + R

0

0

0

0

0

0

0

0

0

1

0

0

0

0

0

1

0

0

0

0

0

0

1

1

0

1

0

1

1

0

0

0

0

0

0

1

0

1

0

0

1

1

1

1

0

1

0

0

1

1

1

1

1

1

1

1

Tabelle 6.2.: Wahrheitstabelle für die Schaltung in Abbildung 6.6 Eine solche Schaltung lässt sich verschiedentlich einsetzen. Beispielsweise kann sie als Mehrheitsentscheider zu Fehlertoleranzzwecken eingesetzt werden. Dabei sollen Werte an den Eingängen stets den gleichen Wert haben. Ist dies nicht der Fall, weil bei einem der vorgeschalteten Geräte ein Fehler aufgetreten ist, ist das Ergebnis F der Schaltung trotzdem korrekt – bis zu ein umgedrehtes Bit wird also korrigiert. Die Schaltung wird weiterhin in einer Addier-Einheit eingesetzt, auf die wir später eingehen werden.

6.2.2. Beispiel 2: Multiplexer Eine nicht ganz so intuitiv verständliche Schaltung ist in Abbildung 6.7 dargestellt. Auch diese Schaltung besitzt drei Eingaben, eine Ausgabe und drei Zwischenwerte. Daraus resultiert die Wahrheitstabelle, die in Tabelle 6.3 abgebildet ist. Wie der Wahrheitstabelle zu entnehmen ist, gilt: Ist X Null, entspricht der Ausgang F dem Eingang Y . Ist aber X Eins, so entspricht F dem Eingang Z . Offenbar steuert in diesem Schaltnetz X also, welche der beiden Eingänge Y und Z zum Ausgang durchgeschaltet wird. Solch eine Schaltung nennt man Multiplexer mit zwei Eingängen. Man könnte die Schaltung auch Selektor nennen. Eine entsprechende elektrische Realisation ist in Abbildung 6.8 dargestellt.

86

Technische Informatik I

6.2. Beispiele für kombinatorische Schaltnetze

Abbildung 6.7.: Kombinatorisches Schaltnetz: Multiplexer X

Y

Z

P=X

Q =YP

R =XZ

F = QR

0

0

0

1

1

1

0

0

0

1

1

1

1

0

0

1

0

1

0

1

1

0

1

1

1

0

1

1

1

0

0

0

1

1

0

1

0

1

0

1

0

1

1

1

0

0

1

1

0

1

1

1

0

1

0

1

Tabelle 6.3.: Wahrheitstabelle für die Schaltung in Abbildung 6.7 Eine wichtige Anwendung für Multiplexer ist die Hardwareimplementierung einer IfThen-Else-Anweisung, d. h. die Durchführung einer zweiseitigen Auswahl. Eine Beispielschaltung wird in Abbildung 6.9 dargestellt. Abhängig vom Inhalt einer Zelle des Condition-Code-Registers (ein Statusregister, welches Eigenschaften des Ergebnisses einer vorausgegangenen Aktion des Rechners speichert), wird der Programmzähler mit dem Inhalt des Registers X oder Y geladen. Diese Register enthalten Adressen der entsprechend nächsten auszuführenden Instruktion.

6.2.3. Beispiel 3: XOR Mit logischen Gattern lassen sich auch Schaltnetze erstellen, deren Funktion ein anderes logisches Gatter repräsentiert. Dies ist beispielsweise nötig, wenn nur bestimmte logische Gatter zur Verfügung stehen (zum Beispiel bei NAND-Flash, einer Speicherart). In die-

Abbildung 6.8.: Schalterrepräsentation eines Multiplexers

Technische Informatik I

87

6. Kombinatorische Schaltnetze

Abbildung 6.9.: Multiplexer als Anwendung eines bedingten Sprungs in einem Prozessor sem dritten Beispiel wird gezeigt, wie sich ein XOR-Gatter ohne die Verwendung eines XOR-Gatters erstellen lässt (Abbildung 6.10).

Abbildung 6.10.: Kombinatorisches Schaltnetz: XOR Dieses Schaltnetz stellt die folgende Funktion dar: F = (A + B )AB = AB + AB

Der Wahrheitstabelle (Tabelle 6.4) lässt sich entnehmen, dass F genau dann Eins ist, wenn genau einer der Eingänge Eins ist; dies entspricht wie angekündigt dem XOR-Gatter. Die Funktion F = AB + AB lässt sich auch in dieser Form in eine Schaltung umsetzen (Abbildung 6.11), wobei nur UND-, ODER- und NICHT-Gatter Verwendung finden. Ebenfalls ist es möglich, nur NAND-Gatter zu verwenden (Abbildung 6.12). Dass das in Abbildung 6.12 dargestellte Schaltnetz mit dem in Abbildung 6.11 äquivalent ist, lässt sich nur schwer durch bloßes Draufschauen nachvollziehen. Tatsächlich realisiert die NANDSchaltung die Funktion F = A AB AB B ; diese lässt sich mithilfe von Umformungen, die später vorgestellt werden, aber in die Form F = AB + AB überführen. Ein anderer Weg,

88

Technische Informatik I

6.2. Beispiele für kombinatorische Schaltnetze A

B

P = A +B

Q = AB

F = PQ

0

0

0

1

0

0

1

1

1

1

1

0

1

1

1

1

1

1

0

0

Tabelle 6.4.: Wahrheitstabelle für die Schaltung in Abbildung 6.10

die Äquivalenz zu zeigen, ist es, für beide Schaltungen die Wahrheitstabellen aufzustellen und zu vergleichen. Dies bleibt dem Leser zur Übung überlassen.

Abbildung 6.11.: XOR-Schaltnetz, nur aus UND, ODER und NICHT erstellt

Abbildung 6.12.: XOR-Schaltnetz, nur aus NAND-Gattern erstellt UND, ODER, NAND und NOR sind nicht auf zwei Eingänge festgelegt. Ebenso ist es möglich, mehr Eingänge einzuleiten oder weniger (ein einziger Eingang zum Beispiel, um aus einem NAND ein NICHT zu machen). In der Praxis setzen allerdings der beschränkte Raum und die Produktionskosten Grenzen. Das XOR-Gatter ist im Gegensatz zu den genannten Gattern auf zwei Eingänge festgelegt. XOR-Gatter werden verwendet, um Bits zu vergleichen. Immer, wenn die beiden Eingänge verschieden sind, signalisiert der XOR-Ausgang dies mit einer Eins. Mit vielen XORGattern lassen sich ganze binäre Wörter vergleichen. Führt man alle XOR-Ausgänge schließlich in einem NOR zusammen (wie in Abbildung 6.13 dargestellt), so liegt am Ausgang F ein Signal an, das Auskunft darüber gibt, ob alle Bits gleich sind (Eins) oder mindestens ein Unterschied aufgetreten ist (Null). Eine solche Schaltung wird in Prozessoren eingesetzt, um ein Kriterium für einen bedingten Sprung schnell zu ermitteln.

Technische Informatik I

89

6. Kombinatorische Schaltnetze

Abbildung 6.13.: Test auf Gleichheit zweier Worte durch XOR-Gatter

6.3. Äquivalenz von Schaltnetzen Zwei Schaltnetze sind zueinander äquivalent, wenn sie identische Wahrheitstabellen (bestehend aus Eingangs- und Ausgangsbelegungen) haben. Für den Hardware-Entwickler als Ingenieur und Menschen der Praxis spielt aber auch die Wirtschaftlichkeit eine hohe Rolle. So gibt es für den Vergleich äquivalenter Schaltnetze mehrere Kriterien: Die Geschwindigkeit, die Anzahl der Verbindungen und die integrierende, allgemeine Kostenfunktion. Geschwindigkeit heißt hierbei, wie lange es dauert, bis eine neue Eingabebelegung das entsprechende Ausgabesignal generiert. Das Maß hierfür ist die maximale Anzahl von Gattern, durch die ein Zustandswechsel der Eingabevariablen propagiert werden muss (längster Weg), d. h. die maximale Anzahl der in Serie geschalteten Gatter im Schaltnetz. Dieses Maß zu minimieren heißt, die Geschwindigkeit zu maximieren. In der Praxis wird dies dadurch erschwert, dass nicht alle Gatter die gleiche Propagierungsverzögerung haben. Wie schon erwähnt, sind NAND- bzw. NOR-Gatter schneller als andere Gatter. Dass die Anzahl der Verbindungen mit in Betracht gezogen werden muss, hat mehrere Gründe. Es kostet Geld, die Gatter miteinander zu verbinden. Je mehr Verbindungen existieren, desto komplexer und teurer wird der Entwurf. Gleichzeitig wächst die Wahrscheinlichkeit einer fehlerhaften Verbindung (was die Anzahl der Verbindungen auch zu einem Maß für die Zuverlässigkeit macht). Die integrierte allgemeine Kostenfunktion enthält das komplette Verbindungsmaß und Aspekte des Geschwindigkeitsmaßes. Generell ist es ausreichend, sich darunter die Summe der Anzahl der Gatter und der Anzahl der Verbindungen vorzustellen.

90

Technische Informatik I

6.4. Zusammenfassung Ein Schaltnetz ist minimal, wenn es bzgl. eines vorgegebenen Maßes (Kostenfunktion) kein äquivalentes Schaltnetz mit geringeren Kosten gibt.

6.4. Zusammenfassung Gatter sind die Basiseinheiten von Schaltnetzen. Ihr Verhalten wird beschrieben durch Wahrheitstabellen. Für die Entwicklung sind minimale Schaltnetze interessant. So bedarf es nun einer formalen, eleganten (d. h. weniger umständlichen als das Hantieren mit Wahrheitstabellen) Methodik zu Beschreibung, Analyse und Synthese sowie Minimierung von Schaltnetzen. Dies wird Aufgabe der booleschen (Schalt-)Algebra sein, die im nächsten Kapitel behandelt wird.

Technische Informatik I

91

7. Methoden zur Analyse und Synthese 7.1. Einführung in die Boolesche Algebra In diesem Kapitel gehen wir näher auf die mathematische Grundlage von kombinatorischen Schaltnetzen ein, die in Kapitel 6 vorgestellt wurden. Außerdem zeigen wir mit den Karnaugh-Diagrammen einen effektiven Weg, boolesche Funktionen zu minimieren. Zunächst definieren wir eine boolesche Algebra: Sei B = 2 = {0, 1} das Alphabet mit den Elementen 0 und 1. Seien auf B die folgenden Operationen definiert für x, y ∈ B : P

¡ ¢ = max x, y ¡ ¢ = min x, y

x+y x·y ¬x = x

= 1−x

Dann ist (B, +, ¬, ·) eine boolesche Algebra. In dieser Algebra gelten folgende Gesetze: • Kommutativgesetze: x + y = y + x , x y = y x ¡

¢

¡

¢ ¡

¢

¡

¢

¡

• Assoziativgesetze: x + y + z = x + y + z , x y z = x y z ¡

¢

¡

¢

¢

• Distributivgesetze: x y + z = x y + xz , x + y z = x + y (x + z) • Komplementgesetze: x + x = 1, xx = 0 • Idempotenzgesetze: x + x = x , xx = x • Doppelnegationsgesetz: x = x • Gesetze vom kleinsten und größten Element: – Neutralitätsgesetze: x · 1 = x , x + 0 = x – Extremalgesetze: x · 0 = 0, x + 1 = 1 • De Morgansche Gesetze: x + y = x y , x y = x + y Dass diese Gesetze tatsächlich gelten, lässt sich über Wahrheitstabellen beweisen, d. h. Überprüfung aller möglichen Kombinationen von Eingangsvariablen. Da hierbei alle Elemente des Definitionsbereiches der Funktion betrachtet werden, entspricht dies einem mathematischen Beweis durch vollständige Induktion. Als Beispiel wird in Tabelle 7.1 der Beweis der sehr wichtigen De Morganschen Gesetze geführt werden. Diese Regeln dienen der Komplementierung von + durch · und umgekehrt sowie von Variablen (x durch x ) und Literalen (0 durch 1). Weitere booleschen Algebren dürften aus der Mathematik (Mengen mit den Operationen ∩, ∪, ), sowie der Aussagenlogik (∨, ∧, ¬) bekannt sein. Wichtig: Die Negation ¬ ist nicht distributiv! Weiterhin gibt es keine Subtraktion und Division in der booleschen Algebra.

93

7. Methoden zur Analyse und Synthese x

y

x+y

x+y

x

x

xy

0

0

0

1

1

1

1

0

1

1

0

1

0

0

1

0

1

0

0

1

0

1

1

1

0

0

0

0

Tabelle 7.1.: Beweis des De Morgansches Gesetzes x + y = x y Seien nun n, m ∈ N, m > 0, dann heißt eine Funktion f = B n → B m Schaltfunktion. Eine Schaltfunktion entspricht einem Schaltnetz als Black Box mit n Eingängen und m Ausgängen (Abbildung 7.1). Als Beispiel stelle man sich eine Schaltung vor, welche die Addition von zwei 16-stelligen Dualzahlen realisiert. Die Eingabe ist ein Bitvektor der Länge 32 (n = 32), wobei jeweils 16 Bit einen Operanden enthalten. Die Ausgabe muss demzufolge ein Bitvektor der Länge m = 17 (16 + 1 Bit für möglichen Übertrag) sein. Die Black Box realisiert also eine Schaltfunktion f = B 32 → B 17 .

Abbildung 7.1.: Black Box mit n Eingängen und m Ausgängen Eine Schaltfunktion f = B n → B heißt (n -stellige) boolesche Funktion. Sei x 1 , x 2 , . . . , x n ∈ B n , dann heißt das Produkt x 1 x 2 . . . x n der Elemente von (x 1 , x 2 , . . . , x n ) ein Minterm von f . Der Minterm heißt einschlägig, wenn f (x 1 , x 2 , . . . , x n ) = 1 . Jede boolesche Funktion lässt sich als Summe (VerODERung) ihrer einschlägigen Minterme darstellen. Dazu ein Beispiel: Eine Wahrheitstabelle beschreibe die boolesche Funktion f (x 1 , x 2 , x 3 ) (Tabelle 7.2). Zeilenindex

x1

x2

x3

f (x 1 , x 2 , x 3 )

0

0

0

0

0

1

0

0

1

0

2

0

1

0

0

3

0

1

1

1

4

1

0

0

0

5

1

0

1

1

6

1

1

0

0

7

1

1

1

1

Tabelle 7.2.: Wahrheitstabelle zum Minterm-Beispiel Die interessanten Zeilen sind die Zeilen 3, 5 und 7, da der Funktionswert dort 1 ist. Diese Zeilen kann man auch in Abhängigkeit von den Variablen x i beschreiben, was an Zeile 3 vorgeführt werden soll: Es muss gelten x 1 = 0 UND x 2 = 1 UND x 3 = 1. Negieren wir x 1 , sodass gilt x 1 = 1, erhalten wir bei der VerUNDung x 1 x 2 x 3 nur dann 1 als Ergebnis, wenn genau die Eingangsbelegung von Zeile 3 gegeben ist.

94

Technische Informatik I

7.1. Einführung in die Boolesche Algebra Dieses Verfahren ermöglicht es, in Abhängigkeit von allen Variablen genau die Terme zu beschreiben, die 1 als Funktionswert ergeben. Wir ersetzen also 0 durch x i und 1 durch x i und erhalten so die Minterme. Verknüpft man diese durch ein ODER (+), erhält man die oben erwähnte Darstellung als Summe der Minterme: f = x 1 x 2 x 3 + x 1 x 2 x 3 + x 1 x 2 x 3 (meint: Wenn eine der Eingangsbelegungen so ist, dass der Funktionswert 1 erzeugt wird, gib 1 aus, sonst 0). Sie beschreibt f vollständig und ersetzt somit die aufwendige Wahrheitstabelle. Es handelt sich bei dieser Darstellungsform um die (aus der Aussagenlogik bekannte) disjunktive Normalform. In diesem Fall liegt sogar die kanonische Form vor, da jeder Minterm in der Funktion alle x i enthält, von denen f abhängig ist. Jede n -stellige boolesche Funktion ist darstellbar als Kombination der zweistelligen booleschen Funktionen + und · sowie der einstelligen booleschen Funktion ¬. Anders ausgedrückt: {+, ·, ¬} ist funktional vollständig. Aus dieser Tatsache, den Idempotenzgesetzen und den De Morganschen Gesetzen folgt, dass auch {+, ¬} und {·, ¬} funktional vollständig sind. Denn mithilfe der Negation lässt sich ein ODER auch als UND ausdrücken oder umgekehrt ein UND als ODER: xy = x · y = x + y

x+y =x+y =x·y

Das wiederum bedeutet, dass NAND und NOR jeweils für sich funktional vollständig sind (Abbildung 7.2).

Abbildung 7.2.: x + y nur mit NANDs (x y ) ¬ ist nicht durch + oder · ersetzbar (ist also unverzichtbar), aber auch allein nicht voll-

ständig. Zur An- und weiteren Verwendung leiten wir noch einige Theoreme her: Absorptionsgesetze: Es gelten x + x y = x und x(x + y) = x ; ersteres lässt sich so herleiten:

x +xy =

x ·1+xy

Neutralitätsgesetz

=

x(1 + y)

Distributivitätsgesetz

=

x(1)

Extremalgesetz

=

x

Extremalgesetz

Technische Informatik I

95

7. Methoden zur Analyse und Synthese Es gilt x + x y = x + y : ¡

Distributivitätsgesetz

=

¢¡ ¢ x +x x +y ¡ ¢ 1 x+y

=

x+y

Neutralitätsgesetz

x +xy =

Komplementgesetz

Es gilt x y + xz + y z = x y + xz : x y + xz + y z =

¡

Neutralitätsgesetz

=

x y + xz + y z · 1 ¡ ¢ x y + xz + y z x + x

Komplementgesetz

=

x y + xz + y zx + y zx

Distributivitätsgesetz

=

2x Kommutativitätsgesetz

=

x y + x y z + xz + x y z ¡ ¢ x y (1 + z) + xz 1 + y

=

x y · 1 + xz · 1

2x Extremalgesetz

=

x y + xz

2x Extremalgesetz

2x Distributivitätsgesetz

¢

¢¡

Es gilt x + y x + z = xz + x y : ¡

¢¡ ¢ x +y x +z =

xx + xz + y x + y z

Distributivitätsgesetz

=

0 + xz + x y + y z

Komplementgesetz, Kommutativgesetz

=

xz + x y + y z

Neutralitätsgesetz

=

xz + x y

Siehe vorheriges Theorem

¡

¢¡

¢¡

¢

¡

¢¡

¢

Es gilt x + y x + z y + z = x + y x + z : ¡

¢¡ ¢ ¢¡ x +y x +z y +z =

¡

¢

Siehe vorheriges Theorem

=

x y z + xzz + x y y + x y z

Distributivgesetz, 2x Kommutativg.

=

x y z + xz + x y + x y z

2x Idempotenzgesetz

xz + x y ¢¡ ¢ x +y x +z

2x Absorptionsgesetz

xz + x y

= =

¡

¢¡

y +z

Siehe vorheriges Theorem

Bislang waren alle Anwendungen der booleschen Algebra rein theoretische Beispiele, die uns als Testbasis für die Gesetze dieser Algebra dienten. Im Gegensatz dazu ist das folgende Beispiel an ein praktisches Problem angelehnt.

7.1.1. Beispiel: Entwurf eines 2-Bit-Multiplizierers Man stelle sich vor, ein Hardwareentwickler möchte einen Zwei-Bit-Multiplizierer entwickeln, also einen Baustein, der zwei Zwei-Bit-Zahlen entgegennimmt und ein Produkt von vier Bit Länge ausgibt (Abbildung 7.3). Die beiden Zwei-Bit-Eingänge heißen X (aufgeteilt in die Biteingänge X 1 und X 0 ) und Y (aufgeteilt in die Biteingänge Y1 und Y0 ). Das

96

Technische Informatik I

7.1. Einführung in die Boolesche Algebra

Abbildung 7.3.: Blockschaltbild eines Zwei-Bit-Multiplizierers

Vier-Bit-Produkt liegt am Ausgang Z (ebenfalls aufgeteilt in die Bitausgänge Z3 , . . . Z0 ) an. Beginnen wir damit, die Wahrheitstabelle aufzustellen. Die Schaltung hat vier Eingänge, die in unserer binären Betrachtungsweise 24 = 16 verschiedene Eingangsbelegungen entgegennehmen kann. Daraus ergibt sich direkt, dass bis zu 16 verschiedene Ausgangsbelegungen an den vier Ausgangsleitungen möglich sind. Die Wahrheitstabelle ist in Tabelle 7.3 abgebildet. X · Y = Z (dezimal)

X1

X0

Y1

Y0

Z3

Z2

Z1

Z0

0·0 = 0

0

0

0

0

0

0

0

0

0·1 = 0

0

0

0

1

0

0

0

0

0·2 = 0

0

0

1

0

0

0

0

0

0·3 = 0

0

0

1

1

0

0

0

0

1·0 = 0

0

1

0

0

0

0

0

0

1·1 = 1

0

1

0

1

0

0

0

1

1·2 = 2

0

1

1

0

0

0

1

0

1·3 = 3

0

1

1

1

0

0

1

1

2·0 = 0

1

0

0

0

0

0

0

0

2·1 = 2

1

0

0

1

0

0

1

0

2·2 = 4

1

0

1

0

0

1

0

0

2·3 = 6

1

0

1

1

0

1

1

0

3·0 = 0

1

1

0

0

0

0

0

0

3·1 = 3

1

1

0

1

0

0

1

1

3·2 = 6

1

1

1

0

0

1

1

0

3·3 = 9

1

1

1

1

1

0

0

1

Tabelle 7.3.: Wahrheitstabelle eines Zwei-Bit-Multiplizierers Jede 4-Bit-Eingabe repräsentiert das Produkt zweier Zwei-Bit-Zahlen, sodass zum Beispiel eine Eingabe von X 1 X 0 Y1 Y0 = 1110 das Produkt 112 · 102 (oder 310 · 210 ) darstellt. Die dazugehörige Ausgabe ist eine 4-Bit-Zahl, welche in diesem Fall den Wert 610 (oder 01102 in binärer Form) hat. Aus der Tabelle lassen sich die Ausdrücke für die vier Ausgänge Z0 – Z3 ableiten. Man beachte, dass für eine Wahrheitstabelle mit m Ausgangsleitungen immer m boolesche Funktionen abgeleitet werden müssen. Eine Funktion ist mit jeweils einer der m Ausgangsspalten assoziiert. Für das Ableiten nehmen wir die einschlägigen Minterme der einzelnen Ausgänge (also die Minterme, wo der entsprechende Ausgang den Wert Eins annimmt) und bringen sie durch ODER-Verknüpfungen in die disjunktive Normalform.

Technische Informatik I

97

7. Methoden zur Analyse und Synthese

Z0

= =

=

X 1 X 0 Y0 + X 1 X 0 Y0 ³ ´ X 0 Y0 X 1 + X 1

=

X 0 Y0

=

Z1

X 1 X 0 Y1 Y0 + X 1 X 0 Y1 Y0 + X 1 X 0 Y1 Y0 + X 1 X 0 Y1 Y0 ³ ´ ³ ´ X 1 X 0 Y0 Y1 + Y1 + X 1 X 0 Y0 Y1 + Y1

=

X 1 X 0 Y1 Y0 + X 1 X 0 Y1 Y0 + X 1 X 0 Y1 Y0 + X 1 X 0 Y1 Y0 + X 1 X 0 Y1 Y0 + X 1 X 0 Y1 Y0 ³ ´ ³ ´ X 1 X 0 Y1 Y0 + Y0 + X 1 X 0 Y0 Y1 + Y1 + X 1 X 0 Y1 Y0 + X 1 X 0 Y1 Y0

=

X 1 X 0 Y1 + X 1 X 0 Y0 + X 1 X 0 Y1 Y0 + X 1 X 0 Y1 Y0

=

=

X 1 X 0 Y1 + X 1 X 0 Y1 Y0 + X 1 X 0 Y0 + X 1 X 0 Y1 Y0 ´ ³ ´ ³ X 0 Y1 X 1 + X 1 Y0 + X 1 Y0 X 0 + X 0 Y1 ³ ´ ³ ´ X 0 Y1 X 1 + Y0 + X 1 Y0 X 0 + Y1

=

X 1 X 0 Y1 + X 0 Y1 Y0 + X 1 X 0 Y0 + X 1 Y1 Y0

=

=

Z2

= =

X 1 X 0 Y1 Y0 + X 1 X 0 Y1 Y0 + X 1 X 0 Y1 Y0 ³ ´ X 1 X 0 Y1 Y0 + Y0 + X 1 X 0 Y1 Y0

=

X 1 X 0 Y1 + X 1 X 0 Y1 Y0 ³ ´ X 1 Y1 X 0 + X 0 Y0 ´ ³ X 1 Y1 X 0 + Y0

=

X 1 X 0 Y1 + X 1 Y1 Y0

= =

Z3

=

X 1 X 0 Y1 Y0

Dadurch erhält man also diese vier vereinfachten disjunktiven Normalformen für Z3 – Z0 :

Z0

=

X 0 Y0

Z1

=

X 1 X 0 Y1 + X 0 Y1 Y0 + X 1 X 0 Y0 + X 1 Y1 Y0

Z2

=

X 1 X 0 Y1 + X 1 Y1 Y0

Z3

=

X 1 X 0 Y1 Y0

Die obigen Ausdrücke sind bezüglich X und Y symmetrisch, d. h. man kann X durch Y ersetzen und umgekehrt und kommt zu den gleichen Ergebnissen. Diese Eigenschaft entspricht der Kommutativität der Aufgabe (z. B. 3 · 1 = 1 · 3).

98

Technische Informatik I

7.1. Einführung in die Boolesche Algebra

Abbildung 7.4.: Schaltnetz eines 2-Bit-Multiplizierers

Es gibt nun mehrere Wege, diese Ausdrücke in einem Schaltnetz zu realisieren. Abbildung 7.4 illustriert einen davon. Wir haben bereits festgestellt, dass NAND und NOR jeweils funktional vollständig sind, d. h. alle Schaltnetze können auch durch Schaltnetze ersetzt werden, die nur aus NANDbzw. NOR-Gattern bestehen. Dadurch kann der bereits erwähnte Vorteil ausgenutzt werden, dass NAND- bzw. NOR-Gatter schneller und billiger sind als UND- oder ODERGatter. In der Praxis gibt es deswegen viele verschiedene Gatter dieser Typen (von Formen mit zwei Eingängen bis hin zu 13), aber nur wenige Typen von UND bzw. ODER-Gattern. Um den oben entworfenen 2-Bit-Multiplizierer nur mit NAND-Gattern aufzubauen, sind nur wenige Umformungen nötig. Es genügt, auf eine disjunktive Normalform zunächst das Doppelnegationsgesetz und anschließend das De Morgansche Gesetz anzuwenden.

Z0

=

X 0 Y0

=

X 0 Y0

Technische Informatik I

99

7. Methoden zur Analyse und Synthese

Z1

=

X 1 X 0 Y1 + X 0 Y1 Y0 + X 1 X 0 Y0 + X 1 Y1 Y0

=

X 1 X 0 Y1 + X 0 Y1 Y0 + X 1 X 0 Y0 + X 1 Y1 Y0

=

X 1 X 0 Y1 · X 0 Y1 Y0 · X 1 X 0 Y0 · X 1 Y1 Y0

Z2

=

X 1 X 0 Y1 + X 1 Y1 Y0

=

X 1 X 0 Y1 + X 1 Y1 Y0

=

X 1 X 0 Y1 · X 1 Y1 Y0

Z3

=

X 1 X 0 Y1 Y0

=

X 1 X 0 Y1 Y0

Das entsprechende Schaltnetz zeigt die Abbildung 7.5. Analog könnte man die Schaltung auch nur aus NOR-Gattern bauen.

Abbildung 7.5.: Schaltnetz eines 2-Bit-Multiplizierers aus NAND-Gattern

100

Technische Informatik I

7.2. Minimierung von Schaltfunktionen mit Karnaugh-Diagrammen

7.1.2. Assoziativität von NAND und NOR Das Assoziativgesetz gilt bei NAND- und NOR-Operationen nicht, daher ist der Logikentwurf mit NAND und NOR nicht so intuitiv wie mit UND und ODER. Die Abbildungen 7.6 und 7.7 zeigen diesen Sachverhalt anhand der bereits vorgestellten Symbole.

Abbildung 7.6.: Mögliche Realisierung eines Drei-Eingänge-UND-Gatters mit ZweiEingänge-UND-Gattern

Abbildung 7.7.: Mögliche Realisierung eines Drei-Eingänge-NAND-Gatters mit ZweiEingänge-NAND-Gattern ¡

¢

Formal ausgedrückt: Es gilt x y z = x y z (Assoziativitätsgesetz). Bei NANDs gilt aber ¡

³

¢

´

nicht x y z = x y z (formal betrachtet ist der Fehler offensichtlich), sondern x y z = x y z .

7.1.3. Zusammenfassung boolesche Algebra Schaltfunktionen sind ein logischer Formalismus zur Beschreibung von Schaltnetzen. Schaltnetze sind einfacher und kostengünstiger zu implementieren, wenn sie aus weniger Gattern und Eingängen bestehen. Aus Effizienzgründen ist es daher ratsam, Schaltfunktionen zunächst zu minimieren, bevor sie in Hardware umgesetzt werden. Ein Weg der Vereinfachung von Schaltfunktionen ist die algebraische Minimierung mithilfe der booleschen Algebra.

7.2. Minimierung von Schaltfunktionen mit Karnaugh-Diagrammen Es ist nicht immer leicht ersichtlich, welches boolesche Gesetz als nächstes anzuwenden ist, um einen Ausdruck minimal werden zu lassen. Hinzu kommt, dass uninteressante Eingangsbelegungen nicht vernachlässigt werden und in das Minimierungsergebnis einfließen (selbst wenn sie beispielsweise nie auftreten).

Technische Informatik I

101

7. Methoden zur Analyse und Synthese Eine (gerade für den Menschen) einfache Alternative ist das nach seinem Entwickler benannte Karnaugh-Diagramm. Dieses ist auch in der Lage, einen Nutzen aus nicht vorkommenden Eingangsbelegungen zu ziehen. Darüber existieren algorithmische Verfahren, die mit beliebig vielen Variablen auskommen, z. B. das Quine-McCluskey-Verfahren. Diese werden in diesem Skript jedoch nicht behandelt.

7.2.1. Karnaugh-Diagramme Das Aufstellen von Karnaugh-Diagrammen ist eine grafische Technik zur Darstellung und Vereinfachung von booleschen Funktionen, die höchstens vier unterschiedliche Variablen enthalten. Prinzipiell ist diese Diagrammform eine zweidimensionale Darstellung von Wahrheitstabellen, wobei jede Zelle des Diagramms genau einen Minterm repräsentiert. Karnaugh-Diagramme (eigentlich Karnaugh-Veitch-Diagramme) wurden 1952 von Edward W. Veitch entworfen. Ein Jahr darauf brachte Maurice Karnaugh sie in die aktuelle Form. Benachbarte Zellen eines Karnaugh-Diagramms unterscheiden sich in nur einer Variablen. Daher sind bei Diagrammen ab drei Eingangsvariablen die 11- und 10-Spalte bzw. -Zeile vertauscht. (Man könnte auch sagen: Es findet ein Zwei-Bit-Grey-Code Anwendung). Ein Beispiel dazu ist in Abbildung 7.8 gegeben.

Abbildung 7.8.: Karnaugh-Diagramm-Beispiele Benachbarte Einsen lassen sich zu einem kleineren Produktterm vereinfachen. (Hierbei handelt es sich um eine Anwendung des Distributiv- und des Komplementgesetzes). Man beachte, dass Karnaugh-Diagramme geometrisch einen Torus repräsentieren, also auch die oberste Zeile zur Untersten benachbart ist, genauso wie die Spalte links außen zu der rechts außen. Weiterhin kann eine Zelle aufgrund des Idempotenzgesetzes zu mehreren Termen gehören. Man kann nur immer 2n (d. h. 1, 2, 4, 8, . . .) Einsen zusammenfassen. In Abbildung 7.8 rechts sind die möglichen Einser-Gruppen eingekreist. Dabei ist zu beachten, dass der entstehende Term dann minimal ist, wenn die größtmögliche Einsergruppe jeweils zusammengefasst wird und jede Eins am Ende erfasst ist. Es gelten folgende Zusammenhänge in einem Karnaugh-Diagramm mit vier Eingangsvariablen: • Ein Produktterm mit 1 Variablen überdeckt 8 Zellen • Ein Produktterm mit 2 Variablen überdeckt 4 Zellen

102

Technische Informatik I

7.2. Minimierung von Schaltfunktionen mit Karnaugh-Diagrammen • Ein Produktterm mit 3 Variablen überdeckt 2 Zellen • Ein Produktterm mit 4 Variablen überdeckt 1 Zelle Wenn wir uns zum Beispiel die dritte Zeile des rechten Diagramms in Abbildung ³ 7.8 an´ sehen, lassen sich die beiden Einsen so zusammenfassen: A BC D + ABC D = AC D B + B = AC D

Diese Formel braucht nicht aufgestellt zu werden, da man direkt im Diagramm sieht, dass das Ergebnis unabhängig vom Wert von B ist. Den vereinfachten Term kann man also direkt ablesen. Die anderen Terme, die man dem Beispiel entnehmen kann, sind AB und AB C D . Bevor wir uns nun das Minimieren mit Karnaugh-Diagrammen näher ansehen werden, werfen wir einen Blick darauf, wie man boolesche Ausdrücke auf diese Diagramme überträgt. Als Beispielfunktion verwenden wir F1 = x 1 x 2 + x 3 x 4 . Das dazugehörige KarnaughDiagramm ist in Abbildung 7.9a) dargestellt.

a)

b)

Abbildung 7.9.: Beispiel-Karnaugh-Diagramm: a) Funktion F1 , b) Funktion F2 Der einschlägige Minterm x 1 x 2 x 3 x 4 ist sowohl durch die Konjuktion x 1 x 2 als auch durch x 3 x 4 erfasst. Die Überlappung führt aufgrund der Idempotenz aber zu keinerlei Auswirkungen. Ein weiteres Beispiel stellt die Funktion F2 = x 2 x 4 + x 1 x 3 x 4 dar, bei der die Toruseigenschaft des Karnaugh-Diagramms eine Rolle spielt: Die vier Ecken können zusammengefasst werden, weil sie Nachbarn sind. Das Diagramm findet sich in Abbildung 7.9b). Das Minimieren mit Karnaugh-Diagrammen gestaltet sich einfacher als die Verwendung der Gesetze der booleschen Algebra. Betrachten wir die folgende boolesche Funktion: f = x 1 x 2 x 3 + x 1 x 2 x 3 + x 1 x 2 x 3 + x 1 x 2 x 3 + x 1 x 2 x 3 . Das entsprechende Karnaugh-Diagramm zeigt Abbildung 7.10. Aus der Abbildung lässt sich der minimierte Term f = x 2 + x 1 x 3 ablesen. Die Funktion wird also dann 1, wenn x 2 = 0, oder wenn x 1 = 0 und x 3 = 0. Wollte man diese Vereinfachung formal nachvollziehen, würde das etwa so aussehen (in der zweiten Zeile wurde der erste Minterm verdoppelt, was durch das Idempotenzgesetz möglich ist):

Technische Informatik I

103

7. Methoden zur Analyse und Synthese

Abbildung 7.10.: Vereinfachung einer Funktion mittels Karnaugh

f

=

x1 x2 x3 + x1 x2 x3 + x1 x2 x3 + x1 x2 x3 + x1 x2 x3

=

x1 x2 x3 + x1 x2 x3 + x1 x2 x3 + x1 x2 x3 + x1 x2 x3 + x1 x2 x3 ¡ ¢ ¡ ¢ x1 x2 x3 + x3 + x1 x2 x3 + x1 x2 x3 + x1 x2 x3 + x3 ¡ ¢ x1 x2 + x1 x3 x2 + x2 + x1 x2

= = =

x1 x2 + x1 x2 + x1 x3 ¡ ¢ x2 x1 + x1 + x1 x3

=

x2 + x1 x3

=

An diesem Beispiel kann man gut sehen, dass mit Karnaugh-Diagrammen eine Vereinfachung erheblich einfacher ist als mit formalen Umformungen. In Abbildung 7.11 sind weitere Beispiele zum Verständnis abgebildet.

Abbildung 7.11.: Karnaugh-Beispiele Beim Vereinfachen strebt man immer eine Überdeckung aller Einsen mit minimaler Anzahl von Gruppen an. Weiterhin wählt man immer Gruppen maximaler Größe (auch mit bereits durch andere Gruppen involvierte Zellen). Es gilt, dass es mehrere minimale Lösungen geben kann (vergleiche Abbildung 7.12).

104

Technische Informatik I

7.2. Minimierung von Schaltfunktionen mit Karnaugh-Diagrammen

Abbildung 7.12.: Unterschiedliche minimale Ergebnisse der gleichen Funktion

7.2.2. Don’t-care-Zustände Es gibt Schaltnetze, bei denen bestimmte Eingabebelegungen von vornherein ausgeschlossen werden können. Die entsprechenden Ausgabebelegungen sind in diesem Fall egal, man braucht sich nicht um sie zu kümmern („Don’t care“). Besser noch: Man kann diese Zustände bei der Minimierung so verwenden, wie man sie gerade braucht: Steht in einem benachbarten Feld eine 1, so kann die Zelle mit dem Don’t-Care-Zustand wie eine 1 behandelt werden. Befindet sich dagegen ein Don’t-Care-Zustand fern jeglicher Einsen, braucht man ihn nicht bei der Minimierung zu berücksichtigen. In diesem Fall entspricht der zugewiesene Inhalt einer 0. Es wird ein drittes Symbol eingeführt, um diese Zustände im Karnaugh-Diagramm (und der entsprechenden Wahrheitstabelle) zu repräsentieren: d. Der Sachverhalt wird an folgendem Beispiel vorgeführt: Es soll eine Schaltung für eine automatische Klimaanlagensteuerung entwickelt werden. Die Klimaanlage soll so gesteuert werden, dass auf die unabhängigen Außenparameter Temperatur und Luftfeuchtigkeit reagiert wird. Dabei bekommt unsere Steuerung nur mitgeteilt, ob es heiß (H ), kalt (K ), feucht (F ) oder trocken (T ) ist. Die genauen Parameter sind in Tabelle 7.4 abgebildet. Eingabevariable

wenn 0

wenn 1

Heiß

< 22 °C

> 22 °C

Kalt

> 15 °C

< 15 °C

Feucht

< 75 %

> 75 %

Trocken

> 40 %

< 40 %

Tabelle 7.4.: Eingabeparameter der Klimaanlagensteuerung Die Klimaanlage besteht aus vier Modulen: Einem Kaltlufterzeuger, einem Warmlufterzeuger, einem Ent- und einem Befeuchter. Diese können jeweils entweder ein- oder ausgeschaltet werden. Wir modellieren diese Schalter als P , Q , R und S (Tabelle 7.5). In einer Wahrheitstabelle können wir nun die Abhängkeiten der Ausgabeparameter von den Eingangsvariablen darstellen (Tabelle 7.6). Einige Eingangsbelegungen sind unmöglich (es kann z. B. nicht gleichzeitig heiß und kalt sein). Außerdem nimmt die relative

Technische Informatik I

105

7. Methoden zur Analyse und Synthese Ausgabe

wenn 0

wenn 1

P

Kaltlufterzeuger aus

Kaltlufterzeuger an

Q

Warmlufterzeuger aus

Warmlufterzeuger an

R

Entfeuchter aus

Entfeuchter an

S

Befeuchter aus

Befeuchter an

Tabelle 7.5.: Ausgabeparameter der Klimaanlagensteuerung Luftfeuchte ab, wenn die Luft erwärmt wird. Deshalb muss kalte und feuchte Luft nicht extra entfeuchtet werden. Umgekehrt muss heiße und trockene Luft zunächst nicht befeuchtet werden, da die relative Feuchte beim Abkühlen steigt. H

K

F

T

Bedeutung

P

Q

R

S

0

0

0

0

neutral

0

0

0

0

0

0

0

1

trocken

0

0

0

1

0

0

1

0

feucht

0

0

1

0

0

0

1

1

unmöglich

d

d

d

d

0

1

0

0

kalt

0

1

0

0

0

1

0

1

kalt/trocken

0

1

0

1

0

1

1

0

kalt/feucht

0

1

0

0

0

1

1

1

unmöglich

d

d

d

d

1

0

0

0

heiß

1

0

0

0

1

0

0

1

heiß/trocken

1

0

0

0

1

0

1

0

heiß/feucht

1

0

1

0

1

0

1

1

unmöglich

d

d

d

d

1

1

0

0

unmöglich

d

d

d

d

1

1

0

1

unmöglich

d

d

d

d

1

1

1

0

unmöglich

d

d

d

d

1

1

1

1

unmöglich

d

d

d

d

Tabelle 7.6.: Wahrheitstabelle der Klimaanlagensteuerung Beim Minimieren per Karnaugh-Diagramm (Abbildung 7.13 und 7.14) lassen sich die Don’t-Care-Zustände wie Einsen behandeln, wodurch größere Gruppen ermöglicht werden, bei denen die Einsen zusammengefasst werden. Die Don’t-Care-Zustände werden so verwendet, dass sich möglichst große und wenige Blöcke ergeben, die alle Einsen einschließen.

7.3. Zusammenfassung Es wurde gezeigt, wie sich kombinatorische Schaltnetze bzw. Schaltungen mit einem bestimmten gewünschten Verhalten auf der Basis von Gattern mit folgenden Schritten realisieren lassen: • Das gewünschte Verhalten wird in Form einer Wahrheitstabelle beschrieben. • Aus dieser Tabelle wird durch Anwendung von algebraischen oder grafischen Methoden eine Schaltfunktion daraus abgeleitet.

106

Technische Informatik I

7.3. Zusammenfassung Kaltlufterzeuger P

Warmlufterzeuger Q

P =H

Q =K

Bei Hitze Kaltluft erzeugen

Bei Kälte Warmluft erzeugen

Abbildung 7.13.: Karnaugh-Diagramme zur Klimaanlagensteuerung (Temperatur) Entfeuchter R

Befeuchter S

R =KF

S = HT

Wenn feucht und nicht kalt, entfeuchten

Wenn trocken und nicht heiß, befeuchten

Abbildung 7.14.: Karnaugh-Diagramme zur Klimaanlagensteuerung (Luftfeuchtigkeit) • Schließlich wandelt man die Schaltfunktion in eine Schaltung um, indem die Operatoren in entsprechende Gatter abgebildet werden, deren Eingänge den Variablen entsprechen, die die Operatoren verwenden.

Technische Informatik I

107

8. Sequenzielle Schaltnetze Jede Schaltung, die uns bis hierhin begegnet ist, war eine kombinatorische Schaltung, deren Ausgabe nur eine Funktion ihrer Eingabewerte war. Das heißt, dass wir mit Kenntnis der Eingabewerte und der booleschen Funktion einer kombinatorischen Schaltung jederzeit ihre Ausgabe berechnen können. Schaltungen, deren Ausgaben nicht nur von ihren momentanen, sondern auch von ihren vergangenen Eingabewerten abhängen, nennt man sequentielle Schaltungen. Selbst wenn die berechnete boolesche Funktion einer solchen sequentiellen Schaltung bekannt ist, lassen sich ihre Ausgabewerte nur mit Kenntnis ihrer vorherigen Eingaben (d. h. der vorherigen internen Zustände) ermitteln. Wesentliche Elemente einer CPU wie Register, Zähler oder Schieberegister werden durch sequentielle Schaltungen realisiert. In diesem Kapitel werden daher einige grundlegende sequentielle Schaltungen behandelt. Die Grundeinheit sequentieller Schaltungen ist ein Flipflop, so wie die Grundeinheit eines kombinatorischen Schaltnetzes das Gatter ist. Ein Flipflop ist eine bistabile Kippstufe, da der Ausgangswert für eine bekannte Eingabe unbestimmt einen von zwei stabilen Zuständen hat. Das heißt, der Ausgangswert kann entweder den Wert 0 oder den Wert 1 annehmen, der aktuelle Zustand hängt dabei von den vorherigen Eingaben ab. Meist handelt es sich um zwei Ausgänge, wobei der zweite in der Regel das Komplement des ersten ist, siehe schematische Darstellung in Abbildung 8.1. Gewissermaßen hat eine solche Schaltung ein Gedächtnis und ist somit eine Art von Speicherelement. Ein Flipflop ist die kleinstmögliche Speicherzelle und speichert lediglich ein Bit.

Abbildung 8.1.: Flipflop in Black-Box-Darstellung Der Begriff Flipflop leitet sich aus dem Geräusch ab, das solche frühe Bauelemente – die allesamt durch elektromagnetische Relais realisiert wurden – beim Wechseln ihres Zustandes machten.

8.1. RS-Flipflop Beginnen wir die Betrachtung der Flipflops mit dem einfachsten Mitglied dieser Familie, dem RS-Flipflop. Die Abbildung 8.2 zeigt ein solches Flipflop aus NOR-Gattern. Obwohl die Schaltung nicht mehr umfasst als zwei NOR-Gatter mit jeweils zwei Eingängen, ist die Arbeitsweise nicht sofort ersichtlich. Aus diesem Grund soll sie im Folgenden schrittweise hergeleitet werden.

109

8. Sequenzielle Schaltnetze

Abbildung 8.2.: RS-Flipflop aus NOR-Gattern Die Schaltung besitzt zwei Eingänge ( A und B ) und zwei Ausgänge ( X und Y ). Mit unseren Kenntnissen der booleschen Algebra lassen sich für die Ausgänge schnell Ausdrücke mit Termen in Abhängigkeit von den Eingängen finden:

X

=

Y

= B +X

A +Y

Wenn wir die untere in die obere Gleichung einsetzen, erhalten wir: X = A + B + X = A · B + X = A (B + X ) = AB + AX

Da die boolesche Algebra keine Operationen der Division oder Subtraktion bietet, um X komplett auf eine Seite zu bringen, ist die Bedeutung der Formel nicht direkt klar. Wir müssen uns einen anderen Weg suchen, um das Verhalten so vernetzter Gatter zu untersuchen. Es ist hilfreich, sich zunächst einen beliebigen Anfangswert für X und die Eingänge zu denken, von denen ausgegangen wird. Wir wählen hier die folgende Werte: X = 1, A = B = 0. Die Eingänge des Gatters G 2 sind X = 1, B = 0; sein Ausgang Y muss demzufolge 0 sein (vgl. Wahrheitswerte für NOR in Tabelle 6.1). Analog ist der Ausgang X des Gatters G 1 mit Y = 0 und A = 0 berechenbar, das Ergebnis ist 1. Offenbar sorgt die Eingangsbelegung dafür, dass die Ausgangsbelegung stabil ist. Diesen Zustand nennt man selbsterhaltend. Der Ausgang des Gatters G 1 ist 1, welcher wieder als Eingang von G 1 zurückgeführt wird, um X im Zustand 1 zu halten. Solch eine Schaltung wird kreuzweise gekoppelt (cross-coupled) genannt, da die Ausgänge gleichzeitig Eingaben der ODER-Gatter sind. Damit erklärt sich auch, warum in der oben hergeleiteten Formel X auf beiden Seiten der Gleichung auftaucht. Nehmen wir für X = 0 und für A = B = 0 an, ist der folgender Gedankengang möglich: Die Eingänge für G 2 sind X = 0 und B = 0, woraus die Ausgabe Y = 1 resultiert. Die Eingänge für G 1 wären somit Y = 1 und A = 0, was wieder X = 0 zur Folge hätte. Wir beobachten also, dass die Schaltung auch mit dieser Eingangsbelegung selbsterhaltend ist. Interessant zu beobachten ist, dass die Eingabeparameter mit A = B = 0 offenbar keinen Einfluss auf die Ausgänge haben; X kann entweder 0 oder 1 sein und Y ist das entsprechende Komplement. Stattdessen sind X und Y vom vorherigen internen Zustand abhängig. Der nächste Schritt der Untersuchung ist es, das Verhalten bei der Änderung der Eingänge A und B zu betrachten. Dafür nehmen wir wieder an, X habe zu Beginn den Zustand 1 und A = B = 0.

110

Technische Informatik I

8.1. RS-Flipflop Nun schaltet der Eingang B des Gatters G 2 auf 1. G 2 , welches nur eine 1 ausgibt, wenn kein Eingang aktiv ist, gibt bereits eine 0 aus, da X = 1. Der Ausgang Y ändert sich also nicht. Damit ändert sich auch nichts an der Eingangsbelegung des Gatters G 1 . Das Setzen des Eingangs B ändert also auf Ausgangsseite nichts. Für den nächsten Gedankengang schalten wir B zunächst auf 0 zurück, sodass die alte Konfiguration ( X = 1, A = B = 0) wieder hergestellt ist. Wir setzen jetzt A auf 1 (B bleibt auf 0). G 1 , das wie G 2 nur dann eine 1 ausgibt, wenn kein Eingang aktiv ist, schaltet den Ausgang X von 1 auf 0. Dadurch werden beide Eingänge von G 2 0, wodurch der Ausgang von G 2 , Y , 1 wird. G 1 ändert sich dadurch nicht mehr, da ja bereits eine 1 anliegt. Setzt man anschließend A zurück auf 0, bleibt der Ausgangszustand (Y = 1, X = 0) erhalten. Diesen Sachverhalt haben wir eingangs bereits betrachtet. Man kann das Setzen von A folglich als Setzen von Y oder als Rücksetzen von X auffassen. Es wurde bereits angemerkt, dass sich der Zustand des Flipflops als Funktion des aktuellen Zustandes beschreiben lässt ( X = AB + AX ). Die Zustandstabelle (Tabelle 8.1) umgeht dieses Problem, indem sie eine neue Variable X ∗ einführt, wobei X ∗ der neue Wert zur alten Ausgabe X und den dazugehörigen Eingängen A und B ist. So lässt sich auch die Gleichung zu X ∗ = AB + AX umschreiben. Die Spalten der Wahrheitstabellen sind nun nicht mehr nur räumlich, sondern auch zeitlich getrennt. Die momentane Ausgabe X wird mit A und B kombiniert, um X ∗ zu generieren. Der Wert X wird damit überschrieben. Wir haben bislang nur die Zeilen 1, 2, 3 und 5 der Tabelle Tabelle 8.1 beschrieben. Die übrigen Zustände durchzugehen bleibt dem Leser als Übung überlassen. A

B

X

X∗

0 0 0 0 1 1 1 1

0 0 1 1 0 0 1 1

0 1 0 1 0 1 0 1

0 1 1 1 0 0 0 0

Tabelle 8.1.: Zustandstabelle für Abbildung 8.2 Die Schaltung wird RS-Flipflop genannt. RS steht dabei für Reset und Set (Rücksetzen und Setzen). Das sind die Namen der Eingänge, wobei wir A als Reset-Eingang und B als Set-Eingang betrachten. Der Ausgang eines RS-Flipflops heißt traditionell Q (vormals X ) und dessen Komplement Q (vormals Y ). Das Schaltzeichen eines RS-Flipflops ist in Abbildung 8.3 dargestellt. Tabelle 8.2 ergänzt Tabelle 8.1 um Eingangs- und Ausgangsbezeichnungen. Außerdem lassen sich die Eingangsbelegungen trotz unterschiedlichem Ausgang gruppieren. Da der zweite Ausgang nur das Komplement des ersten ist, verzichten viele Hersteller auf das Anbinden des zweiten Ausgangs an die Außenseite des Chips und überlassen dem Anwender nur Q .

Technische Informatik I

111

8. Sequenzielle Schaltnetze

Abbildung 8.3.: Schaltzeichen für ein RS-Flipflop R

S

Q

Q∗

0 0 0 0 1 1 1 1

0 0 1 1 0 0 1 1

0 1 0 1 0 1 0 1

0 1 1 1 0 0 X X

Keine Änderung Q setzen Q rücksetzen verboten

Tabelle 8.2.: Zustandstabelle für RS-Flipflop, Q ∗ ist das neue Ausgangssignal Eine Sonderstellung hat die Eingangsbelegung R = S = 1 (bzw. A = B = 1) inne. Für die Speicherfähigkeit und Veränderbarkeit des Flipflops – der grundlegenden Eigenschaft – werden nur drei Zustände benötigt: Speichern (keine Änderung; R = S = 0), Setzen (R = 0, S = 1) und Rücksetzen (R = 1, S = 0). Der vierte Zustand R = S = 1ist über (man kann nicht gleichzeitig setzen und rücksetzen). Da sich in diesem Zustand ein RS-Flipflop aus NOR-Gattern anders verhält als eins aus NANDs, bezeichnen wir diese Eingangsbelegung als “verboten”. Bei NOR-Gattern fallen beide Ausgänge auf 0. Schalten beide Eingänge gleichzeitig auf 0, ist unklar, welchen Wert Q annimmt. Der Wert ist in diesem Fall sogar von den physikalischen Bauteilen abhängig. In der Praxis versucht man daher, den Zustand R = S = 1 zu vermeiden (zumal wenn unbekannt ist, wie das Flipflop intern aufgebaut ist. Die Wahrheitstabelle oben war insofern umständlich, dass jeweils zwei Zeilen für jede Belegung der Eingänge nötig waren, eine für Q = 0 und eine für Q = 1. Einen alternativen Ansatz zeigt Tabelle 8.3, die nur den algebraischen Wert zeigt. Hierbei bedeutet X als Symbol entweder „nicht definiert“ (d. h. kann nicht berechnet werden) oder das bekannte „Don’t care“. R

S

0 0 1 1

0 1 0 1

Q Q

Q Q

1 0 X

0 1 X

Tabelle 8.3.: Zustandsfolgetabelle für RS-Flipflop RS-Flipflops lassen sich auch mit NAND-Gattern realisieren. Diese Version ist low-active, d. h. die logische 1 wird elektrisch durch einen niedrigen Spannungspegel dargestellt und die 0 durch einen hohen Spannungspegel. In der Praxis ist dies ein häufig anzutreffender Modus (negative Logik), weshalb NAND-RS-Flipflops gängig sind.

112

Technische Informatik I

8.1. RS-Flipflop

8.1.1. Beispiel Impulsfolgengenerator Eine einfache Anwendung von RS-Flipflops ist der Impulsfolgengenerator, wie ihn Abbildung 8.4 zeigt. Er erzeugt bei jeder Triggerung (Aktivierung) eine Sequenz von n Impulsen. Der Wert von n wird dabei vom Anwender festgelegt und durch Schalter in die Schaltung überführt. In dieser Anwendung des RS-Flipflop dient S dazu, die Ausgabe der Impulsfolge zu starten, und R dazu, sie zu stoppen. Nehmen wir zu Beginn an, dass die Eingänge des Flipflops R und S jeweils 0 sind und dass der Ausgang Q im Zustand 0 ist.

Abbildung 8.4.: Impulsfolgengenerator

Wenn eine 1 auf den Eingang S (d. h. Start) des Flipflops gelegt wird, steigt Q auf 1 und aktiviert das UND-Gatter G 1 . Eine Folge von Takt-Impulsen am zweiten Eingang von G 1 wird nun auf den Ausgang des UND abgebildet. Diese Impulsfolge dient nun als Eingabe eines Zählers (den wir später noch betrachten werden), welcher die Impulse zählt und einen Drei-Bit-Ausgabewert (Wertebereich 0–7) entsprechend der gezählten Impulse an den Ausgängen Q A , Q B und QC ausgibt. Der Ausgabewert wird über XOR-Gatter (G 2 – G 4 ) mit einem vom Anwender festgelegten Drei-Bit-Wert an C A , C B und CC verglichen. Anfangs ist der Zähler auf Null gesetzt (Q A = Q B = QC = 0). Sobald er beginnt hochzuzählen, vergleichen die XOR-Gatter den Zählerstand mit den festgelegten Werten C A , C B und CC . Sollten die Werte gleich sein, steigt der Ausgang des Gatters G 5 auf 1 und setzt das RS-Flipflop zurück. Dies hat den Effekt, dass sowohl der Zähler als auch das Flipflop zurückgesetzt werden. Durch das Rücksetzen des Flipflops sperrt das UND-Gatter G 1 und am Ausgang der Schaltung und am Zähler kommen keine Impulse mehr an.

Technische Informatik I

113

8. Sequenzielle Schaltnetze

8.2. Getaktete Flipflops Das in Abbildung 8.2 gezeigte Flipflop reagiert sofort auf Änderung der Eingangssignale. Es gibt aber auch Situationen, in denen wir ein RS-Flipflop dazu bringen wollen, bis zu einem bestimmten Zeitpunkt seine Eingänge zu ignorieren.

Abbildung 8.5.: Schaltverzögerungen Abbildung 8.5 demonstriert den Effekt von Schaltverzögerungen eines Systems. Zwei Eingänge A und B werden von zwei Prozessen A und B (der Begriff Prozess steht hier für eine sequentielle Schaltung) genutzt, um jeweils ein Signal zu erzeugen, welches als Eingabe Prozess C zugeführt wird. Die Art der Prozesse spielt im Moment keine Rolle, da wir nur daran interessiert sind, inwiefern sie die Signale verzögern. Stellen wir uns vor, dass zum Zeitpunkt t = 0 die Eingänge der Prozesse A und B gültig werden (d. h. es sind von da an die korrekten Werte, die durch den Prozess verarbeitet werden). Nehmen wir weiterhin an, dass der Prozess A eine Verzögerung von zwei Zeiteinheiten und der Prozess B eine Verzögerung von einer Zeiteinheit verursacht. Wie groß diese Zeiteinheit ist, spielt keine Rolle. Die Ausgaben von A und B dienen Prozess C als Eingabe. Auch C verursacht eine Verzögerung von zwei Zeiteinheiten. Offensichtlich ist die Ausgabe von C erst vier Zeiteinheiten nach t = 0 gültig. Der Ausgabewert von C ändert sich in der Zeit von t = 0 bis t = 4 mindestens einmal, nämlich wenn der Prozess B bereits den korrekten Wert am Ausgang ausgibt, A aber noch nicht. Erst ab t = 4 können wir davon ausgehen, dass C den korrekten Wert ausgibt. Das stellt ein Problem dar: Woher weiß ein Anwender, wann er mit den Ausgabewerten von C arbeiten kann? Eine Problemlösung wäre es, den Zugriff auf das Ausgangssignal von Prozess C erst nach vier Zeiteinheiten zuzulassen. Dies erreicht man, indem man das Signal zusammen mit einem „Erlaubnissignal“ als Eingabe in ein UND-Gatter leitet. Dessen Ausgabe ist das neue Ausgangssignal des Prozesses C. Erst, wenn das „Erlaubnissignal“ auf 1 steht, wird das Ergebnis von C zum Ausgang durchgeschaltet. Wir werden auf dieses Anwendungsbeispiel zurückkommen. Die Schaltung der Abbildung 8.6 zeigt, wie man dieses Prinzip nutzen kann, um ungewollte Änderungen in einem RS-Flipflop zu unterbinden. Die innere Box in der Abbildung enthält ein gewöhnliches RS-Flipflop. Dessen Eingänge R 0 und S 0 werden durch UND-Gatter beschaltet, die die externen Eingänge R und S mit einem Takt-Eingang C (Clock) verknüpfen. Solange C = 0 gilt, bleiben beide Eingänge des RS-Flipflops auf 0. Dadurch speichert das (innere) Flipflop; der Ausgang des Flipflops bleibt also konstant.

114

Technische Informatik I

8.3. D-Flipflop

Abbildung 8.6.: Getaktetes RS-Flipflop Wann immer C = 1 gilt, werden die Eingänge R und S zum Flipflop „durchgestellt“, sodass R 0 = R und S 0 = S gilt. Man kann sich als den Takt-Eingang als Hemmer vorstellen, der das Flipflop daran hindert, vor einem bestimmten Zeitpunkt seinen Zustand zu verändern. Wenn wir nachfolgend den Takt von 0 auf 1 ändern, sprechen wir vom Takten (z. B. des Flipflops).

8.3. D-Flipflop Eine etwas andere Herangehensweise wird mit dem D-Flipflop geschaffen: Es ist taktgesteuert und hat neben dem Takteingang C (clock input) nur einen Dateneingang (D ). Solange C = 0 ist, hat eine Änderung des Dateneingangs keine Auswirkung. Erst wenn C gesetzt wird, wird der Dateneingang übernommen und zum Ausgangssignal Q „durchgestellt“. Das D-Flipflop gibt es folglich auch nur in der getakteten Version. Das D-Flipflop wird verwendet, um ein Bit zu halten bzw. um eine Übernahme zu verzögern (delay). Es wird daher auch als Verzögerungselement bezeichnet, als „Zustandsbewahrer“ (staticizer). Das Verhalten des D-Flipflops lässt sich wieder durch Zustandstabelle (Tabelle 8.4) und Zustandsfolgetabelle (Tabelle 8.5) darstellen. C

D

Q

Q∗

0 0 0 0 1 1 1 1

0 0 1 1 0 0 1 1

0 1 0 1 0 1 0 1

0 1 0 1 0 0 1 1

Q bleibt

Q∗ = D

Tabelle 8.4.: Zustandstabelle des D-Flipflops Eine Möglichkeit, wie ein D-Flipflop intern aufgebaut ist, zeigt Abbildung 8.7. Es besteht aus einem RS-Flipflop und einigen zusätzlichen Gattern. Die Aufgabe der UND-Gatter ist es, das RS-Flipflop mit einer Taktung zu versehen. So lange C = 0 gilt, sind R und S ebenfalls 0 und Q verändert sich nicht. Wenn C auf 1 steigt, wird S zu D verbunden und R zu D .

Technische Informatik I

115

8. Sequenzielle Schaltnetze C

D

0 0 1 1

0 1 0 1

Q Q Q

Q Q Q

0 1

1 0

Tabelle 8.5.: Zustandsfolgetabelle des D-Flipflops

Abbildung 8.7.: Getaktetes D-Flipflop

8.3.1. Beispiel m-Bit-Datenbus Eine typische Anwendung von D-Flipflops ist der Anschluss an einen m Bit breiten Datenbus, der zur Datenübertragung in einem digitalen System eingesetzt wird (Abbildung 8.8). Die Daten auf dem Bus ändern sich andauernd, da verschiedene Geräte ihn benutzen, um Daten von einem Register ins nächste zu übermitteln.

Abbildung 8.8.: Auslesen eines m-Bit-Datenbusses mit D-Flipflops Die D-Eingänge einer Gruppe von m D-Flipflops sind mit den m Leitungen des Busses verbunden. Die Takteingänge C hingegen sind alle miteinander verbunden, womit sich alle Flipflops gleichzeitig takten lassen. So lange C = 0 gilt, ignorieren die Flipflops die

116

Technische Informatik I

8.3. D-Flipflop Daten auf dem Bus und ihre Ausgänge Q ändern ihre Werte nicht. Wenn nun Geräte ihre Daten zu den Flipflops übermitteln wollen, legen sie diese auf den Bus und takten die Flipflops, wodurch die Daten in sie überführt werden. C fällt danach auf 0 zurück und die Daten werden in den Flipflops „eingefroren“. Wie wir später sehen werden, besteht ein Computer eigentlich aus kaum mehr Komponenten, als aus den bisher vorgestellten kombinatorischen Logikbausteinen, Bussystemen und Gruppen von Flipflops, welche Daten von und zu den Bussystemen lesen und schreiben. Wenden wir uns wieder der Frage der zeitlichen Verzögerung zu (Abbildung 8.5). Wenn ein D-Flipflop am Ausgang von Prozess C angebracht und z. B. vier Zeiteinheiten nach dem Zeitpunkt t = 0 getaktet wird, werden die gewünschten Daten in das Flipflop übertragen und bis zum nächsten Taktimpuls beibehalten. Getaktete Systeme halten also digitale Information mittels Flipflops konstant, während mit der Information durch Gruppen logischer Bausteine gearbeitet wird, analog zu den Prozessen in der Abbildung. Zwischen den einzelnen Taktimpulsen werden die Ausgaben der Flipflops von den Logikelementen verarbeitet und die neuen Werte werden den Eingängen der Flipflops zur Verfügung gestellt. Nach einer gewissen Zeitverzögerung (mindestens so lang wie der langsamste Prozess bis zu seinem Ende benötigt), werden die Flipflops getaktet. Die Ausgaben werden konstant gehalten, bis die Flipflops das nächste Mal getaktet werden. Ein getaktetes System heißt synchron (zeitgesteuert, taktgesteuert), da alle Prozesse gleichzeitig bei jedem neuen Taktimpuls gestartet werden. In einem asynchronen (eigengesteuerten) System ist das Ende des einen Prozesses der Start des nächsten. Wie man sich leicht denken kann, ist ein asynchrones System schneller als seine synchrone Entsprechung. Asynchrone Systeme sind dabei aber auch viel komplexer und schwieriger zu entwerfen als synchrone Systeme. Aus der praktischen Erfahrung weiß man, dass man sie am besten vermeiden sollte, da sie in sich selbst weniger zuverlässig sind als synchrone Schaltungen. Die Auswirkungen, eines D-Flipflop mit dem Ausgang des Prozesses C zu verbinden, zeigt Abbildung 8.9. Neue Daten werden dort nur bei einem Taktimpuls übernommen.

Abbildung 8.9.: Getaktete Schaltverzögerung Nun ist es natürlich auch möglich, jeden der Prozesse A, B und C mit einem D-Flipflop zu versehen. Jedes dieser Flipflops wird gleichzeitig alle zwei Zeiteinheiten getaktet. In diesem Fall wird eine neue Eingabe alle zwei Zeiteinheiten übernommen, anstatt nur alle vier, wie oben. Das Geheimnis in der Steigerung unserer Durchsatzrate heißt Pipelining, da an den unterschiedlichen Stufen der Verarbeitungskette auch mit unterschiedlichen Daten gearbeitet wird. So ist die Ausgabe am Ausgang des Prozesses C abhängig vom Datensatz i − 2. Prozess C verarbeitet die Daten i − 1 und Prozesse A und B verarbeiten die Daten i .

Technische Informatik I

117

8. Sequenzielle Schaltnetze Man stelle sich dies vielleicht ähnlich wie ein Fließband vor. An den Punkten A und B wird ein Produkt verpackt und in C ein bereits verpacktes Produkt mit einer Adresse versehen. Im Korb am Ende des Fließbandes liegen bereits verpackte und adressierte Waren, welche vor einigen Momenten noch auf dem Fließband die einzelnen Stationen durchliefen.

8.4. JK-Flipflop Das JK-Flipflop ist das wahrscheinlich am häufigsten gebrauchte Flipflop. Es unterscheidet sich fast nicht vom getakteten RS-Flipflop. Der Eingang J entspricht dabei S und K entspricht R. Als Eselbrücke mag man sich vorstellen, das J für „jump“ und K für „kill“ steht. Nur für J = K = 1 ist das Verhalten im Vergleich zum RS-Flipflop ein anderes: Die Ausgabewerte werden getauscht (siehe Tabelle 8.6 und Tabelle 8.7), man spricht dabei auch vom Togglen. J

K

Q

Q∗

0 0 0 0 1 1 1 1

0 0 1 1 0 0 1 1

0 1 0 1 0 1 0 1

0 1 0 0 1 1 1 0

Keine Änderung Q rücksetzen Q setzen Q „kippt“

Tabelle 8.6.: Zustandstabelle für das JK-Flipflop J

K

0 0 1 1

0 1 0 1

Q Q

Q Q

0 1

1 0

Q

Q

Tabelle 8.7.: Zustandsfolgetabelle für das JK-Flipflop Das lässt sich z. B. mit einem RS-Flipflop bewerkstelligen, wobei S = JQ und R = K Q sind, wie in Abbildung 8.10 gezeigt. Der Leser möge sich im Selbststudium von der korrekten, gewünschten Funktion dieser Schaltung überzeugen. Bei den ersten drei Eingangsbelegungen der Zustandsfolgetabelle ist der resultierende Ausgang immer stabil, d. h. auch die eventuell resultierenden neuen Inputbelegungen durch ein verändertes Q führen nicht zu einem neuen (geänderten) Ausgangssignal. Bei der (beim RS-Flipflop verbotenen) Belegung J = K = 1 jedoch wird die Ausgabe des momentanen Wertes während der Taktung, also wenn C = 1, fortlaufend komplementiert. Dieses Phänomen wird als Oszillationsproblem bezeichnet. Dabei ändert jede neue Ausgangsbelegung sich auch wieder selbst: Das Flipflop oszilliert und erreicht keinen

118

Technische Informatik I

8.5. Master-Slave-Flipflop

Abbildung 8.10.: Interner Aufbau eines JK-Flipflops stabilen Zustand mehr. Es ist im Sinne der Definition streng genommen kein Flipflop (d. h. Speicherelement) mehr! Dieses Problem kann man auf zwei voneinander unabhängige Arten lösen: Eine davon ist die Organisation des Flipflops in einer Master-Slave-Form, welches im nächsten Abschnitt am Beispiel des RS-Flipflops erläutert wird. Die andere ist die Taktflankensteuerung, auf die danach eingegangen wird.

8.5. Master-Slave-Flipflop Das Master-Slave-Flipflop (MS-Flipflop) kommt nur in getakteter Fassung vor und entkoppelt die Eingänge von den Ausgängen. Es hat die äußere Erscheinung eines einzelnen Flipflops, besteht im Inneren aber aus zwei in Reihe geschalteten Flipflops. Eins dieser Flipflops wird als Master, das andere als Slave bezeichnet (Abbildung 8.11). Der Master kann seinen Zustand nur ändern, wenn der Takt 1 ist, der Slave dagegen nur, wenn der Takt 0 ist. Nur die Ausgänge des Slave-Flipflops werden nach außen geleitet.

Abbildung 8.11.: RS-Master-Slave-Flipflop Im Detail funktioniert das so: Ist der Takt aktiv, werden die Eingabedaten (S und R ) in das Master-Flipflop übernommen. Da die Ausgänge des Master-Flipflops nur mit den Eingängen des Slave-Flipflops verbunden sind und nicht an die Außenwelt weitergeleitet werden, ändert sich der Zustand der Ausgänge des gesamten Master-Slave-Flipflops zu diesem Zeitpunkt nicht. Weil der Master ein pegelgesteuertes RS-Flipflop ist, übernimmt es Werte von R und S , solange der Taktimpuls anhält (d. h. solange C = 1). Sobald C auf 0 fällt, wird der letzte Zustand der Eingänge im Master gespeichert (eingefroren). Zugleich wird das Ausgangssignal des Master-Flipflops zu den Eingängen des Slave-Flipflops durchgestellt. Dadurch

Technische Informatik I

119

8. Sequenzielle Schaltnetze kann dieses seinen Zustand ändern und damit auch das Ausgangssignal des gesamten MS-Flipflops. Da sich die Ausgänge des Slave-Flipflops erst ändern, wenn das Ausgangssignal des Master-Flipflops eingefroren ist, sind sie komplett von den Eingängen des gesamten MS-Flipflops entkoppelt. Im vorhergehenden Abschnitt wurde bei den „einfachen“ JK-Flipflops, die durch ein einzelnes RS-Flipflop realisiert sind, auf das Oszillationsproblem hingewiesen. Eine Lösungsmöglichkeit stellt das Master-Slave-Prinzip dar, da die Oszillation durch die direkte Rückkopplung der Ausgänge mit den Eingängen zustande kommt und beim MasterSlave-Flipflop eine solche direkte Rückkopplung keine Auswirkungen mehr haben kann. Abbildung 8.12 zeigt ein solches getaktetes JK-Master-Slave Flipflop. Das Kippen (toggeln) bleibt hier insofern erhalten, als dass es jeweils bei einem neuen Taktimpuls erfolgt. Dadurch ergibt sich die sehr nützliche Eigenschaft, dass in diesem Fall am Ausgang genau halb so viele Impulse ausgegeben werden wie am Takteingang eingespeist. Diese Eigenschaft wird zum Aufbau von Zählern genutzt.

Abbildung 8.12.: Getaktetes JK-Master-Slave-Flipflop (pegelgesteuert)

8.6. Flankengesteuerte Flipflops Bislang waren alle Flipflops, die mit einem Takt verknüpft wurden, pegelgesteuert, d. h. solange der Takt in einem bestimmten Zustand war (z. B. auf 1), wurden die Eingangsänderungen in das Flipflop übernommen. Die Taktung erfolgte also über den Pegel des Taktsignals. Beim JK-Flipflop führte das zum Oszillationsproblem, da wärend der gesamten Zeit, in der der Pegel auf 1 liegt, eine Permanente Zustandsänderung erfolgt. Eine Alternative zu diesem Ansatz ist die Steuerung über die Flanken (edges) des Takts, also den Übergängen von 0 zu 1 oder 1 zu 0. Diese Übergänge sind nur augenscheinlich unmittelbar; tatsächlich muss sich der Spannungspegel einschwingen. Die Einschwingzeit ist im Vergleich zum Takt sehr kurz. In der Regel dauert das Einschwingen weniger als fünf Nanosekunden1 . Man kann im Vergleich zu der Zeitlänge, die ein Pegel konstant ist (etwa einen halben Takt lang), eine Taktflanke wie einen Impuls von sehr kurzer Dauer betrachten, quasi als Zeitpunkt. Wenn man per Taktflanke steuern möchte, muss man darauf achten, dass die Bauteile entsprechend eine geringe Reaktionszeit haben, also schnell sind. Eine Taktflanke ist kurz genug, um bei flankengesteuerten JK-Flipflops kein Oszillationsproblem entstehen zu lassen. 1 eine Nanosekunde entspricht einem Milliardstel einer Sekunde; etwa 10−9 Sekunden

120

Technische Informatik I

8.6. Flankengesteuerte Flipflops Man unterscheidet zwischen der positiven Flanke (auch: steigende Flanke; gemeint ist der Übergang von 0 auf 1) und negativer Flanke (auch: fallende Flanke; gemeint ist der Übergang von 1 auf 0). Entsprechend unterscheidet man zwischen positivflankengesteuerten und negativflankengesteuerten Bauteilen.

Abbildung 8.13.: Negativflankengesteuertes D-Flipflop In Abbildung 8.13 ist ein negativflankengesteuertes D-Flipflop dargestellt. Es besteht aus sechs NOR-Gattern G i , i ∈ {1, . . . , 6}. Diese kann man gruppieren zu drei NOR-Flipflops. Die Funktionsweise wird nachfolgend beschrieben: Ist der Takt aktiv (C = 1), geben dadurch die Gatter G 2 und G 3 eine 0 aus (man erinnere sich, dass NOR-Gatter nur dann 1 ausgeben, wenn alle Eingänge auf 0 stehen). Dies führt dazu, dass an den Eingängen der Gatter G 5 und G 6 0 als Eingabe liegt, was das Flipflop zum Speichern seines Zustandes veranlasst. Die Ausgabe des gesamten Flipflops ist in diesem Zustand folglich stabil; Q und Q sind konstant. Der Zustand des D -Eingangs hat in diesem Taktabschnitt nur Auswirkungen auf die Gatter G 1 und G 4 : G 1 hat als Eingangssignale den Ausgang von G 2 , also 0, und D . Es gibt bei D = 0 eine 1 aus, sonst eine 0, und G 4 gibt (da ja dessen zweiter Eingang gleich dem Ausgang von G 3 ist, also 0) das Gegenteil davon aus. Man kann das auch so beschreiben: G 1 gibt D aus und G 4 gibt D aus. In dem Moment, wenn der Takt auf 0 fällt, ändern sich die Ausgänge der Gatter G 2 und G 3 und damit die Eingabewerte des “hinteren” Flipflops (G 5 und G 6 ). Ist C = 0, haben weitere Änderungen von D keine Auswirkungen mehr auf die Eingänge der Gatter G 5 und G 6 . Wir betrachten dazu zwei Fälle: • D = 0 während der fallenden Flanke: G 3 invertiert den Ausgangswert von G 4 und leitet dieses Signal als Eingabe an G 6 (d. h. D = 1 liegt an diesem Punkt der Schaltung an). Weiterhin wird diese 1 an G 2 geleitet, wodurch dieses Gatter zunächst nur eine 0 ausgeben kann. Damit steht auch der Eingang von G 5 fest auf 0. Zusammengefasst: G 5 hat nun eine 0 als Eingangssignal, G 6 eine 1, das bedeutet, der Ausgang Q wird zurückgesetzt (wenn er es noch nicht war). • D = 1 während der fallenden Flanke: Gatter G 2 ändert den Ausgabewert auf 1, wodurch zum einen der zweite Eingang an Gatter G 1 auf 1 gesetzt wird und weitere Änderungen von D zunächst keine Auswirkungen auf den Rest der Schaltung

Technische Informatik I

121

8. Sequenzielle Schaltnetze mehr haben, zum anderen ist damit der Eingang des Gatters G 5 1, dessen Ausgang Q dadurch 0, wodurch der Ausgang Q von G 6 1 wird. Wenn C danach zu Beginn des nächsten Impulses auf 1 steigt, werden die Eingabewerte des „hinteren“ Flipflops (G 5 und G 6 ) auf 0 gesetzt, wodurch sie von den Ausgängen der restlichen Schaltung isoliert werden.

8.7. Zusammenfassung Flipflops Flipflops haben interne Zustände und externe Eingänge. Ihr Ausgang ist sowohl von den externen Eingaben als auch von den internen Zuständen und damit auch von früheren Eingaben abhängig. Flipflops haben die Fähigkeit, einen Zustand beibehalten zu können und sind somit Speicherelemente. Unterschieden werden Flipflops durch die Art ihrer Eingaben und ihrer Taktung. Wir haben das RS-, D- und JK-Flipflop kennengelernt. Dabei sind alle Flipflops im Prinzip Variationen von RS: • Das RS-Flipflop (Reset/Set) löscht bzw. setzt den Ausgang bei einem Impuls auf einem der Eingänge. • Beim D-Flipflop wird, wenn das Flipflop getaktet wird (d. h. der zweite Eingang C = 1), der Eingang D zum Ausgang durchgereicht. • Das JK-Flipflop arbeitet weitgehend wie das RS-Flipflop, nur dass hier zusätzlich der vierte Zustand (beide Eingänge sind 1) zum Kippen des Ausgangs Q genutzt wird. Es wird zwischen Taktzustandssteuerung (Taktpegelsteuerung) und Taktflankensteuerung unterschieden. Dabei beschränkt die Taktflankensteuerung den Zeitbereich, in dem die Eingangssignale übernommen werden. Weiterhin wird zwischen „normalen“ und Master-Slave-Flipflops unterschieden. Gewöhnlich führt eine Zustandsänderung aufgrund einer veränderten Eingangsbelegung direkt zu einer veränderten Ausgangsbelegung (bzw. mit minimaler Verzögerung, die nur von der Implementierung abhängt). Master-Slave-Flipflops dagegen entkoppeln die Eingänge von den Ausgängen; sie schalten in zwei zeitlich überlappungsfreien Stufen. Dadurch werden unerwünschte Rückkopplungseffekte vermieden.

8.8. Beispiele für sequentielle Schaltnetzelemente So wie Gatter mit anderen Gattern verbunden werden, um kombinatorische Schaltungen wie Addierer und Multiplexer zu schaffen, lassen sich Flipflops zu sequentiellen Schaltungen zusammenschließen (wie wir es schon bei dem taktflankengesteuerten D-Flipflop gesehen haben). Nachfolgend beschäftigen wir uns mit zwei Arten dieser sequentiellen Schaltungen: Dem Schieberegister und dem Zähler.

8.8.1. Register In Computern wird zur Komplexitätsminderung nicht mit einzelnen Bits gearbeitet, sondern mit Wörtern. Das sind geordnete Bit-Gruppen, zum Beispiel Bytes (8-Bit-Wörter) oder Doppelwörter (32-Bit-Wörter).

122

Technische Informatik I

8.8. Beispiele Ein solches Wort speichert man in einem Register; es umfasst mehrere Flipflops, die jeweils ein Bit speichern. Damit die Flipflops nicht beliebig ihren Zustand ändern, sondern untereinander abgestimmt sind, werden sie über ein gemeinsames Taktsignal synchronisiert. Damit übernimmt entweder kein Flipflop eine Eingangsbelegung oder alle gleichzeitig. Wie in Unterabschnitt 8.3.1 gesehen, nimmt eine Gruppe von m D-Flipflops ein m -Bitwort auf.

8.8.2. Schiebe- und Rotationsregister Bei der Programmierung werden häufig die Operationen Schieben und Rotieren verwendet. Schieben (shift) bedeutet ein Verrücken der Bits eines Wortes. Herausgeschobene Bits sind verloren. Die frei werdenden Stellen werden mit Nullen oder Einsen aufgefüllt; wir gehen an dieser Stelle von einem logischen Schieben aus, bei dem Nullen eingeschoben werden. Schieben kann man nach links (in Richtung des höchstwertigen Bits) oder rechts (in Richtung des niederwertigsten Bits). Mathematisch betrachtet entspricht ein Linksverschieben (left shift) einer Verdopplung der Zahl, ein Rechtsverschieben (right shift) einer Ganzzahldivision durch Zwei. Betrachten wir beispielsweise drei Zustände eines (so genannten logischen) Rechtsschieberegisters: Zu Beginn enthalten die Flipflops die Werte 01110101. Wird das Register getaktet, werden alle Bits um eine Stelle nach rechts verschoben, das letzte Bit fallengelassen und in die freie linke Stelle eine Null geschrieben. Das Wort sieht danach so aus: 00111010. Nach einem weiteren Impuls ändert sich das Binärmuster zu 00011101 usw. Ähnlich funktioniert das Linksschieberegister, bei dem das Bitmuster nach links verrückt, das erste Bit fallengelassen und rechts eine Null angefügt wird. Verwandt mit den Schieberegistern sind Rotationsregister: Diese lassen das herausgeschobene Bit nicht fallen, sondern fügen es in die frei werdende Stelle am anderen Wortende ein. Dies ist ebenfalls nach links oder rechts möglich.

Abbildung 8.14.: Schieberegister aus D-Flipflops Abbildung 8.14 zeigt ein aus D-Flipflops aufgebautes Schieberegister. Der Ausgang Q jedes Flipflops ist mit dem Eingang D des jeweils rechten Flipflops verbunden. Jedes Flipflop bekommt das Taktsignal eines gemeinsamen Taktgebers. Wenn das Flipflop i getaktet wird, übernimmt es den Zustand des vorherigen (linken) Flipflops i − 1. Wäre diese Schaltung taktpegelgesteuert und bestünde aus „normalen“ Flipflops, so würde eine Eingabe an D i n sofort bis zum letzten Flipflop durchgereicht werden. Um dies zu verhindern, müssen die D-Flipflops flankengesteuert (wie in der Abbildung) oder als Master-Slave-Flipflop umgesetzt sein. Flipflops speichern Daten, unabhängig von der genaueren Ausgestaltung (RS, D, JK usw.). Daher lassen sich sequentielle Schaltnetze mit ein paar kleineren Anpassungen auf die Verwendung einer anderen Flipflopart ändern. Dies wird gezeigt in Abbildung 8.15, in

Technische Informatik I

123

8. Sequenzielle Schaltnetze

Abbildung 8.15.: Schieberegister aus JK-Flipflops der die gleiche Schaltung wie in Abbildung 8.14 realisiert ist, jedoch nicht aus D-, sondern aus JK-Flipflops. Das Dateneingangssignal wird in den J-Eingang und als Komplement in den K-Eingang des ersten Flipflops geleitet. Es ist also entweder J aktiv (Bit setzen) oder K (Bit rücksetzen), aber keinesfalls beide gleichzeitig. Auch der Speicherzustand J = K = 0 wird nicht erreicht. Somit entspricht das Flipflop als Black Box betrachtet dem D-Flipflop.

Abbildung 8.16.: Paralleles Schieberegister Dementsprechend kann ein solches Register auch aus RS-Flipflops gebildet werden. Dies wird als 4-Bit-Variante in Abbildung 8.16 gezeigt, in der durch ein paar zusätzliche Gatter ermöglicht wird, das Register entweder parallel oder seriell zu laden. Die Ausgabe erfolgt parallel. Die Eingabe Schieben/Laden (SL ) steuert, ob geschoben oder geladen wird: Ist SL = 1, so wird geschoben und seriell geladen (Schiebemodus); ist SL = 0, so wird parallel geladen (Parallellademodus). Intern sorgt ein Zwei-Eingabe-Multiplexer als zusätzliche Logik (additional gating) für die Umsetzung der Funktionalität. Pro Flipflop wird dabei ein solches Multiplexer ver-

124

Technische Informatik I

8.8. Beispiele wendet, der aus zwei UNDs, einem ODER und einem Inverter besteht. Je nach Zustand des Steuersignals SL wird an den Eingang des jeweiligen Flipflops entweder der Ausgang des Vorgängerflipflops durchgeleitet oder Signal am Paralleleingang. Wie in Abbildung 6.5 dargestellt, werden die UND-Gatter zur Informationsflusssteuerung verwendet.

8.8.3. Frequenzteiler und Zähler Genauso nützlich wie bei der Konstruktion von Registern sind Flipflops bei der Implementierung von Frequenzteilern und Zählern. Diese spielen in digitalen Geräten eine wichtige Rolle. Dabei wird im Prinzip die gleiche Schaltung für beide Zwecke eingesetzt. Sie unterscheidet sich in ihrer Ausgangsanzahl: Ein Frequenzteiler, der ein Taktsignal quasi streckt, hat nur einen Ausgang, ein Zähler dagegen mehrere.

Abbildung 8.17.: 1:16-Frequenzteiler bzw. Vier-Bit-Rückwärts-Zähler aus JK-Flipflops Abbildung 8.17 zeigt einen 4-Bit-Zähler/-Frequenzteiler aus JK-Flipflops, die in diesem Fall positivflankengesteuert sind. Es wird die Eigenschaft ausgenutzt, dass bei J = K = 1 der Ausgang bei jedem Taktimpuls invertiert wird. Interpretiert man am ersten Flipflop F1 den Ausgang Q 1 als neues Taktsignal, so kann man beim Vergleich mit dem Eingangstaktsignal erkennen, dass der Ausgang nur halb so schnell taktet. Anders gesagt: Um an Q 1 vom Zustand 1 über eine 0 wieder zum Zustand 1 zu gelangen, werden zwei Takte benötigt. Da Q 1 ebenfalls als Taktsignal des zweiten Flipflops verwendet wird, hat dessen Ausgang Q 2 nur noch ein Viertel der Frequenz des Systemtakts. Entsprechend gibt F3 ein Taktsignal von einem Achtel der Ursprungsfrequenz aus und an Q 4 liegt nur noch ein Sechzehntel der Frequenz an. Wenn wir also nur den letzten Ausgang Q 4 abgreifen, ist diese Schaltung für uns ein 1:16-Frequenzteiler. Wenn wir dagegen alle Ausgänge gleichzeitig auslesen, können wir beobachten, wie die Schaltung binär von 11112 bis 00002 herunterzählt. Im Anschluss daran beginnt sie wieder bei 11112 ; die Schaltung arbeitet also zyklisch. Man beachte, dass dabei Q 4 das höchstwertige Bit und Q 1 das niederwertigste Bit repräsentiert. Allgemein definiert, ist ein Zähler eine spezielle sequentielle Schaltung mit einem einzelnen Takteingang und einer bestimmten Anzahl von Ausgängen. Jedes Mal, wenn der Zähler getaktet wird, ändert sich einer oder mehrere der Ausgänge. Diese Ausgänge bilden eine Sequenz mit n unterschiedlichen und eindeutigen Werten. Nachdem der Wert n an den Ausgängen beobachtet werden konnte, setzt der nächste Taktimpuls den Zähler wieder auf die Ausgangsbelegung zurück, die zu Beginn der Sequenz vorlag. Daraus folgt, dass die Sequenz zyklisch ist. Dabei muss der Wert n nicht das Maximum sein, sondern kann – wie im Beispiel gesehen – auch das Minimum darstellen.

Technische Informatik I

125

8. Sequenzielle Schaltnetze Einen Zähler wie in Abbildung 8.17 nennt man auch asynchron oder ripple counter (ripple – engl.: dahinplätschern, kleine Wellen schlagen), da der Ausgang des ersten Flipflops sofort das zweite Flipflop auslöst, welches sogleich das dritte Flipflop ändert usw. Das hat zur Folge, dass eine Zustandsänderung des ersten Flipflops sofort Auswirkungen auf das nächste Flipflop hat. Im Extremfall sind alle Flipflops betroffen. Das wäre kein Problem, wenn alle Flipflops ohne Zeitverzögerung den neuen Ausgangswert anzeigen würden. Tatsächlich aber verursachen Flipflops eine gewisse Verzögerung d , die sie benötigen, bis das korrekte Ergebnis am Ausgang anliegt. Beim asynchronen Zähler summiert sich diese Verzögerung auf. Im schlechtesten Fall steht bei unserem Vier-Bit-Zähler der neue korrekte Wert an allen Ausgängen erst nach der Zeit 4d bereit. Nicht in jedem Fall ist ein Takt lang genug, um eine solche Verzögerung zu tolerieren. Dem Problem kann man begegnen, indem man unter Hinzunahme einiger Logikgatter einen synchronen Zähler konstruiert. Bei diesem sind alle Flipflops durch einen gemeinsamen Takt gesteuert. Dadurch ändern sie gemeinsam ihren Zustand. Da die maximal mögliche Verzögerung durch diese Maßnahme stark reduziert ist, kann ein solcher Zähler erheblich schneller als ein asynchroner Zähler arbeiten. Zähler finden in digitalen Rechnern vielfache Anwendung. Beispielsweise ist der Programmzähler des Prozessor so aufgebaut (vgl. Kapitel 2). Sobald der Befehl an der aktuell anliegenden Adresse geladen ist, muss der Programmzähler um 1 erhöht werden und zeigt dann auf den nächsten Befehl. So kann der sequentielle Ablauf eines Programms realisiert werden.

8.9. Beschreibung durch Zustandsdiagramme Wir haben bereits an den Beispielen gesehen, dass die Beschreibungen von sequentiellen Schaltungen und deren Arbeitsweise recht umfangreich und komplex werden können. Es ist daher erstrebenswert, eine Darstellung zu finden, die sich leichter lesen lässt. Eine häufig verwendete Form, die Arbeitsweise von sequentiellen Schaltungen zu beschreiben, sind die so genannten Zustandsdiagramme. Diese werden im Folgenden beschrieben. Ein Zustand beschreibt dabei eine bestimmte innere Konfiguration und Ausgabebelegung. Zustände werden in einem Zustandsdiagramm durch bezeichnete Kreise symbolisiert. Gleiche Zustände werden zusammengefasst. Beispielsweise gibt es beim Flipflop zwei Zustände: Entweder Q ist 0 (Zustand S 1 ) oder Q ist 1 (Zustand S 2 ). Weiterhin gibt es Übergänge, die einer Eingabekonfiguration entsprechen. Übergänge gehen von einem Zustand aus und führen wieder in einen Zustand. Da in der Regel alle Eingabekonfigurationen ungeachtet des inneren Zustands möglich sind, müssen von einem Zustand aus alle möglichen Übergänge behandelt werden. Es handelt sich um gerichtete Kanten, die im Zustandsdiagramm durch beschriftete Pfeile dargestellt werden. Um das obige Beispiel fortzuführen, nehmen wir an, dass es sich beim betrachteten Flipflop um ein JK-Flipflop handelt (Abbildung 8.18). Egal, ob dieses gerade 1 oder 0 ausgibt, kann bei einem Taktimpuls am Eingang eine der vier möglichen Eingangsbelegungen anliegen (Tabelle 8.8). Die Übergänge müssen alle diese Möglichkeiten abdecken. Ist Q = 0 (Zustand S 1 ), ändert ein Rücksetzen (C 2 ) oder Speichern (C 1 ) den Ausgangszustand des Flipflops nicht. Der Übergang ist also eine Kante, die in den gleichen Zustand zurück-

126

Technische Informatik I

8.9. Zustandsdiagramme

Abbildung 8.18.: Zustandsdiagramm eines JK-Flipflops führt. Die anderen beiden Eingangsbelegungen (C 3 und C 4 ) dagegen ändern die Ausgabe des Flipflops und somit auch den internen Zustand. Daher führt die entsprechende Kante in den Zustand S 2 . J

K

Eingangsbelegungen

0 0 1 1

0 1 0 1

C1 C2 C3 C4

Tabelle 8.8.: Eingabemöglichkeiten an einem JK-Flipflop Analog dazu ändert sich die Ausgangsbelegung S 2 nicht, wenn die Eingangsbelegung C 1 oder C 3 anliegt. Bei C 2 und C 4 dagegen wird der Zustand S 2 in Zustand S 1 überführt. Wie wir sehen, ändert die Eingangsbelegung C 4 den Zustand, egal, in welchem Zustand man sich vorher befunden hat (JK-Flipflop-Eigenschaft).

8.9.1. Zustandsdiagrammdarstellung eines Drei-Bit-Zählers Ein weiteres Beispiel zeigt Abbildung 8.19. Sie zeigt ein Zustandsdiagramm eines DreiBit-Zählers. Da ein Zähler wie oben erklärt nur einen Eingang hat (nämlich den Takt), hat jeder Zustand nur jeweils eine abgehende Kante, die in diesem Fall einer Taktung entspricht. Zwischen den einzelnen Taktungen verbleibt das System in dem entsprechenden Zustand.

Abbildung 8.19.: Zustandsdiagramm eines Drei-Bit-Zählers

Technische Informatik I

127

8. Sequenzielle Schaltnetze Wenn das System getaktet wird, rotiert es durch die Zustände S 0 bis S 7 , welche in den Ausgangsbelegungen den binären Ganzzahlen von 010 bis 710 entsprechen. Alle acht Taktimpulse erreicht der Zähler wieder den Ausgangszustand. An dem Diagramm erkennt man gut die zyklische Natur eines Zählers.

8.10. Testen von digitalen Schaltungen Gegenüber kombinatorischen Schaltnetzen bringen sequentielle Schaltnetze eine Komplexitätssteigerung mit sich. Dadurch ist es nun unter anderem möglich, Speicher oder Zähler zu bauen. Nachteilig ist dagegen, dass es schwieriger wird, eine Schaltung auf korrekte Funktionsweise zu testen. Ein Beispiel wird dies veranschaulichen: Wir möchten einen ein Kilobyte großen Speicher testen. Er hat einen 10-Bit-Adress-Eingang, kann also über 210 verschiedene Adressen angesprochen werden. Der Datentransfer erfolgt über eine 8-Bit-Datenschnittstelle; der Speicher kann also entweder ein Byte empfangen, das an die entsprechende Adresse geschrieben wird, oder das Byte ausgeben, welches durch die eingegebene Adresse angesprochen wird. Mögliche Funktionstests wären: 1. Trivialer Test: Schreiben eines (beliebigen) Testmusters in jede der 210 Speicherzellen und anschließendes Auslesen (210 Tests). Dieser Test überprüft zwar die prinzipielle Funktionsweise des Speichers, aber nicht, ob in jeder Zelle auch jedes denkbare 8-Bit-Wort korrekt gespeichert wird. Besser wäre: 2. Überprüfen aller 28 Testmuster für jede der 210 Speicherzellen (28 · 210 = 218 Tests). Dieser Test überprüft zwar, ob jede Speicherzelle für sich sämtliche möglichen Bitbelegungen korrekt speichert, aber nicht, ob diese auch stabil gespeichert sind, wenn irgendein anderer Wert auf dem Speicherchip geändert wird. Daher folgt die dritte Teststufe: ¢ ¤ £¡ 3. Überprüfung auf musterabhängige Fehler: 218 · 210 − 1 · 28 ≈ 236 Tests Zur Überprüfung auf musterabhängige Fehler müssen wir also für jeden der 218 Tests folgendes machen: In jede der 210 −1 übrigen Zellen müssen jeweils zusätzlich sämtliche mögliche Wörter (jeweils 28 ) geschrieben werden, um zu sehen, ob sich das Wort in der zu testenden Zelle verändert hat. Selbst bei einer Rate von 106 Tests pro Sekunde würde ein Speichertest nach Variante 3 ca. 19 Stunden, 5 Minuten dauern – und es handelt sich nur um die Funktionsüberprüfung eines einzelnen Kilobytes. Diese Testkomplexität zeigt, dass vollständige Tests nur in Ausnahmefällen durchführbar sind. Bei einem Rechnerstart werden daher auch nur einfache Speichertests durchgeführt. Diese Tests heißen Power-On Self-Test (POST). Es wird beispielsweise der gesamte Speicher mit 0 beschrieben und anschließend getestet, ob im Speicher tatsächlich 0 steht (Test nach Variante 1). Damit sind nur sehr schwerwiegende Fehler wie defekte Speicherzellen zu entdecken. Dieser Test dient mehr dazu herauszufinden, ob und wo Speicher vorhanden ist. Weitaus wahrscheinlicher als eine kaputte Speicherzelle sind Überbrückungsfehler. Dabei beeinflusst eine Speicherzelle eine benachbarte oder es sind zwei Leitungen überbrückt. Solche Fehler lassen sich dadurch finden, indem ein Testmuster in eine Zelle geschrieben wird und dann getestet wird, ob dadurch eine andere Speicherzelle verändert

128

Technische Informatik I

8.10. Testen von digitalen Schaltungen wurde (Test ähnlich der Variante 3). Um nicht jedes Testmuster schreiben zu müssen, werden beispielsweise durchlaufende Einsen und anschließend das inverse Testmuster geschrieben (walking ones, moving inverse), was eine brauchbare Annäherung an den Test nach Variante 2 darstellt, wobei zusätzlich benachbarte Zellen gelesen werden. Da sich ein Überbrückungsfehler auf eine höhere oder niedrigere Adresse auswirkt, wird der Test einmal beginnend von der kleinsten und einmal beginnend von der größten Adresse ausgeführt. Dadurch wird eine gute Annäherung an Testvariante 3 erzielt. Generell ist es dennoch ein Dilemma, Tests zu entwerfen, die eine ausreichende Zuverlässigkeit beim Aufspüren des größten Teils der möglichen Fehler bieten, ohne dabei zu viel Zeit in Anspruch zu nehmen. Der erste Schritt bei der Planung solcher Tests ist die Unterscheidung zwischen einem Defekt (defect) und einer Störung (fault). • Ein Defekt ein Fehler, der bereits bei der Herstellung in das System integriert wurde. Zum Beispiel kann ein Chip Fehler machen, wenn es einen Defekt auf der Komponentenebene (ein defekter Kristall im Siliziumchip) oder auf der Systemebene (ein Lötspritzer, der zwei getrennte Leitungen auf einer Platine miteinander verbindet und so für einen Kurzschluss sorgt). • Den beobachteten Fehler, also das Symptom, nennt man Störung. Obwohl es eine große Anzahl an möglichen Defekten gibt, ist die Menge der Symptome recht klein (Bei einem Auto, das nicht anspringt, sind ebenfalls unterschiedliche Ursachen möglich). Digitale Systeme kann man deswegen in Störungsmodellen oder Fehlermodellen beschreiben. Typische Störungen sind: 1. Stuck-at-one (Auf Eins festgefahren) Der Eingang oder Ausgang einer Schaltung bleibt konstant bei Logisch 1, unabhängig von den anderen Schaltparametern. Eine Schreibweise dafür ist s_a_1. 2. Stuck-at-zero (Auf Null festgefahren) In diesem Fall bleibt der Eingang oder Ausgang konstant auf Logisch 0 (Schreibweise s_a_0). 3. Bridging Fault (Überbrückungsfehler) Zwei Eingänge oder Ausgänge sind anscheinend miteinander verbunden und können keine unterschiedlichen Werte annehmen, das heißt sie sind entweder beide 0 oder beide 1. Es ist möglich, eine längere Liste von Fehlermodellen aufzustellen. Interessant ist, dass das „stuck-at“-Modell in der Lage ist, eine überraschend hohe Anzahl von möglichen Defekten zu erkennen. Dies bedeutet, dass der Test auf alle möglichen „stuck at“-Fehler fast alle möglichen Defekte erkennen wird. Solche Fehler lassen sich mithilfe einer Pfadsensitivierung finden.

8.10.1. Test durch Pfadsensitivierung Pfadsensitivierung bedeutet, dass Eingangsbelegungen so gewählt werden, dass sich bei Änderung einer Eingangsbelegung in eine andere der Ausgang ändern sollte. Man sensibilisiert den Ausgang für einen bestimmten Fall. Abbildung 8.20(a) zeigt einen zu testenden Pfad zwischen dem Eingang A und dem Ausgang K. In Abbildung 8.20(b) haben wir den sensitivierten Pfad bereits ausgewählt, indem wir durch das Setzen aller anderen Parameter auf 1 (UND) bzw. 0 (ODER) sichergestellt haben, dass der Eingang A durch die Schaltung durchgereicht wird. Dadurch ist nun K = A, sodass wir durch Setzen von A auf 0 oder 1 auf eine „A stuck at“-Störung testen können.

Technische Informatik I

129

8. Sequenzielle Schaltnetze

(a)

(b)

Abbildung 8.20.: Beispiel einer Pfadsensitivierung Nun könnte man eine Störungsliste für die Schaltung anfertigen, welche in diesem Falle aus den Einträgen A s_a_0, A s_a_1, B s_a_0, B s_a_1,... bestehen würde. Eine übliche Notation dafür ist auch A/0, A/1, B/0, B/1 usw. Dabei bedeutet „/“ so viel wie „stuck at“. Um auf A/0 zu testen, werden die anderen Ausgänge entsprechend belegt und überprüft, ob sich der Ausgang K bei einer entsprechenden Änderung von A von 0 auf 1 ebenfalls ändert. Sollte dies der Fall sein, so hängt A nicht fest. Für den Test auf A/1 verfährt man analog dazu. Störungstests werden von Ingenieuren (möglicherweise mit CAD-Systemen) entwickelt und können entweder manuell oder durch eine computergesteuerte automatische Testausrüstung (ATE, automatic test equipment) durchgeführt werden. Diese setzt die entsprechenden Eingangssignale und vergleicht den Ausgang mit dem erwarteten Wert. Es ist aufgrund des topologischen Aufbaus einer Schaltung nicht immer möglich, diese wie eben beschrieben zu testen. Zum Beispiel kann ein digitales Signal mehr als einen Weg durch die Schaltung nehmen und mehrere Defekte können sich in einem bestimmten Knoten gegenseitig eliminieren, sodass keine Störung beobachtet wird. Darüber hinaus können Schaltungen auch nicht erkennbare Fehler haben.

Abbildung 8.21.: Beispiel für einen nicht erkennbaren Fehler Ein Beispiel für eine solche Schaltung ist in Abbildung 8.21 dargestellt. Um hierbei einen sensitiven Pfad für den Knoten D zu schaffen, müssen die Punkte G und F auf 0 gesetzt werden, was aber nur möglich ist, wenn der Eingang A gleichzeitig 0 und 1 ist. Aufgrund dieses Widerspruches lässt sich der Eingang D nicht auf einen „stuck at“-Fehler testen. Man beachte, dass diese Art eines nicht erkennbaren Fehlers durch Redundanz auftritt, und durch eine Neugestaltung der Schaltung behoben werden könnte. Alternativ dazu

130

Technische Informatik I

8.11. Zusammenfassung kann man eine Schaltung auch leichter testen, indem man bei der Konstruktion einige interne Knoten mit weiteren Ausgängen versieht, die sich dann direkt untersuchen ließen. Viele Schaltungen auf einem Chip (d. h. hohe Integrationsdichte) bei einer konstanten Anzahl von Pins (Ausgänge des Chips) resultieren in einer schlechten Testbarkeit. Aus diesem Grund werden in kritischen Anwendungen in der Regel ältere, sehr gut erprobte Schaltkreise (sogenannte good known dies) verwendet, deren Entwicklungsstand eine gewisse Reife erreicht hat.

8.11. Zusammenfassung In diesem Kapitel haben wir einige der grundlegenden Komponenten eingeführt, die bei der Entwicklung von digitalen Systemen angewendet werden. Aus der Sicht des Entwicklers sind die wichtigsten Parameter bei der Entwicklung eines Chips die Kosten und die Geschwindigkeit. Beide Parameter lassen sich verbessern, indem man die Komplexität einzelner Schaltungen so klein wie möglich hält. Bei großen Chips ist das möglich, da mehrere komplexere logische Schaltungen auf einem einzelnen Chip Platz finden. Zwei andere Prinzipien des Designs werden zunehmend wichtig. Die Fähigkeit, die Schaltung einfach entwerfen zu können, vereinfacht die Aufgabe, die korrekte Funktion der neuen Hardware nachzuweisen und sie andernfalls leicht reparieren zu können. Weiterhin ist es meist erstrebenswert, die Zuverlässigkeit des Systems mit Hilfe von weiterer, auch überflüssiger, Logik zu erhöhen (z. B. manche Bestandteile doppelt zu integrieren). Beide dieser Prinzipien führen schnell zu erhöhten Kosten. Es ist Aufgabe des Entwicklers, hier einen zufriedenstellenden Kompromiss zwischen den Überlegungen zu finden. Es gibt leider keine optimalen Wahrheiten wie in der Mathematik, weil es keine widerspruchsfreien Randbedingungen gibt.

Technische Informatik I

131

9. Rechnerarithmetik Im folgenden Kapitel werden wir uns im Wesentlichen mit den folgenden vier, scheinbar trivialen, Fragen auseinandersetzen: 1. Wie werden Zahlen repräsentiert und konvertiert? 2. Wie werden negative Zahlen und Brüche repräsentiert? 3. Wie werden die Grundrechenarten ausgeführt? 4. Was ist, wenn das Ergebnis einer Operation größer ist als die größte darzustellende Zahl? Die Antwort erhält man, wenn man sich fragt: Was sind die wesentlichen Unterschiede zwischen der Arithmetik, wie sie vom Rechner ausgeführt wird, und der menschlichen Rechenweise? Die Antwort lautet im Wesentlichen, dass im Rechner die Genauigkeit der Zahlen und ihr Platzbedarf für die Darstellung endlich und begrenzt ist. Wenn man selbst rechnet, denkt man wenig über die Frage nach, wie viele Ziffern man braucht, um eine Zahl darzustellen. Physiker können berechnen, dass es 1078 Elektronen im Universum gibt, ohne sich davon stören zu lassen, dass man 79 Ziffern brauchen würde, um diese Zahl ganz aufzuschreiben. Wenn jemand das Ergebnis einer Funktion auf sechs Stellen genau berechnen möchte, behält er einfach Zwischenergebnisse mit einer Genauigkeit von sieben oder acht Stellen. Das Problem, dass es kein Papier gibt, welches nicht breit genug für siebenstellige Zahlen ist, tritt niemals auf. In Computern sieht dies nun ganz anders aus. Bei den meisten Rechnern wird die Speichermenge, die für das Speichern einer Zahl zur Verfügung steht, bereits festgelegt, wenn der Rechner entworfen wird. Mit einem bestimmten Aufwand kann ein Programmierer damit auch Zahlen darstellen, die doppelt oder dreifach so groß sind, was das Problem an sich aber nicht aus der Welt schafft. Die endliche Natur der Computerarithmetik zwingt uns, lediglich auf Zahlen, die durch eine endliche Anzahl von Ziffern dargestellt werden können, zu beschränken. Solche Zahlen heißen Zahlen mit endlicher Genauigkeit (finite precision numbers). Rechner speichern solche Zahlen in Einheiten festgelegter Länge, die man Worte nennt. Einige Beispiele für reale Wortlängen sind in Tabelle 9.1 aufgelistet. Eine Gruppe von acht Bits nennt man auch Byte. Oft sagt man auch, ein Wort ist zwei oder vier Bytes lang, da die Anzahl der Bits ein Vielfaches von Acht ist. Mikroprozessoren und Kleincomputer sind normalerweise byte-orientiert mit Wortlängen und Adressen der Länge 8, 16 oder 32 Bits. Um die Eigenschaften von Zahlen mit endlicher Genauigkeit kennen zu lernen, untersuchen wir die Menge aller positiven Ganzzahlen mit drei Ziffern. Die Menge hat genau 1000 Elemente: 000, 001, 002, 003, ..., 999; es sei darauf hingewiesen, dass die führenden Nullen normalerweise nicht hingeschrieben werden, da das Weglassen den Wert der Zahl nicht beeinflusst. Eine wichtige Eigenschaft der Arithmetik auf der Menge von Ganzzahlen ist die Abgeschlossenheit bezüglich der Operationen Addition, Subtraktion und Multiplikation (vgl.

133

9. Rechnerarithmetik Mikroprozessor 8085, Z80, 6809, AVR 8085, 68000 80386, 68020 Pentium, Athlon, PowerPC (Sun SPARC, IBM AIX) Xeon, Athlon 64 typische Mikrocontroller Cray-1 (Supercomputer)

Wortlänge in Bit 8 16 32 32 64 (4), 8, 16, (32) 64

Tabelle 9.1.: Wortlängen in verschiedenen Computersystemen Lehrveranstaltung Mathematik I „Algebra“). Das heißt, dass für jedes Paar der Ganzzahlen i und j auch i + j , i − j und i · j Ganzzahlen sein müssen. Abgeschlossenheit bezüglich der Division existiert nicht, da es Werte für i und j gibt, sodass i / j nicht mehr durch eine Ganzzahl darstellbar ist (z. B. 7/2). Unsere Ganzzahlmenge ist nicht abgeschlossen bezüglich der anderen drei Grundrechenoperationen: 600 + 600 =

1200

Resultat ist zu groß (Überlauf)

3−5 =

−2

Resultat ist zu klein (negativer Überlauf)

50 · 50 =

2500

Resultat ist zu groß (Überlauf)

Daraus folgt, dass alle Zahlen mit endlicher Genauigkeit nicht abgeschlossen bezüglich den Grundrechenoperationen sind, da es immer ein Paar gibt, dessen Operationsergebnis außerhalb der Grundmenge liegt. Das hat wiederum zur Folge, dass das Ergebnis bestimmter Berechnungen aus der Sicht der klassischen Mathematik einfach falsch sind! Ein Rechengerät, das die falsche Antwort gibt, obwohl es korrekt arbeitet, mag auf den ersten Blick ungewöhnlich erscheinen. Aber der Fehler ist eine logische Konsequenz der endlichen Natur des Gerätes. Einige Rechner haben spezielle Hardware, welche Überlauffehler erkennt. Es gibt noch weitere Unterschiede zwischen der Algebra in einem Rechner und der normalen Algebra. Sehen wir uns zum Beispiel das Assoziativgesetz bei unseren dreistelligen, positiven Ganzzahlen an: a + (b − c) = (a + b) − c

Berechnen wir beide Seiten mit a = 700, b = 400 und c = 300. Zum Errechnen der linken Seite, berechnen wir zunächst (b −c), mit dem Ergebnis 100, und addieren diesen Wert zu a, sodass das Gesamtergebnis 800 ist. Für die rechte Seite berechnen wir anfangs (a + b), was uns sofort einen Überlauf in dieser algebraischen Struktur liefert. Das exakte Ergebnis hängt von der genauen Implementierung des Rechners ab, aber es wird nicht 1100 sein. Das Assoziativgesetz gilt also nicht. Die Reihenfolge der Operationen ist also wichtig. Als ein weiteres Beispiel betrachten wir das Distributivgesetz: a · (b − c) = a · b − a · c

134

Technische Informatik I

9.1. Zahlensysteme mit unterschiedlichen Basen Wenn wir für a = 5, b = 210 und c = 192 annehmen, ist der Wert der linken Seite 5 · 15, also 75. Das Ergebnis der rechten Seite ist aber nicht 75, da a · b bereits überläuft. Aus diesen Beispielen kann man schließen, dass Computer, obwohl sie Mehrzweckgeräte sind, durch ihre endliche Natur nur unzureichend für Arithmetik geeignet sind. (Und das, obwohl sie genau dafür eigentlich erfunden wurden: Das Rechnen). Der Schluss ist natürlich so auch nicht ganz richtig, aber er dient zur Illustration der enormen Wichtigkeit, zu verstehen, wie Rechner arbeiten und welche Einschränkungen sie haben.

9.1. Zahlensysteme mit unterschiedlichen Basen Eine normale Dezimalzahl, wie sie jedermann geläufig sein sollte, besteht aus einer Folge aus Ziffern und möglicherweise einem Dezimalkomma (oder einem Dezimalpunkt, wie er im englischsprachigen Raum gebräuchlich ist). Diese Notation ist positionell. Positionell heißt hierbei, dass der Wert oder die Wichtung einer Ziffer von ihrer Position innerhalb der Zahl abhängt. Wenn eine Ziffer um eine Stelle nach links verschoben wird, wird sie mit dem Wert zehn (der Basis dieses Zahlensystems) multipliziert. Analog dazu entspricht einer Verschiebung um eine Stelle nach rechts einer Division durch zehn. Also ist die Ziffer 9 in der Zahl 95 zehnmal so viel wert wie die 9 in einer 59. Das mag nun offensichtlich und nicht der Rede wert erscheinen. Allerdings ist es für uns nur deshalb so trivial, da wir täglich damit arbeiten. Betrachten wir zum Beispiel der Kultur der alten Römer: Während sie die halbe Welt eroberten und die lateinischer Grammatik erfanden, hatten sie dennoch eine sehr schwerfällige Mathematik. Da sie kein direktes positionelles System verwendeten, musste jede große Zahl ein eigenes spezielles Symbol haben. Ihr Zahlensystem war indirekt abhängig von der Position, d. h. wenn X = 10 und I = 1, so war X I = 11 (10 + 1) und I X = 9 (10 − 1). Den Nachteil dieses Systems erkennt man beispielsweise bei der Multiplikation. Bekanntlich ist die Fläche eines Rechteckes das Produkt der Seitenlängen. Versuchen Sie einmal, die Fläche eines rechteckigen Feldes mit den Seitenlängen von 35 und 45 Längeneinheiten mit Hilfe von römischen Zahlen zu berechnen! Ein scheinbar triviales Problem, doch das war damals nur von Experten lösbar. Eine Zahl N , die in einer positionellen Notation mit der Basis b dargestellt wird, wird als a n a n−1 a n−2 . . . a 1 a 0 a −1 . . . a −m dargestellt und ist definiert als a n b n + a n−1 b n−1 + a n−2 b n−2 + · · · + a 1 b 1 + a 0 b 0 + a −1 b −1 + · · · + a −m b −m =

n X

ai b i

i =−m

Die ai der obigen Gleichung werden Ziffern genannt und können einen von b verschiedenen Wert annehmen. Die positionelle Darstellung verwendet das Komma, um ganzzahlige und gebrochene Teile der Zahl zu trennen. Die Wahl der Basis für das menschliche Zahlensystem fällt auf zehn, da wir dezimale Zahlen (also mit der Basis zehn) nutzen. Wenn man mit Computern zu tun hat, werden gewöhnlicherweise andere Basen als zehn verwendet. Die wichtigsten davon sind die Basen 2, 8 und 16. Die darauf aufbauenden Zahlensysteme nennt man demzufolge binär, oktal, und hexdezimal. Da alle diese Basen Zweierpotenzen sind, lassen sie sich leicht ineinander konvertieren.

Technische Informatik I

135

9. Rechnerarithmetik Wenn es notwendig ist, die Basis einer Zahl mit anzugeben, so geschieht dies für gewöhnlich durch einen Index. So bedeutet 12310 auch 123 dezimal und 1238 folglich 123 oktal. Menschen arbeiten normalerweise dezimal und Rechner binär. Wir werden später sehen, dass der Zweck des oktalen und des hexadezimalen Systems als eine Hilfe für das menschliche Gedächnis dient. Es ist nahezu unmöglich, sich lange Folgen von binären Ziffern zu merken. Wenn man diese in das oktale oder hexadezimale System umwandelt (eine sehr einfach Aufgabe), lassen sich die kürzeren oktalen bzw. hexadezimalen Zahlen besser im Gedächtnis behalten (8916 ist einfacher zu merken als 100010012 ). Diese Eigenschaft macht sich auch das eingangs erläuterte Abstraktionsprinzip zu nutze: Bei der Assemblerprogrammierung wird das hexadezimale System sehr häufig verwendet. Darüberhinaus sind Oktal- und Hexadezimalzahlen kompakter als ihre binären Entsprechungen (1 Oktalziffer = 3 Binärziffern, 1 Hexziffer = 4 Binärziffern), weswegen sie häufig in Computertexten und core dumps (Kernspeicherauszüge, wie sie beim Debuggen systemnaher Programme üblich sind) benutzt werden.

9.2. Zahlbasiswechsel Die Umwandlung von Oktal- oder Hexadezimalzahlen und Binärzahlen ist einfach. Um eine Binärzahl in eine oktale umzuwandeln, teilt man sie in Gruppen zu drei Bits auf, mit den 3 Bits direkt links oder rechts vom Dezimalpunkt als jeweils ein Gruppe und dann jeweils fortschreitend nach links oder rechts. Jede Dreiergruppe lässt sich direkt in eine einzelne Oktalziffer (0...7) umwandeln. Es kann nötig sein, eine oder zwei führende oder folgende Nullen anzufügen, damit eine vollständige Dreiergruppe entsteht. Die Rücktransformation ist ebenso trivial. Jede oktale Ziffer wird lediglich durch die entsprechende Dreibitbinärzahl ersetzt. Eine Umwandlung zwischen hexadezimalem und binärem System wird nach dem gleichen grundlegenden Prinzip vorgenommen, nur das hier statt Dreiergruppen jeweils vier Bit zusammengefasst werden (Abbildung 9.2). Hexadezimal 0 1 2 3 4 5 6 7

Binär 0000 0001 0010 0011 0100 0101 0110 0111

Hexadezimal 8 9 a b c d e f

Binär 1000 1001 1010 1011 1100 1101 1110 1111

Tabelle 9.2.: Umrechnung zwischen Hexadezimal und Binär In der Abbildung sind in der Binärdarstellung die führenden Nullen, die man normalerweise nicht mitschreibt, enthalten. Dies hat den Hintergrund, dass man gewöhnlich Hexzahlen aus mehreren Stellen auf einmal umwandelt und jede Hex-Ziffer für vier Binärstellen steht.

136

Technische Informatik I

9.2. Zahlbasiswechsel Zahlen des Dezimalsystems umzuwandeln, ist etwas schwieriger. Stellvertretend für den binären Block (binär, oktal, hexadezimal) betrachten wir nur die Umwandlung zwischen binärem und dezimalem System.

9.2.1. Dezimal zu binär, oktal, hexadezimal Die Umwandlung von dezimalen Zahlen in Binärzahlen kann über zwei unterschiedliche Wege erfolgen. Die erste Methode folgt direkt aus der Definition binärer Zahlen. Die größte Zweierpotenz, die kleiner als die umzuwandelnde Zahl ist, wird von eben dieser abgezogen. Das Verfahren wird mit der Differenz wiederholt. Sobald die Zahl in Zweierpotenzen zerlegt wurde, kann die Binärzahl aus Einsen, wenn die entsprechende Potenz ein Summand der zerlegten Zahl ist, und Nullen sonst zusammengesetzt werden. Ein Beispiel für die Zahl 1500: 1500−

1024 =

476

1024 = 210

476−

256 =

220

256 = 28

220−

128 =

92

128 = 27

92−

64 =

28

64 = 26

28−

16 =

12

16 = 24

12−

8=

4

8 = 23

4−

4=

0

4 = 22

Setzt man in einer Binärzahl 210 29 28 27 26 25 24 23 22 21 20 nun alle Zweierpotenzstellen, die in dieser Rechnung auftauchen, auf 1 und die übrigen auf 0, so erhält man 1500 als Binärzahl, nämlich 10111011100. Die andere Methode, die allerdings nur bei Ganzzahlen anwendbar ist, besteht darin, die Zahl jeweils direkt durch Zwei zu dividieren. Der Quotient wird dabei direkt unter die Originalzahl geschrieben und der Rest, 0 oder 1, daneben. Dann wird dieser Quotient betrachtet und das Verfahren so lange erneut angewendet, bis die Zahl Null erreicht ist. Als Ergebnis erhält man zwei Spalten mit Zahlen: Die Quotienten und die Reste. 1500 : 2 =

750

Rest 0

750 : 2 =

375

Rest 0

375 : 2 =

187

Rest 1

187 : 2 =

93

Rest 1

93 : 2 =

46

Rest 1

46 : 2 =

23

Rest 0

23 : 2 =

11

Rest 1

11 : 2 =

5

Rest 1

5:2=

2

Rest 1

2:2=

1

Rest 0

1:2=

0

Rest 1

Technische Informatik I

137

9. Rechnerarithmetik Die Binärzahl lässt sich nun direkt aus der Restwertspalte auslesen, nämlich von unten nach oben. Dezimal-zu-oktal- und Dezimal-zu-hexadezimal-Umwandlungen lassen sich leicht bewerkstelligen, indem man die Zahl erst binär darstellt und anschließend wie oben beschrieben umwandelt. Andererseits kann man auch gleich durch 8 bzw. 16 dividieren (nach dem eben erläuterten Verfahren).

9.2.2. Binär zu dezimal Auch für den umgekehrten Weg gibt es auch wieder zwei Möglichkeiten. Die erste besteht darin, einfach die Zweierpotenzen, denen die Einsen in der Binärzahl entsprechen, aufzuaddieren. Zum Beispiel ist 101102 = 24 + 22 + 21 = 16 + 4 + 2 = 22. Eine methodischere Technik basiert auf dem folgenden rekursiven Algorithmus. Man nimmt das am weitesten links stehende (höchstwertige) Bit, das nicht Null ist. Dieses verdoppelt man und addiert es zum Bit rechts daneben. Dieses Ergebnis nimmt man, verdoppelt es und addiert es wieder zum nächstrechten Bit. Dies wiederholt man, bis das kleinste Bit auch hinzuaddiert wurde. Diese rekursive Prozedur ließe sich mathematisch durch (a 0 + 2 (a 1 + 2 (a 2 + . . .)))

beschreiben, wobei a0 das kleinste Bit der Binärzahl ist.

9.2.3. Oktal zu dezimal Man erinnere sich, dass jede Regel, die auf ein System mit Basis X zutrifft, ebenfalls in einem System mit Basis Y gilt. Wenn wir also Binärzahlen in Dezimalzahlen umwandeln, indem wir 2ai zu ai −1 addieren, so können wir Oktalzahlen zu Dezimalzahlen machen, wenn wir durchgängig 8ai zu ai −1 hinzuaddieren. Eine Oktalzahl lässt sich mathematisch als (a 0 + 8 (a 1 + 8 (a 2 + . . .)))

beschreiben. Wir nehmen uns die am weitesten links stehende Ziffer, multiplizieren sie mit Acht und addieren sie zur Ziffer zu ihrer Rechten. Dieses Zwischenergebnis verachtfachen wir wieder und addieren es wieder zur rechten Ziffer. Das Verfahren endet, wenn wir bei der kleinsten Ziffer rechts außen angekommen sind.

9.2.4. Hexadezimal zu dezimal Die Methode ist identisch zu der für oktale und binäre Zahlen, wie sie oben beschrieben ist. Allerdings verwenden wir hier die 16 als Faktor.

9.3. BCD – Binary Coded Decimal Binary Coded Decimal (binärkodierte Dezimalzahl) ist eine weitere Möglichkeit der Zahlendarstellung in Binärform. Der Name deutet hierbei schon das Prinzip an: So wird hierbei nicht die gesamte Dezimalzahl in eine Binärzahl umgewandelt, sondern jede einzelne Dezimalziffer für sich (473910 entspricht 01000111001110012 in der BCD-Darstellung). Wie

138

Technische Informatik I

9.4. Darstellung ganzer Zahlen mit Vorzeichen man sieht, ist BCD auch nur eine Form der Dezimaldarstellung, die sich von der klassischen Form nur in der Weise unterscheidet, wie die zehn Ziffern dargestellt werden. Um der Logik mit ihrer Zweiwertigkeit (0 und 1) Rechnung zu tragen, werden hier für jede Dezimalziffer binäre 4-Tupel verwendet. Diese Darstellung ist für uns Menschen, die an Dezimalzahlen gewohnt sind, am einfachsten. Der größte Vorteil des BCD-Codes ist die einfache Umwandlung von dezimaler in binäre Darstellung. Das Konvertierungsprinzip ist praktisch identisch zu dem der Unwandlung zwischen hexadezimalen und binären Zahlen, allerdings werden hier nur zehn der 16 möglichen Bitmuster genutzt. Daraus resultiert auch der größte Nachteil dieser Zahlendarstellung, und zwar in zweierlei Hinsicht. Zum ersten verkompliziert sich die Arithmetik. 473910 +128710 602610

0100 +0001 0101 510

0111 0010 1001 910

0011 1000 1100 1210

1001 0111 0000 010

= 59?010

Tabelle 9.3.: Neue Rechenprobleme bei einer BCD-Addition Wie man im Beispiel in Tabelle 9.3 sieht, treten bei der Addition der 4-Tupel undefinierte Werte auf, die letzte Ziffer geht sogar ganz verloren (7 + 9 = 16). Dies liegt daran, dass der Übertrag dort erst bei 16, und nicht bei zehn erfolgt. Es muss eine sogenannte Dezimalanpassung erfolgen, sodass man – von rechts beginnend – die Dualkodierung der „verlorenen“ Sechs (01102 ) bei jedem Übertrag und bei jeder ungültigen BCD-Ziffer (die durch einen Übertrag korrigiert werden muss) nachträglich addiert. Für das Beispiel bedeutet dies, dass wir bei der letzten Stelle des offenbar falschen Ergebnisses 0101 1001 1100 0000, bei der es ja einen Übertrag gab, eine 6 (binär 110) addieren. Bei der zweitletzten Stelle gab es zwar keinen Übertrag, aber als Ergebnis eine Zahl, die nicht im Zahlenbereich 0–9 liegt. Auch hier wird eine 6 addiert (1100 + 110 = 0010), wobei wieder ein Übertrag entsteht. Dieser wird der nächsthöheren Stelle hinzugefügt. Dadurch gerät die Zahl aber aus dem gültigen Zahlenbereich (1001 + 1 = 1010, dezimal 11); es muss eine 6 addiert werden. Dadurch entsteht wieder ein Übertrag (1010 + 110 = 0000). Der Übertrag wird auf die höchstwertige Hex-Ziffer addiert. Das Ergebnis lautet folglich 602610 . Grundsätzlich hat die BCD-Darstellung einen schwer wiegenden Nachteil: Sie geht aufgrund der nicht verwendeten Binärbelegungsmöglichkeiten (hex A, B, C, D, E, F) verschwenderisch mit Speicherplatz um. Dadurch ist sie vergleichsweise ineffizient. In Zahlen sieht dies folgendermaßen aus: Um eine Dezimalzahl binär darzustellen, erhöht sich die Stellenanzahl (also die Länge der Zahl) um Faktor 3,3. Die Umwandlung einer Dezimalzahl in die BCD-Repräsentation verursacht jedoch immer eine Vervierfachung der Länge. Mit anderen Worten: BCD-Zahlen brauchen etwa 20 % mehr Speicher als „natürliche“ Binärzahlen.

9.4. Darstellung ganzer Zahlen mit Vorzeichen Die Darstellung positiver Ganzzahlen im Binärsystem ist vergleichsweise einfach, da es ein im Vergleich zum Dezimalsystem entsprechendes System zur Bildung der Zahlen anbietet: Die Zahl ist rechts ausgerichtet und man beginnt damit, die letzte Stelle so lange

Technische Informatik I

139

9. Rechnerarithmetik zu erhöhen, bis der Zeichenvorrat aufgebraucht ist. Beim Dezimalsystem durchläuft die Stelle dabei die Zahlen von 0–9, beim Dualsystem dagegen ist nach der 1 schon Schluss. Um die nachfolgende Zahl darzustellen, muss die Stelle links der erhöhten Stelle um 1 erhöht werden und die ursprünglich betrachtete Stelle wird zurück auf 0 gesetzt. Beispielsweise muss beim Übergang von der 9 zur 10 die Stelle links der 9 (eine implizite 0) um 1 erhöht werden und die 9 zurück auf 0 gesetzt werden. Im Binärsystem sieht es ähnlich aus: Um von der 1 zur 10 zu kommen, wird die Stelle links der 1 (auch hier eine implizite 0) um 1 erhöht und die ursprüngliche 1 zurück auf 0 gesetzt. Die Darstellung negativer Zahlen dagegen stellt ein Problem dar: Wie wir sehen werden, ist die bei Dezimalzahlen verwendete Darstellungsart nicht sehr effizient. Es lässt sich nämlich recht schwer damit rechnen. Diese Darstellungsart heißt Vorzeichen-/BetragDarstellung.

9.4.1. Vorzeichen-/Betrag-Darstellung Bei der Darstellung einer Zahl durch Vorzeichen und Betrag wird links der Zahl ein Vorzeichen gesetzt: Ein + für positive Zahlen, ein − für negative Zahlen und kein Vorzeichen für die 0. Diese Konvention stellt das Binärsystem vor ein Problem, da sich drei Zustände nicht mit einem Bit, sondern nur (mindestens) mit zwei Bit darstellen lassen. Pragmatischerweise ordnet man der 0 daher ein Vorzeichen zu – denn zwei Zustände lassen sich mit genau einem Bit darstellen. Bei der typischen Vorzeichen-/Betrag-Darstellung wird das höchstwertige Bit eines Wortes exklusiv für die Angabe des Vorzeichens genutzt. Eine Zahl Z lässt sich durch Z = (−1)S · M bezeichnen, wobei S das Vorzeichen (sign) und M der Betrag (magnitude) ist. Wenn das Vorzeichenbit gleich 0 ist, gilt (−1)0 = 1 und die Z ist positiv, andererseits gilt (−1)1 = −1 und Z ist negativ. Betrachten wir als Beispiel ein Wort der Länge acht Bit: Bei Ganzzahlen ohne Vorzeichen ist der Wertebereich [0, 255]. Verwendet man die Vorzeichen-/Betrag-Darstellung, so repräsentieren sieben Bit den Betrag der Zahl und ein Bit das Vorzeichen. Der Wertebereich ist damit [−127, 127] oder [111111112 , 011111112 ]. Wer genau nachzählt, wird feststellen, dass damit nur 255 verschiedene Werte möglich sind. Prinzipiell kann man mit acht Bit aber 28 = 256 verschiedene Zustände realisieren. Der „fehlende“ Zustand liegt in der Darstellung der 0 begründet: Diese gibt es sowohl mit positivem als auch mit negativem Vorzeichen, also doppelt. Ein Standardvergleicher (siehe Abbildung 6.13) würde diese beiden Zahlen (000000002 und 100000002 ) naturgemäß als ungleich ansehen, obwohl sie den gleichen Wert meinen. Bei Additionen und Subtraktionen muss entschieden werden, ob addiert oder subtrahiert werden muss, in welcher Operandenreihenfolge und welches Vorzeichen das Ergebnis haben wird. Die möglichen Fälle sind in Tabelle 9.4 dargestellt. Wohlgemerkt: Dieser Aufwand kommt zur eigentlichen Rechnung hinzu! Bei Binärzahlen bietet sich aufgrund der Zweiwertigkeit der Zahlen ein anderer Ansatz an: Die Komplementdarstellung, bei der negative Zahlen den invertierten positiven Zahlen gleichen Betrags entsprechen (evtl. mit weiteren Modifikationen).

140

Technische Informatik I

9.4. Darstellung ganzer Zahlen mit Vorzeichen Operanden

Auszuführende Operation ¡ ¢ + x+y Addition ¡ ¢ − x+y Addition ¡ ¢ + x−y Subtraktion ¡ ¢

+x, +y −x, −y ¯ ¯ +x, −y mit |x| ≥ ¯ y ¯¯oder ¯ −x, +y mit |x| ≤ ¯ y ¯ ¯ ¯ +x, −y mit |x| < ¯¯ y ¯¯ oder −x, +y mit |x| > ¯ y ¯ oder

Tabelle 9.4.: Fallunterscheidung in Addition/Subtraktion

+ y −x ¡ ¢ − x−y ¡ ¢ − y −x

der

Subtraktion

Vorzeichen-/Betrag-Darstellung

bei

9.4.2. Einerkomplement-Darstellung Wie bei der Vorzeichen/Betrag-Darstellung dient auch bei der Komplement-Darstellung das erste Bit zur Darstellung des Vorzeichens. Der Wert wird allerdings anders dargestellt: Man kippt sämtliche Bits des Betrags in positiver Darstellung. Das Vorzeichenbit ist Teil des Summanden und wird in eine arithmetische Operation mit ¡ ¢ eingeschlossen. Die Subtraktion wird auf die Addition zurückgeführt (x − y = x + −y ); somit besteht keine Notwendigkeit für ein zusätzliches Subtrahierwerk. Beim Einerkomplement wird eine Zahl N aus dem Wertebereich 0, 2n−1 − 1 durch bit£ n−1 ¤ weises Invertieren in die Zahl −N aus dem Wertebereich −2 − 1, 0 umgewandelt. Wie man sieht, ist auch hier die Null doppelt vorhanden. £

¤

Da N eine bestimmte Länge besitzt (nämlich n ), kann man die negative Entsprechung einer Zahl auch rechnerisch bestimmen: −N = 2n − N − 1. Mit dieser Formel erhält man die positive Ganzzahl, die als Einerkomplement interpretiert, die gewünschte Zahl −N ergibt. Beispiel: Nehmen wir an, wir haben eine Fünf-Bit-Zahl (also n = 5) und interessieren uns für die Darstellung der Zahl −9 (also N = 9). Das Einerkomplement ist folglich −910 = 25 − 9 − 1 = 22. N ist binär 010012 . 2210 = 101102 , was genau dem Inversen von 010012 = 910 entspricht. Die Subtraktion ist dabei gleich der Addition plus einem end-around-carry bzw. Carry-Out (im Nachfolgenden durch 1 gekennzeichnet), d. h. zu der Summe wird das am weitesten links stehende Bit der Summanden aufaddiert. Sehen wir uns dies näher an: 1. x − y mit x > y x = 9 = 010012 y = −410 = −001002 = 10110 01001 + 11011 =1 00100 00100+1 = 001012 = 510 ¡ ¢ ¡ ¢ Das Carry-Out ist bei x > y immer 1. x − y = x + 2n − y − 1 = 2n + x − y − 1 Dies ist genau x −y , wenn man das Carry-Out streicht und den Wert 1 hinzuaddiert.

2. x − y mit x < y x = 4 = 001002 = 00100 y = −910 = −010012 = 10110 00100 + 10110 = 11010 11010 = −001012 = −510 Das Carry-Out ist bei x > y immer 0. Offenbar gilt somit, dass auch ohne diesen

letzten Schritt des Carry-Out-Addierens das korrekte Ergebnis berechnet wird.

Technische Informatik I

141

9. Rechnerarithmetik ¡

3. (−x) + −y

¢

x = −410 = −001002 = 11011 y = −910 = −010012 = 10110 11011 + 10110 =1 10001 10001+1 = 10010 10010 = −011012 = −1310

Das Carry-Out ist bei zwei negativen Zahlen immer 1. Wie man sieht, ist für das Rechnen mit dem Einerkomplement kein zusätzliches Subtrahierwerk nötig. Allerdings gibt es immer noch keine eindeutige Darstellung der Null. Außerdem muss ein eventuelles Übertragsbit in einem anschließenden Rechenschritt addiert werden. Mit dem Zweierkomplement entfällt dieser und es gibt nur noch eine 0.

9.4.3. Zweierkomplement-Darstellung In der Zweierkomplement-Darstellung ist −N = 2n − N . Bestimmen kann man die negative Entsprechung der Zahl N im Zweierkomplement durch Zuhilfenahme des Einerkomplements: −N = Einerkomplement+1. Man invertiert also die Zahl N bitweise und addiert schließlich eine 1. −N ist dabei aus dem Wertebereich [−2n − 1, −1] . Die Darstellung der £ ¤ Null ist eindeutig. Der Wertebereich des Zweierkomplements ist −2n−1 , 2n−1 − 1 .

Abbildung 9.1.: Zweierkomplement-Darstellung am Zahlenkreis Die Zweierkomplement-Darstellung lässt sich an einem Zahlenkreis veranschaulichen, wie er in Abbildung 9.1 gezeigt wird. Additionen entsprechen einer schrittweisen Bewegung im Uhrzeigersinn und Subtraktionen der entgegengesetzten Richtung. Ein Beispiel: −4+3 soll berechnet werden. Binär entspricht dies der Rechnung von 1100+0011, was offensichtlich 1111 ergibt, genau der Darstellung von −1 im Zweierkomplement. Nehmen wir nun die Beispiele, bei denen wir beim Einer-Komplement aufgrund des Carry-out noch die Korrekturoperation (+1) machen mussten: 1. x − y mit x > y x = 510 = 1012 y = −310 = −00112 + 1 = 1100 + 1 = 1101 0101 + 1101 =1 0010

142

Technische Informatik I

9.5. Überlauf 00102 = 210

Wie man sieht, kann man das Carry-Out ignorieren. Es ist keine Korrekturaddition notwendig. 2. −x − y x = −310 = 11012 y = −410 = 11002 1101 + 1100 =1 1001 10012 = −710

Hier ist ebenfalls keine Korrekturaddition notwendig Generell gilt: Additionen mit Zahlen in Zweierkomplement-Darstellung bedürfen wie in den Beispielen oben gezeigt nur eines einzigen Rechenschrittes. Solange das Ergebnis der Addition den Wertebereich nicht überschreitet, ist die Berechnung damit abgeschlossen. Die Subtraktion lässt sich einfach in die Addition überführen: Man negiert die Zahl, die abgezogen werden soll, und addiert im Anschluss. Aufgrund dieser für Berechnungen günstigen Eigenschaften wird in Computern bei Ganzzahlen mit Vorzeichen die Zweierkomplementdarstellung verwendet. Es sei darauf hingewiesen, dass auch das Zweierkomplement nachteilige Aspekte hat: Nicht jeder Betrag kann sowohl positiv als auch negativ dargestellt werden. Das untere Extremum des Wertebereichs hat nämlich keine mögliche positive Entsprechung. Beispiel: Mit sieben Bits kann man die Zahl −128 darstellen, aber nicht die Zahl +128, sondern höchstens +127. Dies liegt daran, dass die Null den positiven Zahlen zugeordnet wurde.

9.5. Überlauf Als Überlauf (Overflow) bezeichnet man das Ergebnis einer Operation, deren Summe außerhalb des Wertebereiches liegt. Eine Erkennung des Überlaufs ist wesentlich für den korrekten Ablauf einer Berechnung. Bei Addition von natürlichen Zahlen kann das Carry-Out-Bit als Indikator für einen Überlauf dienen. Bei Addition ganzer Zahlen gilt dies nicht. Weiterhin ergibt die Addition von Summanden mit unterschiedlichen Vorzeichen nie einen Überlauf, da der absolute Wert ihrer Summe immer kleiner als der absolute Wert eines der beiden Summanden ist. Demzufolge tritt ein Überlauf nur genau dann auf, wenn beide Summanden das gleiche Vorzeichen haben. Dies soll an den folgenden 4-BitBeispielen verdeutlich werden:

01112

+ 710

+01002

+ 410

= 0 10112

− 510

11002

− 410

+10102

− 610

= 1 01102

+ 610

Falsches Ergebnis, aber Carry-Out ist 0

Falsches Ergebnis und Carry-Out ist 1

Technische Informatik I

143

9. Rechnerarithmetik Günstigerweise gibt es eine Schaltfunktion, mit der man auf einen Überlauf schließen kann: O = a n−1 b n−1 s n−1 + a n−1 b n−1 s n−1

Die Faktoren repräsentieren hier die Vorzeichenbits der Summanden a und b sowie der Summe s . Diese Schaltfunktion kann in ein Rechenwerk integriert werden. Über sie wird ein Signal (ein so genanntes Flag) im Condition Code Register (CCR) gesetzt, in dem bestimmte Eigenschaften von Operationsergebnissen hinterlegt werden. Das Überlaufsignal (meistens mit V bezeichnet) wird oft zur Erzeugung eines Interrupts genutzt, welcher auf eine Fehlerbehandlung (exception handling) auf Programmebene hinausläuft, d. h. der Programmierer entscheidet über den weiteren Verlauf der Ausführung.

9.6. Addition Mit Gattern der booleschen Algebra und den Grundlagen der Binärarithmetik haben wir die Dinge kennengelernt, die für die Entwicklung einer Addierschaltung für binäre Zahlen notwendig sind. Im Folgenden werden verschiedene Konzepte dazu vorgestellt.

9.6.1. Halbaddierer Die einfachste Schaltung, die zur Binäraddition genutzt wird, nennt sich Halbaddierer (half adder). Er addiert zwei Bits A und B zu einer Summe S und einem Übertrag (carry) C (Tabelle 9.5). A

B

S

C

0 0 1 1

0 1 0 1

0 1 1 0

0 0 0 1

Tabelle 9.5.: Wahrheitstabelle eines Halbaddierers Wie man aus der Tabelle erkennen kann, ist S = AB + AB = A ⊕ B , also A XOR B , berechenbar. Der Übertrag C berechnet sich als A UND B , also C = AB . Aus dem früheren Kapitel zu Gattern wissen wir bereits, dass sich diese Funktion durch mindestens drei verschiedene Schaltungen realisieren lässt. Aus Abstraktionsgründen wird die Schaltung daher unabhängig von der eigentlichen Implementierung durch ein Symbol symbolisiert, wie es in Abbildung 9.2 dargestellt ist.

9.6.2. Volladdierer Ein Halbaddierer genügt so lange, wie nur das Ergebnis einer Addition zweier Bits berechnet werden soll. Normalerweise werden jedoch ganze Bitketten addiert, wobei zusätzlich ein Übertrag als Eingabe berücksichtigt werden muss. Aus der Addition der Zahlen A und B wird also die Addition dieser beiden Zahlen und des Übertrags.

144

Technische Informatik I

9.6. Addition

Abbildung 9.2.: Symbol eines Halbaddierers

a n−1 . . . a 2 a 1 a 0 +b n−1 . . . b 2 b 1 b 0 +c n−1 . . . c 2 c 1 c 0 c 1 entspricht dabei beispielsweise dem Übertrag, der sich aus der Addition a 0 + b 0 + c 0 ergibt. Wie man sieht, kann der Berechnung mit c 0 von außen mitgeteilt werden, ob ei-

ne vorherige Berechnung einen Übertrag verursacht hat. So lassen sich Additionen von Zahlen realisieren, die länger als die n Bits sind, wobei n beispielsweise eine Hardwarebeschränkung ist. Außerdem stellt diese zusätzliche Eingabe eine einfache Möglichkeit dar, die Zahl um 1 zu erhöhen. Dies kann beispielsweise bei der Bildung einer negativen Ganzzahl in Zweierkomplement-Darstellung nach Bildung des Einerkomplements praktisch sein. c in

0 0 0 0 1 1 1 1

a

b

s

c out

0 0 1 1 0 0 1 1

0 1 0 1 0 1 0 1

0 1 1 0 1 0 0 1

0 0 0 1 0 1 1 1

Tabelle 9.6.: Wahrheitstabelle eines Volladdierers Ein Volladdierer entspricht der Wahrheitstabelle, die in Tabelle 9.6 abgebildet ist. Eine einfache Möglichkeit einen Volladdierer zu implementieren, ist zwei Halbaddierer in Reihe zu schalten (siehe Abbildung 9.3). Im Prinzip bedeutet dies, dass man die beiden Bits von A und B miteinander addiert, um wiederum das daraus entstehende Ergebnis mit dem Carry-In zu addieren. Der Ausgang S des Volladdierers wird durch den Summenausgang des zweiten Halbaddierers bestimmt, während der Übertrag C out durch ODERVerknüpfung der beiden Überträge der Halbaddierer bestimmt wird. In der Praxis wird man eine solche Implementierung nur selten finden, da der Pfad der Berechnung durch die beiden Halbaddierer eine Verzögerung von sechs Zeiteinheiten hervorruft. Eine alternative Volladdiererschaltung kann direkt aus den Gleichungen für die Summe und dem Übertrag aus der Wahrheitstabelle abgeleitet werden:

Technische Informatik I

145

9. Rechnerarithmetik

a)

b)

Abbildung 9.3.: Volladdierer aus Halbaddierern

s = c ab + c ab + c a b + c ab

und

c out =

c ab + c ab + c ab + c ab ³ ´ c ab + c ab + c a b + b

=

c ab + c ab + c a ¡ ¢ c ab + c ab + a

= = =

c ab + c (b + a) = c ab + cb + c a ¡ ¢ a cb + c + cb = a (b + c) + cb

=

c a + cb + ab

=

Das Übertragsbit entspricht also genau dem in Abschnitt 6.2.1 kennengelernten Mehrheitsentscheider. Es ist also 1, wenn mindestens zwei Eingänge 1 sind. Abbildung 9.4 zeigt einen Schaltplan für s und c out . Beachten Sie, dass die maximale Schaltverzögerung dieser Schaltung drei Gatterlaufzeiten beträgt. Das parallel berechenbare Carry-Out benötigt nur zwei Gatterlaufzeiten (siehe Abbildung 6.6). Die schnellere Berechnung gegenüber den in Reihe geschalteten Halbaddierern erkauft man sich durch mehr verwendete Gatter.

9.6.3. Addition von Wörtern Die bisherige Überlegung betraf weitgehend das Addieren zweier Bits. Nun ist es aber so, dass in Computern selten nur einzelne Bits, sondern in der Regel ganze Wörter addiert werden. Wir werden uns deshalb im Folgenden mit Möglichkeiten auseinandersetzen, zwei n -Bit-Zahlen mit Volladdierern zu addieren. Werfen wir zunächst einen Blick auf den seriellen Volladdierer und betrachten wir anschließend den parallelen Volladdierer.

146

Technische Informatik I

9.6. Addition

Abbildung 9.4.: Schaltnetz eines Volladdierers Es ist sehr gut möglich, zwei n -Bit-Zahlen A und B seriell miteinander zu addieren, wobei man jeweils ein Bit des Ergebnisses während eines Taktes miteinander addiert. Betrachten wir dazu Abbildung 9.5.

Abbildung 9.5.: Serieller Volladdierer Die Inhalte der Schieberegister, in welchen die n -Bit-Zahlen A und B stehen, werden jeweils mit einem Bit pro Takt in den Volladdierer geschoben. Das Ergebnis jeder Addition wird dann weiter in ein Ergebnisregister geschoben. Ein einzelnes Flipflop nimmt sich des Carry-Bits, also des Übertrages an, sodass das alte Carry-Out jeweils zum neuen Carry-In wird. Nach n Taktimpulsen beinhaltet das Summenregister S die Summe von A und B . Serielle Addierer finden sich häufig in älteren Lehrbüchern mit der Bemerkung, dass sie aufgrund der Tatsache, dass sie nur einen Volladdierer benötigen, günstiger als Paralleladdierer sind. Solche Überlegungen mögen in der Vergangenheit richtig gewesen sein, nicht aber mehr heute. Mit der heutigen Möglichkeit, mehrere hunderttausend Transistoren auf einem einzigen Chip unterzubringen, kann eine Schaltung auf Höchstleistung

Technische Informatik I

147

9. Rechnerarithmetik anstatt auf minimale Gatteranzahl hin entwickelt werden. In diesem Skript findet sich der serielle Volladdierer dennoch, damit der Kontrast zwischen seriellen (Bit für Bit) und parallelen Operationen, bei denen alle Bits gleichzeitig verarbeitet werden, deutlich wird. Wenn das Carry-out des höchstwertigen Bits gleich 1 ist, tritt ein Überlauf während der Operation auf, was zur Folge hat, dass n Bits nicht ausreichen, um das Ergebnis darzustellen. Die praktischere Variante der Addierer ist der parallele Volladdierer (siehe Abbildung 9.6), welcher sich n Volladdierern bedient, um zwei n -Bit-Zahlen miteinander zu addieren. Das Carry-Out jedes Volladdierers wird dabei als Carry-In des nächsten verwendet. Die n Bits von Zahl A und die n Bits von Zahl B werden dabei in einem gleichzeitigen Schritt addiert. Der Begriff „parallel“ weist auch darauf hin, dass alle n Additionen zur gleichen Zeit stattfinden. Man ist sicherlich geneigt anzunehmen, dass der parallele Addierer n -mal so schnell wie sein serielles Gegenstück ist. In der Praxis wird ein paralleler Addierer jedoch dadurch verlangsamt, dass der Übertrag durch alle Stufen hindurchgereicht werden muss. Darauf kommen wir gleich noch einmal zurück.

Abbildung 9.6.: Paralleler Volladdierer Das erste Carry-In, welches bei der Addition von a0 und b0 (die niederwertigsten Bits der Zahlen A und B ) verwendet wird, lässt sich auch beim parallelen Volladdierer wieder dazu nutzen, zwischen einer normalen Addition (Carry-In = 0) oder zur Erzeugung von A + B + 1 zu wählen. Letzteres ist z. B. bei der Erzeugung des Zweier-Komplements oder zu Inkrementierung von A (wenn man für B einfach den Wert 010 annimmt) von Nutzen. Das Carry-Out des höchstwertigen Bits lässt sich an ein Flipflop weiterreichen, dass z. B. Teil des Condition Code Registers sein könnte. Wenn die Addition durch Software als Teil eines Programms durchgeführt wird, testet der Programmierer für gewöhnlich das Carry-Bit, ob das Ergebnis der Addition außerhalb des Wertebereiches liegt (ÜberlaufErkennung). Eine letzte Bemerkung zum parallelen Volladdierer betrifft die Bedeutung des Begriffes parallel. Es muss klar sein, dass, während a0 und b0 zu s 0 in der ersten Stufe zusammenaddiert werden, sobald die Zahlen A und B an den Eingängen des Volladdierers anliegen, die zweite Stufe stattdessen auf den Übertrag der ersten warten muss, bevor dieser bei der Addition von a1 und b1 berücksichtigt werden kann und somit s 0 einen gültigen Wert annimmt. Der schlechteste Fall ist die Addition der Zahlen 11 . . . 12 und 12 . Bei diesem muss der Übertrag sich durch alle Stufen fortpflanzen (hier könnte man wieder den Begriff „ripple-through“ – „durchplätschern“ verwenden). Der hier beschriebene Volladdierer heißt deswegen parallel, weil alle Bits von A in einem Schritt zu den Bits von B addiert werden, ohne dass die Notwendigkeit für eine gewissen Anzahl Taktzyklen besteht. Wenn die Werte von A und B an die Eingänge des Volladdierers angelegt wur-

148

Technische Informatik I

9.6. Addition den, muss das System warten, bis die Schaltung diese verarbeitet hat und alle Überträge durchgereicht wurden, bevor die nächste Operation erfolgen kann.

9.6.4. Vorausschauende Addierer Es wird davon ausgegangen, dass eine arithmetische Operation in einem Taktzyklus vom Rechenwerk durchgeführt werden kann. Dies bedeutet, dass ein Prozessor, der mit einer Taktrate von 100 MHz arbeitet, eine Addition in 10−8 s (10 ns) abgeschlossen haben muss (100 MHz = 108 s−1 ). Die Verzögerung eines n -Bit-Addierers kann dabei wie folgt bestimmt werden: Nehmen wir an, dass die Verzögerung von einem Übertrag c i zum nächsten Übertrag c i +1 jeder Addiererstufe in der prozessorinternen Schaltung 1 ns beträgt. Eine n -Bit-Addition kann also in der Zeit ausgeführt werden, die nötig ist, damit der Übertrag die Position c n−1 erreicht, plus der Zeit, um die letzte Volladdiererstufe s n−1 zu errechnen. Wenn für Letzteres eine Verzögerung von 1, 5 ns annehmen, würde eine 32-Bit-Addition (31 · 1 ns) + 1, 5 ns = 32, 5 ns benötigen (Die Überlegungen gehen von einer Gatterverzögerung von 0, 5 ns aus und eine Addition hat drei Gatter in Serie). Die Taktgeschwindigkeit hat mithin den größten Einfluss auf die Leistung eines Prozessors. Es ist sehr wichtig, die arithmetischen Operationen in möglichst wenigen Taktzyklen (optimal einem) abschließen zu können. Die Zeit, die nötig ist, um eine arithmetische Operation durchzuführen, hat einen dominanten Effekt auf die zu erzielende Taktrate. Es gibt zwei Ansätze, um die Verzögerung unter die gewünschten 10 ns zu bringen. Der erste Ansatz ist die Nutzung von schnelleren Schaltungstechnologien, mit denen man die Logik der Übertragweitergabe implementiert. Der zweite Ansatz besteht darin, ein andere Struktur der Logik als der oben verwendeten zu entwerfen. Die logischen Strukturen für die Entwicklung schneller Addierer müssen die Erzeugung der Überträge beschleunigen. Die logischen Ausdrücke für s i (Summe) und c i +1 (CarryOut) der Stufe i sind s i = x i y i c i + x i y i c i + x i y i c i + x i y i c i¡ und c i¢+1 = y i c i + x i c i + x i y i . Dabei lässt sich die zweite Gleichung auch als c i +1 = x i y i + x i + y i c i schreiben. Diesen Term kann man unterteilen: Der erste Teil erzeugt auf dieser Stufe ggf. einen Übertrag; man nennt ihn darum auch Generierungsfunktion (g i = x i y i ). Der hintere Teil pflanzt, falls c i = 1 ist, möglicherweise den Übertrag fort und heißt daher Fortpflanzungsfunktion ¡ ¢ (p i = x i + y i ; p steht für propagate). Man kann also auch c i +1 = g i +p i c i schreiben. Offensichtlich lassen sich g i und p i berechnen, sobald X und Y anliegen, und zwar innerhalb einer Gatterlaufzeit. Sobald c i bekannt ist, dauert es eine weitere Gatterlaufzeit, bis p i c i berechnet ist, und noch eine Gatterlaufzeit, bis der Übertrag c i +1 = g i + p i c i berechnet ist. So betrachtet ist es von wesentlichem Interesse, die Berechnung von c i zu beschleunigen. Ansonsten hätte man wieder den Effekt des parallelen Addierers, dass höherwertige Stellen auf die Überträge der niederwertigen Stellen warten müssen. Für i = 0, also das niederwertigste Bit, ist der Übertrag c 0 vorgegeben: Es gehört zu den Eingaben, die von außen kommen. Dadurch lässt sich c 1 = g 0 + p 0 c 0 direkt, das heißt in drei Gatterlaufzeiten, berechnen. Um die Stelle links davon (i = 1) schnell berechnen zu berechnen. Um die Stelle links davon (i = 1) schnell berechnen zu können, setzt man in¡ c 2 = g 1 +¢ p 1 c 1 für den Übertrag c 1 entsprechend g 0 + p 0 c 0 ein und erhält c 2 = g 1 + p 1 g 0 + p 0 c 0 = g 1 + g 0 p 1 + p 1 p 0 c 0 . Der Übertrag c 2 lässt sich somit auch in drei Gatterlaufzeiten berechnen. Auch für c 3 lässt sich dieses Einsetzungsverfahren fortführen, was zu c 3 = g 2 + g 1 p 2 + g 0 p 2 p 1 + p 2 p 1 p 0 c 0 führt. Für die übrigen Stellen des zu addieren-

Technische Informatik I

149

9. Rechnerarithmetik den Wortes lassen sich ebenfalls derlei Formeln aufstellen, die dafür sorgen, dass alle Überträge nach drei Gatterlaufzeiten bekannt sind. Die allgemeine Gleichung lautet c i +1 = g i + p i g i −1 + p i p i −1 g i −2 + · · · + p i p i −1 . . . p 1 g 0 + p i p i −1 . . . p 1 p 0 c 0

Dadurch lassen sich alle Überträge mit der Verzögerung von drei Logikgattern bestimmen, nachdem die Eingangswerte X , Y und c 0 verfügbar sind (eine Gatterverzögerung , um alle p i - und g i -Signale zu entwickeln, gefolgt von zwei Gatterverzögerungen durch die AND/OR-Schaltung zur Bestimmung von c i +1 ). Nach drei weiteren Gatterverzögerungen, die ein Volladdierer zur Berechnung des Ergebnisses benötigt, sind alle Summenbits verfügbar. Deswegen benötigt die n -Bit-Addition unabhängig von n nur sechs Logikebenen bzw. Gatterlaufzeiten. Ein solcher Addierer ist ziemlich schnell. Leider bringt er praktische Probleme mit sich, die in einem Beispiel veranschaulicht werden. Es soll ein vorausschauender Addierer für n = 4 entworfen werden. Die Überträge berechnen sich nach den folgenden Formeln:

c1 = g 0 + p 0 c0 c2 = g 1 + g 0 p 1 + p 1 p 0 c0 c3 = g 2 + g 1 p 2 + g 0 p 2 p 1 + p 2 p 1 p 0 c0 c4 = g 3 + g 2 p 3 + g 1 p 3 p 2 + g 0 p 3 p 2 p 1 + p 3 p 2 p 1 p 0 c0

Abbildung 9.7.: Vorausschauender Addierer mit n = 4 Eine Schaltung des entsprechenden Addierers ist in Abbildung 9.7 dargestellt. Da die eigentlichen Additionen in den Volladdierern stattfindet, die in der Schaltungsgrafik symbolisch gekapselt sind, zeigt sich, dass die Übertragsberechnung einen großen Überbau verursacht. Wohlgemerkt: Die Schaltung berechnet nur die Addition von vier Bits! Man

150

Technische Informatik I

9.6. Addition stelle sich vor, wie groß der Überbau bei der Berechnung von acht, sechzehn oder zweiunddreißig Bit breiten Wörtern sein mag. Aus diesem Grund beschränkt zum Einen die bloße Machbarkeit die beliebige Verbreiterung des Addierers, andererseits lassen sich auch mehrere vorausschauende Addierer hintereinander schalten, um die Wortbreite zu erhöhen. Dadurch leidet die Geschwindigkeit etwas, aber nicht in dem Maße, dass es diese Art der Addierer unattraktiv werden ließe. Ein weiteres Problem sind die Eingänge an den Gattern: Das letzte ODER-Gatter zur Berechnung von c 4 benötigt fünf Eingänge; generell benötigt es zur Berechnung von c i genau i + 1 Eingänge. Auch hier beschränkt das hardwareseitig Machbare die beliebige Wortverlängerung. Die meisten auf dem Markt verfügbaren Gatter haben nicht mehr als 8, aber meistens erheblich weniger Eingänge. Mit acht vorausschauenden 4-Bit-Addierern lässt sich ein 32-Bit-Addierer erstellen, der eine Addition in nur 20 Gatterlaufzeiten durchführt. Bei einer Gatterlaufzeitdauer von 0,5 ns bedeutet das, dass die Summe nach 10 ns bereitsteht. Das ist viel besser als die 32,5 ns, die man mit einem parallelen Addierer erzielen würde. Man beachte, dass bei dem zusammengesetzten vorausschauenden Addierer nur innerhalb der 4-Bit-Addierer vorausschauend addiert wird. Zwischen diesen Blöcken „plätschert“ das Übertragsbit wie zuvor durch. Für längere Wörter mag es notwendig sein, die Addition noch weiter zu beschleunigen. Dies kann dadurch erreicht werden, indem man die Vorschau-Technik auf die Überträge zwischen den Blöcken anwendet und so eine zweite Ebene der Vorschau und der Abstraktion schafft. Zunächst einmal denken wir uns wieder einen 4-Bit-Addierer mit seiner vorausschauenden Logik als eine Blackbox. Wenn wir nun vier davon in Reihe zusammenschalten, um einen 16-Bit-Addierer zu schaffen, wird die Addition mit nur wenig mehr Hardware deutlich schneller als ein normaler 16-Bit-Addierer sein. Um schneller zu sein, benötigen wir eine Übertragsvorschau auf einer höheren Ebene. Also brauchen wir auch Übertrags- und Fortpflanzungssignale und -funktionen auf dieser höheren Ebene, wie sie unten für jeden der vier 4-Bit-Addierer aufgeführt sind.

P0 = p3 p2 p1 p0 P1 = p7 p6 p5 p4 P 2 = p 11 p 10 p 9 p 8 P 3 = p 15 p 14 p 13 p 12

Diese übergeordneten Fortpflanzungsfunktionen P i für die 4-Bit-Abstraktion sind nur dann wahr, wenn jedes der Bits innerhalb der Gruppe einen Übertrag durchreicht. Für die übergeordneten Generierungsfunktionen braucht es uns nur zu interessieren, ob es einen Übertrag durch das niederwertigste Bit der 4-Bit-Gruppe gibt. Dies ist offensichtlich dann der Fall, wenn die Generierungsfunktion für das niederwertigste Bit wahr ist, oder wenn eine noch frühere Generierungsfunktion einen Übertrag erzeugt hat, welcher von allen zwischenliegenden Stufen durchgereicht wurde.

Technische Informatik I

151

9. Rechnerarithmetik

G0 = g3 + p3 g2 + p3 p2 g1 + p3 p2 p1 g0 G1 = g7 + p7 g6 + p7 p6 g5 + p7 p6 p5 g4 G 2 = g 11 + p 11 g 10 + p 11 p 10 g 9 + p 11 p 10 p 9 g 8 G 3 = g 15 + p 15 g 14 + p 15 p 14 g 13 + p 15 p 14 p 13 g 12

Dann sind die Gleichungen für die vier Carry-Ins der Blöcke auf dieser höheren Abstraktionsebene ähnlich denen für die Carry-Outs innerhalb jeder 4-Bit-Gruppe. C 1 = G 0 + P 0 c0 C 2 = G 1 + P 1G 0 + P 1 P 0 c 0 C 3 = G 2 + P 2G 1 + P 2 P 1G 0 + P 2 P 1 P 0 c 0 C 4 = G 3 + P 3G 2 + P 3 P 2G 1 + P 3 P 2 P 1G 0 + P 3 P 2 P 1 P 0 c 0 C 4 = c 16 ist hierbei bereits nach fünf Gatterlaufzeiten, nachdem X , Y und c 0 angelegt wurden, verfügbar. Bei der reinen Verwendung von g i - und p i -Funktionen brauchten

wir dafür noch zwölf Gatterlaufzeiten. Größere Addierer lassen sich wieder entwickeln, indem man solche 16-Bit-Addierer und eine weitere Abstraktion zugrunde legt. Dadurch lässt sich die Gesamtverzögerung auf die Hälfte gegenüber der direkten Nutzung der 4-Bit-Addierer reduzieren. Die vorausschauenden Addierer sind ein gutes Beispiel für eine Anwendung des Abstraktionsprinzips. Das Vorschau-Prinzip selbst wird im Rechner durchgehend angewendet, um möglicherweise seriell ablaufende Vorgänge durch Parallelisierung zu beschleunigen. Dies ist z. B. auch bei der Adressenberechnung für zukünftige Speicherzugriffe, bei Eingangsberechnungen für einzelne Stufen des Pipelinings (siehe Kapitel 12.3.4) und bei der Compiler-Optimierung der Fall.

9.7. Multiplikation Mit der Konstruktion der ALU und der Erläuterung von Addition, Subtraktion und Verschiebungen (shifts) sind wir nun bereit, die nächsthöhere Operation des Multiplizierens anzugehen. Betrachten wir jedoch vorher die schriftliche Multiplikation von Dezimalzahlen, um uns an den dort zugrundegelegten Algorithmus und die Bezeichnungen der Operanden zu erinnern. Wir werden dieses Dezimalbeispiel auch auf die Ziffern 0 und 1 beschränken, da uns im Binärsystem auch nicht mehr zur Verfügung stehen. Multiplizieren wir also 100010 mit 100110 . Diese Faktoren kann man auch unterschiedlich bezeichnen: Der erste Operand heißt der Multiplikand (der Wert, der vervielfacht wird) und der zweite der Multiplikator (wie oft vervielfacht wird). Multiplikand Multiplikator

Produkt

152

×

1 1

0 0 0

0 0 0 0

1 1 1 0 0 0 1

0 0 0 0 0

0 0 0 0

010 110

0

0

010

Technische Informatik I

0

9.7. Multiplikation Wie man sich vielleicht erinnert, multipliziert man laut dem Grundschulalgorithmus die Ziffern des Multiplikators von rechts nach links einzeln mit dem Multiplikanden und verschiebt jedes Zwischenergebnis eine Stelle weiter nach rechts als das vorherige Zwischenresultat. Eine wichtige Feststellung ist, dass die Anzahl der Ziffern des Produktes beträchtlich größer ist als die Anzahl der Ziffern des Multiplikanden oder des Multiplikators. Tatsächlich ist das Ergebnis der Multiplikation einer n -Bit-Zahl und einer m -Bit-Zahl – die Vorzeichenbits beiseite gelassen – n + m Bits lang. Das bedeutet, dass man n + m Bits braucht, um alle möglichen Produkte darzustellen. Folglich muss die Multiplikation wie beim Addieren ebenfalls mit Überläufen fertig werden, da man häufig ein 32-Bit-Produkt als Ergebnis zweier 32-Bit-Faktoren haben möchte. In diesem Beispiel beschränkten wir die Dezimalziffern auf 0 und 1. Mit diesen beiden Alternativen allein ist jeder Schritt der Multiplikation simpel: 1. Nimm eine Kopie der Multiplikanden als aktuelles Zwischenergebnis, wenn die aktuelle Multiplikatorziffer 1 ist (1·Multiplikand) 2. Nimm ansonsten 0 als Zwischenergebnis (0·Multiplikand), wenn die Ziffer des Multiplikators 0 ist. Dies ist der Algorithmus. Sehen wir uns nun die Hardware an, die diesen ausführen soll. Im Rest dieses Abschnitts betrachten wird aufeinanderfolgende Verfeinerungen der Hardware und des Algorithmus, bis wir zu einer in realen Rechnern verwendeten Version kommen. Nehmen wir vorerst an, dass wir nur positive Ganzzahlen miteinander multiplizieren.

9.7.1. Erste Version einer Multiplikationshardware Das ursprüngliche Design spiegelt den Grundschulalgorithmus wieder. Nehmen wir an, dass sich der Multiplikator im 32-Bit-Multiplikatorregister befindet (Abbildung 9.8) und dass das 64-Bit-Produktregister auf 0 initialisiert wurde. Von der manuellen schriftlichen Multiplikation wissen wir, dass wir den Multiplikanden bei jedem Schritt um eine Ziffer nach links bewegen müssen, bevor er zu den Zwischenprodukten addiert werden kann. Bei 32 Schritten bewegt sich der 32-Bit-Multiplikand dabei um 32 Bits nach links. Folglich brauchen wir ein 64-Bit-Multiplikandenregister, welches in der rechten Hälfte mit den 32 Bits des Multiplikanden initialisiert werden muss, während in der linken Hälfte nur Nullen stehen. Dieses Register wird dann bei jedem Schritt jeweils um ein Bit nach links verschoben, um den Multiplikanden an der Summe, die im 64-Bit-Produktregister steht, auszurichten. Der Algorithmus in der Abbildung 9.9 zeigt die drei Grundschritte, die für jedes Bit auszuführen sind. Das niederwertigste Bit des Multiplikators bestimmt dabei, ob der Multiplikand zum Produktregister hinzuaddiert wird. Die Linksverschiebung im zweiten Schritt verschiebt die Zwischenergebnisse jeweils nach links, analog der manuellen schriftlichen Multiplikation. Die Rechtsverschiebung in Schritt 3 hingegen liefert uns das nächste Bit des Multiplikators, welches im nächsten Schleifendurchlauf untersucht wird. Diese drei Schritte werden 32-mal wiederholt, um das Produkt zu erhalten. Als Beispiel multiplizieren wir 210 mit 310 , also 00102 mit 00112 . Tabelle 9.7 zeigt die Werte jedes Registers nach jedem der besprochenen Schritte, mit dem Endwert 000001102 = 610 . Die fettgedruckten Bits des Multiplikators sind dabei diejenigen, die untersucht werden.

Technische Informatik I

153

9. Rechnerarithmetik

Abbildung 9.8.: Erste Version einer Multiplikationshardware Schleife

Schritt

Multiplikator

Multiplikand

0

Anfangswerte

0011

0000 0010

0000 0000

1

1a: 1 ⇒ Prod = Prod + Mkand

0011

0000 0010

0000 0010

2

3

4

Produkt

2: Schiebe Multiplikand nach links

0011

0000 0100

0000 0010

3: Schiebe Multiplikator nach links

000h

0000 0100

0000 0010

1a: 1 ⇒ Prod = Prod + Mkand

0001

0000 0100

0000 0110

2: Schiebe Multiplikand nach links

0001

0000 1000

0000 0110

3: Schiebe Multiplikator nach links

0000

0000 1000

0000 0110

1: 0 ⇒ keine Operation

0000

0000 1000

0000 0110

2: Schiebe Multiplikand nach links

0000

0001 0000

0000 0110

3: Schiebe Multiplikator nach links

0000

0001 0000

0000 0110

1: 0 ⇒ keine Operation

0000

0001 0000

0000 0110

2: Schiebe Multiplikand nach links

0000

0010 0000

0000 0110

3: Schiebe Multiplikator nach links

0000

0010 0000

0000 0110

Tabelle 9.7.: Multiplikation Version 1 in Registern Die Wichtigkeit von arithmetischen Operationen wie der Multiplikation variiert von Programm zu Programm, aber die Addition und die Subtraktion sind überall 5 bis 100 mal wichtiger als das Multiplizieren. Deswegen kann das Multiplizieren in den meisten Anwendungen mehrere Taktzyklen benötigen, ohne die Geschwindigkeit spürbar zu beeinträchtigen. Man beachte aber, dass selbst eine mittlere Häufigkeit für eine langsame Operation die Geschwindigkeit signifikant verringern kann. Fast alle modernen Rechner bieten die Multiplikation in ihrem Maschinenbefehlssatz. Die neuesten Hochleistungsprozessoren nutzen einen beträchtlichen Anteil der Chipfläche, um arithmetische Operationen auf Ganzzahl- und Gleitkommaoperanden auszuführen.

9.7.2. Zweite Version einer Multiplikationshardware Die zweite Version unterscheidet sich im Wesentlichen nur im Schritt 1a von der ersten. Im Gegensatz dazu wird hier nicht ein 64-Bit-Multiplikand zum Produkt addiert, sondern nur ein 32 Bit breites Wort (welches den Multiplikanden beinhaltet) zu der linken Hälfte des Produktes (Abbildung 9.10 und Abbildung 9.11).

154

Technische Informatik I

9.8. Der Booth-Algorithmus

Abbildung 9.9.: Algorithmus zur ersten Version einer Multiplikationshardware Auch dies soll wieder am Beispiel der Multiplikation von 00102 mit 00112 verdeutlicht werden (Tabelle 9.8). Das Ergebnis ist ebenfalls 01102 = 610 .

9.7.3. Finale dritte Version einer Multiplikations-Hardware Die letzte Beobachtung der sparsamen Rechnerpioniere war, dass das Produktregister Platz in der Größe des Multiplikators verschwendete: Wenn der verschwendete Platz verschwindet, dann verschwinden auch die Bits des Multiplikators. In der Folge verbindet die dritte Version der Multiplikation die rechte Hälfte des Produktes mit dem Multiplikator (Abbildung 9.12). Dadurch entfällt zusätzlich die Notwendigkeit für ein eigenes Multiplikatorregister (und somit auch der Schritt 3) (Abbildung 9.13). Beim Test wird also nun das niederwertigste Bit des Produkts überprüft. Auch hierzu wieder unser Beispiel: 00102 mal 00112 (Tabelle 9.9).

9.8. Der Booth-Algorithmus Bisher haben wir ausschließlich die Multiplikation von positiven Ganzzahlen betrachtet und die Hardware darauf optimiert. Nun gilt es, dass Vorzeichen zu berücksichtigen und dann die Algorithmen anzupassen und schneller zu machen.

Technische Informatik I

155

9. Rechnerarithmetik

Abbildung 9.10.: Zweite Version einer Multiplikationshardware Schleife

Schritt

Multiplikator

Multiplikand

Produkt

0

Anfangswerte

0011

0010

0000 0000

1

2

3

4

1a: 1 ⇒ Prod = Prod + Mkand

0011

0010

0010 0000

2: Schiebe Produkt nach rechts

0011

0010

0001 0000

3: Schiebe Multiplikator nach rechts

0001

0010

0001 0000

1a: 1 ⇒ Prod = Prod + Mkand

0001

0010

0011 0000

2: Schiebe Produkt nach rechts

0001

0010

0001 1000

3: Schiebe Multiplikator nach rechts

0000

0010

0001 1000

1: 0 ⇒keine Operation

0000

0010

0001 1000

2: Schiebe Produkt nach rechts

0000

0010

0000 1100

3: Schiebe Multiplikator nach rechts

0000

0010

0000 1100

1: 0 ⇒keine Operation

0000

0010

0000 1100

2: Schiebe Produkt nach rechts

0000

0010

0000 0110

3: Schiebe Multiplikator nach rechts

0000

0010

0000 0110

Tabelle 9.8.: Multiplikation Version 2 in Registern Ein mächtiger Algorithmus für die Multiplikation vorzeichenbehafteter ganzer Zahlen ist der Booth-Algorithmus. Er generiert ein 2n -Bit-Produkt und behandelt positive und negative Zahlen gleich. Man denke sich eine Multiplikation, bei welcher der Multiplikator positiv ist und eine einfache Folge von Einsen beinhalte, zum Beispiel 0011110. Um das Produkt zu erhalten, könnte man vier entsprechend verschobene Versionen des Multiplikanden aufsummieren:

0

0 0

0 0 0

0 0 0 1

0 1 0 0 0

0 1 0 0 0 1

0 1 0 1 0 0 0

0 0 0 1 0 1 1 0 0 1

1 0 0 0 1 1 0 0

0 1 0 1 1 0 1

1 1 0 1 0 1

1 1 0 0 1

0 1 0 1

1 0 0

0

0

0

1

1

0

Das Problem hierbei: Wenn der Addierer eine Verzögerung von 10 ns hat und das Testen und die Schiebeoperationen in jedem Zyklus weitere 10 ns brauchen, dann würde eine

156

Technische Informatik I

9.8. Der Booth-Algorithmus

Abbildung 9.11.: Algorithmus zur zweiten Version der Multiplikations-Hardware

hartverdrahtete Multiplikationsoperation 640 ns benötigen. Aus diesem Grund dauerte das Multiplizieren bei frühen Rechnern viel länger als das Addieren. Mehrere Techniken werden eingesetzt, um die Multiplikation in modernen Prozessoren zu beschleunigen, einige davon werden wir in den nächsten Abschnitten besprechen. Wie auch immer, wir können die Anzahl der benötigten Operationen verringern, indem wir diesen Multiplikator als Differenz zweier Zahlen ansehen: 0100000 −0000010

0011110

= 3210 = 210 = 3010

Abbildung 9.12.: Dritte Version der Multiplikations-Hardware

Technische Informatik I

157

9. Rechnerarithmetik

Abbildung 9.13.: Algorithmus zur dritten Version der Multiplikations-Hardware Dadurch ist leicht ersichtlich, dass das Produkt generiert werden kann, indem man „25 mal den Multiplikand“ zum Zweierkomplement von „21 mal dem Multiplikanden“ addiert. Aus Bequemlichkeit können wir die Sequenz der benötigten Operationen beim Umschreiben der vorangegangenen Multiplikators als 0

+1

0

0

0

-1

0

beschreiben. Im Allgemeinen wird im Booth-Schema „−1 mal dem verschobenen Multiplikanden“ benutzt, wenn sich der aktuelle Prüfwert von 0 auf 1 ändert. Analog addiert man „+1 mal dem geshifteten Multiplikanden“, wenn sich der Wert von 1 auf 0 ändert. Dabei wird der Multiplikator von rechts nach links untersucht. Für das Beispiel von vorhin bedeutet das:

0 1 0 0 0 1 0 0

158

0 1 0 0 0 1 0 0

0 1 0 0 0 0 0 0

0 1 0 0 0 1 0 1

0 1 0 0 0 0 0 0

0 1 0 0 0 1 0 1

0 0 0 0 0 1 0 0

0 0 0 1 0 0 0 0 0 1

1 +1

0 0 0 0 0 1 0

0 0 0 1 0 0 0

1 0 0 1 0 0

1 0 0 0 0

0

0

1

Technische Informatik I

0 −1

0 1

1

1 0 0

0

9.8. Der Booth-Algorithmus Schleife

Schritt

Multiplikand

0

Anfangswerte

0010

0000 0011

1

1a: 1 ⇒ Prod = Prod + Mkand

0010

0010 0011

2: Schiebe Produkt nach rechts

0010

0001 0001

1a: 1 ⇒ Prod = Prod + Mkand

0010

0011 0001

2: Schiebe Produkt nach rechts

0010

0001 1000

2

3

4

Produkt

1: 0 ⇒ keine Operation

0010

0001 1000

2: Schiebe Produkt nach rechts

0010

0000 1100

1: 0 ⇒ keine Operation

0010

0000 1100

2: Schiebe Produkt nach rechts

0010

0000 0110

Tabelle 9.9.: Multiplikation Version 3 in Registern Und tatsächlich erhalten wir das gleiche Ergebnis wie oben. Allein in diesem Beispiel werden schon zwei Additionen eingespart. Wovon nun die Größe der Einsparung abhängt, werden wir später noch betrachten. Der Trick bei der Arbeit des Booth-Algorithmus ist seine Einteilung der Bits in Gruppen des Beginns, der Mitte und des Endes einer Folge von Einsen. Klar ist, dass eine Folge von Nullen uns Arithmetik erspart und somit ignoriert werden kann. Der Booth-Algorithmus ändert den ersten Schritt der zuletzt besprochenen Multiplikations-Hardware, indem hier nicht mehr nur ein Bit genutzt wird, um zu entscheiden, ob der Multiplikand zum Produkt hinzuaddiert wird. Stattdessen werden hierbei zwei Bits des Multiplikators überprüft. Dieser neue erste Schritt hat dann also vier verschiedene Fälle zu unterscheiden, welche von den Werten dieser zwei Bits abhängen. Nehmen wir an, dass das untersuchte Bitpaar aus dem momentanen Bit und dem rechts daneben (welches im vorherigen Schritt das momentane Bit war) besteht. Der zweite Schritt besteht immer noch darin, dass Produkt um eine Stelle nach rechts zu verschieben. Der neue Algorithmus sieht dann folgendermaßen aus: 1. Abhängig vom momentanen und dem vorherigen Bit mache folgendes: • 00: Innenraum in einer Folge von Nullen, also keine Arithmetik nötig • 01: Ende einer Folge von Einsen, also addiere den Multiplikanden zur linken Hälfte des Produkts • 10: Beginn einer Folge von Einsen, also subtrahiere den Multiplikanden von der linken Hälfte des Produkts • 11: Innenraum in einer Folge von Einsen, also keine Arithmetik nötig 2. Verschiebe wie im vorherigen Algorithmus das Produktregister um ein Bit nach rechts Zum Vergleich betrachten wir die beiden Algorithmen am Beispiel (Tabelle 9.10). Dabei nehmen wir für den Booth-Algorithmus zu Beginn immer eine Null herausgeschobenes Bit an. Man beachte, dass der Booth-Algorithmus immer durch zwei Bits entscheidet, welche Operation auszuführen ist. Nach dem vierten Schritt haben die beiden Algorithmen den selben Wert in ihren Produktregistern. Die einzige weitere Voraussetzung ist, dass – da wir es mit vorzeichenbehafteten Zahlen zu tun haben – die Zwischenergebnisse trotz Rechtsverschiebung im Produkt das Vorzeichen beibehalten. Dafür wird das Vorzeichen schlicht erweitert, d. h. der Wert des

Technische Informatik I

159

9. Rechnerarithmetik Schleife

Multi-

Urspr. Algorithmus

Booth-Algorithmus

plikant

Schritt

Produkt

Schritt

Produkt

0

0010

Anfangswerte

0000 0110

Anfangswerte

0000 0110 0

1

0010

1: 0 ⇒ keine Operation

0000 0110

1d: 00 ⇒ keine Operation

0000 0110 0

0010

2: Schiebe Produkt nach rechts

0000 0011

2: Schiebe Produkt nach rechts

0000 0011 0

2

0010

1a: 1 ⇒ Prod = Prod + Mkand

0010 0011

1c: 10 ⇒ Prod = Prod − Mkand

1110 0011 0

0010

2: Schiebe Produkt nach rechts

0001 0001

2: Schiebe Produkt nach rechts

1111 0001 1

3

0010

1a: 1 ⇒ Prod = Prod + Mkand

0011 0001

1d: 11 ⇒ keine Operation

1111 0001 1

0010

2: Schiebe Produkt nach rechts

0001 1000

2: Schiebe Produkt nach rechts

1111 1000 1

4

0010

1: 0 ⇒ keine Operation

0001 1000

1b: 01 ⇒ Prod = Prod + Mkand

0001 1000 1

0010

2: Schiebe Produkt nach rechts

0000 1100

2: Schiebe Produkt nach rechts

0000 1100 0

Tabelle 9.10.: Vergleich normale Multiplikation und Multiplikation mit Booth ersten Bits wird beibehalten. Dadurch erzeugt der zweite Schritt des zweiten Schleifendurchlaufs aus 1110001102 den Wert 1111000112 (anstatt 0111000112 ). Diese Art der Verschiebung heißt arithmetische Rechtsverschiebung (arithmetic shift right), um sie von der logischen Rechtsverschiebung zu unterscheiden, die immer Nullen anfügt. Nachfolgendend noch ein Beispiel für einen negativen Multiplikator: 210 · (−310 ) = −610 bzw. 00102 · 11012 = 111110102 (Tabelle 9.11). Schleife

Schritt

Multiplikand

0

Anfangswerte

0010

0000 1101 0

1

1c: 10 ⇒ Prod = Prod − Mkand

0010

1110 1101 0

2

3

4

Produkt

2: Schiebe Produkt nach rechts

0010

1111 0110 1

1b: 01 ⇒ Prod = Prod + Mkand

0010

0001 0110 1

2: Schiebe Produkt nach rechts

0010

0000 1011 0

1c: 10 ⇒ Prod = Prod − Mkand

0010

1110 1011 0

2: Schiebe Produkt nach rechts

0010

1111 0101 1

1d: 11 ⇒ keine Operation

0010

1111 0101 1

2: Schiebe Produkt nach rechts

0010

1111 1010 1

Tabelle 9.11.: Beispiel negative Multiplikation Jetzt, da wir gesehen haben, dass der Booth-Algorithmus funktioniert, können wir uns ansehen, warum er für vorzeichenbehaftete Ganzzahlen in Zweierkomplementdarstellung funktioniert. Lassen wir a den Multiplikator und b den Multiplikanden sein. Wir werden weiterhin ai für das i -te Bit von a schreiben. Gestaltet man den Booth-Algorithmus so um, dass man nach den Werten der zu überprüfenden Bits unterscheidet, erhält man Tabelle 9.12. Anstatt dies in tabellarischer Form darzustellen, lässt sich der Booth-Algorithmus durch den Ausdruck (ai −1 − ai ) beschreiben. Ist das Ergebnis 0, so wird mit dem nächsten Schritt im Algorithmus fortgefahren. Bei +1 wird zuvor b addiert und bei −1 subtrahiert. Da wir wissen, dass eine Linksverschiebung des Multiplikanden als eine Multiplikation um den Faktor Zwei angesehen werden kann, können wir den Booth-Algorithmus als folgende Summe aufschreiben:

160

Technische Informatik I

9.8. Der Booth-Algorithmus ai

a i −1

0 0 1 1

0 1 0 1

Auszuführende Operation Nichts Addiere b Subtrahiere b Nichts

Tabelle 9.12.: Bitgesteuerte Boothbelegungen

(a −1 − a 0 ) × b × 20 + (a 0 − a 1 ) × b × 21 + (a 1 − a 2 ) × b × 22 ··· + (a 30 − a 31 ) × b × 231

Wir können diese Summe vereinfachen, da:

−a i × 2i + a i × 2i +1 = (−a i + 2a i ) × 2i = (2a i − a i ) × 2i = a i × 2i

Wir erinnern uns, dass a−1 = 0 und klammern b aus jedem Term aus: ¡¡ ¢ ¡ ¢ ¡ ¢ ¡ ¢¢ b × a 31 × −231 + a 30 × −230 + · · · + a 1 × −21 + a 0 × −20

Die lange Formel in Klammern auf der rechten Seite der ersten Multiplikation ist einfach die Zweierkomplementdarstellung von a , wie wir sie bereits behandelt haben. Dass a31 mit −231 multipliziert wird, hat zur Folge, dass a negativ ist, wenn das höchstwertige Bit (a31 ) gesetzt ist, und positiv, falls a31 = 0. Folglich führt der Booth-Algorithmus in der Tat die Zweierkomplementmultiplikation von a und b aus. Der Booth-Algorithmus hat zwei attraktive Vorteile. Erstens behandelt er positive und negative Multiplikatoren gleich. Zweitens ist der Booth-Algorithmus im Mittel genauso langsam wie die herkömmliche Multiplikation, begünstigt aber Multiplikationen von Daten, die lange Einser- oder Nullerketten haben. Dies ist in Computern häufig der Fall. In jedem Fall ist es hilfreich, vor der Multiplikation zu überprüfen, welcher Faktor sich aufgrund weniger 0-1- oder 1-0-Wechsel besser als Multiplikator eignet. Will man als Beispiel 01012 und 11112 miteinander multiplizieren, so wäre eine ungünstige Zuordnung von Multiplikand und Multiplikator folgende:

Technische Informatik I

161

9. Rechnerarithmetik

(umcodiert) 0 1 0 1 1

0 1 0 1 1

0 1 0 1 1

0 1 0 1 1

1 0

1 1

1 0

1 1

+1

−1

+1

−1

0 1 0 1 1

0 1 1

0 1

1

0

1

1

Günstig wäre dagegen folgende Zuordnung:

(umcodiert) 1 0 0 0 1

1 0 0 0 1

1 0 0 0 1

1 0 0 0 1

0 1 0 1 0 0 0 1

1 1 0 0 0 0

0 1 0 1 0

1 1 −1

0

1

1

1

9.9. Schnelle Multiplikation Wir beschreiben nun zwei Techniken, mit denen man die Multiplikationsoperation beschleunigen kann. Die erste Technik reduziert die maximale Anzahl von Summanden (Versionen des Multiplikanden), die addiert werden müssen, unter Umständen auf die Hälfte. Die zweite Technik verringert die zum Addieren der Summanden notwendige Zeit. Eine Technik namens bit-pair recoding (Neucodierung eines Bitpaares) kann die Anzahl der Summanden gegebenenfalls halbieren. Die Technik kann direkt aus dem Booth-Algorithmus abgeleitet werden und wird deshalb oft auch als Booth-2-Algorithmus bezeichnet. Das Bit-Pair-Verfahren nutzt die Tatsache, dass in der binären Darstellung eine Ziffer a links einer Ziffer b für den Wert a = 2 · b steht. Zum Beispiel steht das niederwertigste Bit für 20 = 1 und das links davon stehende Bit für 21 = 2. Offensichtlich gilt 2 = 2 · 1. Wenn man gemäß der Booth-Algorithmus-Vorverarbeitung einen der Faktoren umcodiert, bekommt man Folgen aus +1, −1 und 0. Ist ein Ausschnitt daraus das Paar {+1, −1}, so entspricht das nach obiger Definition (+1) · 2b + (−1) · b = 2b − b = b . Folglich genügt es, nur die rechte Stelle einmal zu addieren; das Bitpaar lässt sich also in {0, +1} umcodieren. Das bedeutet, dass wir an dieser Stelle eine Addition sparen können. Die Umcodierungsmöglichkeiten sind in Tabelle 9.13 dargestellt. Ursprüngliches Bitpaar

Umcodiertes Bitpaar

{+1, −1} {−1, +1}

{0, +1} 0, −1

Tabelle 9.13.: Varianten des Bit-Pair-Recoding

162

Technische Informatik I

9.10. Ganzzahldivision Der umgeschriebene Multiplikator wird jeweils auf zwei Bits zugleich geprüft wird (von rechts beginnend). Bei Zahlwörtern mit vielen 1-0- oder 0-1-Wechseln kann die tatsächliche Anzahl an Operationen dadurch erheblich reduziert werden. Die Multiplikation bedient sich der Addition mehrerer Summanden. Eine Technik namens carry-save addition (Übertragssichernde Addition) beschleunigt den Additionsvorgang. Bei der Addition der drei Summanden A , B und C wird gleich A + B + C errechnet (wobei ansonsten ja zwei mal Übertragsdurchreichung stattfinden müsste), anstatt erst das Zwischenergebnis A + B = S zu berechnen. Eine mögliche Schaltung finden Sie in Abbildung 9.14. Einzelheiten entnehme man der weiterführenden Literatur.

Abbildung 9.14.: Carry-Save-Addierer Dabei werden aus drei Summanden immer wieder die Vektoren S und C generiert. Die verbliebenen S und C werden wieder zu Dreiergruppen zusammengefasst und das Verfahren wird erneut angewendet. Am Ende ist das ganze System auf zwei Vektoren S und C reduziert. Hier muss eine Addition mit Carry durchgeführt werden. Sie kann mit Hilfe eines Carry-Lookahead-Addierers realisiert werden. Fassen wir die Anwendung dieser Techniken bei der Hochgeschwindigkeitsmultiplikation zusammen. Neucodierung der Bitpaare des Multiplikators, abgeleitet aus dem BoothAlgorithmus, reduziert die Anzahl der Summanden um die Hälfte. Diese Summanden lassen sich dann auf nur zwei reduzieren, wenn man eine verhältnismäßig kleine Anzahl von übertragssichernden Operationen anwendet. Das Endprodukt kann durch eine einzige Addition generiert werden und diese letzte Operation kann durch vorausschauende Addierer weiter beschleunigt werden. Alle diese drei Grundtechniken sind in vielen Variationen durch Entwickler von Hochleistungsprozessoren angewendet worden, um die für eine Multiplikation nötige Zeit zu reduzieren.

9.10. Ganzzahldivision Im vorhergehenden Abschnitt besprachen wir die Multiplikation positiver Zahlen, indem wir die Operation, wie sie durch eine Logikschaltung ausgeführt wird, an die manuelle Rechnung anlehnten. Bei der Division gehen wir jetzt denselben Weg. Wir besprechen zunächst nur die positiven Ganzzahlen und machen später ein paar allgemeine Bemerkungen zu vorzeichenbehafteten Operanden. Abbildung 9.15 zeigt dezimale Division und die binär-codierte Division mit denselben Werten.

Technische Informatik I

163

9. Rechnerarithmetik 1 2 2

7 6 1 1

4 4 3 1

: 13

= 21

0 1

0 1 1

0 0 0 1

1 1 0 1

0

0

1

0

0 0 1 1

0 1 1 1

1 0

0 1 1

: 1101

= 10101

Abbildung 9.15.: Schriftliche Division in dezimaler und binärer Darstellung Betrachten wir zunächst nur die dezimale Version. Die 2 im Quotienten wird folgendermaßen bestimmt: Zuerst versuchen wir, 2 durch 13 zu teilen, was natürlich nicht geht. Darauf teilen wir 27 durch 13 (mit dem Ergebnis 2). Wir machen die Probe und erhalten 13 · 2 = 26. Dieses Ergebnis ist kleiner als 27, es bleibt also ein Rest von 1. Also tragen wir 2 als erstes Ergebnis ein und vollführen die benötigte Subtraktion. Die nächste Ziffer des Dividenden (4) holen wir runter und beenden die Division mit der Entscheidung, dass 13 genau einmal in 14 passt, wobei ein Rest von 1 bleibt. Wir können die binäre Division durch die gleiche Methode durchführen, mit der Vereinfachung, dass die Quotientenbits nur 0 oder 1 sein können. Es kann also länger dauern, bis wieder eine Teil-Division durchführbar ist (z. B. lässt sich 1 nicht durch 1101 teilen, ebenso wenig 10, 100 oder gar 1000 – erst mit 10001 ist die Operation durchführbar).

9.10.1. Erste Version einer Divisions-Hardware Nehmen wir nun an, die Operanden sowie das Ergebnis der Division sind 32-Bit-Werte und positiv. Die erste Version der Divisionshardware hält sich komplett an den Grundschulalgorithmus (Abbildung 9.16).

Abbildung 9.16.: Hardware zur Division (Erste Version) Wir beginnen mit einem 32-Bit-Quotientenregister, welches komplett auf 0 gesetzt ist. Bei jedem Schritt des Algorithmus muss der Divisor um eine Ziffer nach rechts bewegt werden, also befindet er sich zu Beginn in der linken Hälfte des 64-Bit-Divisorregisters und wird bei jedem Schritt um ein Bit nach rechts verschoben, um ihn am Dividenden ausrichten. Das Restregister wird mit dem Dividenden initialisiert.

164

Technische Informatik I

9.10. Ganzzahldivision

Abbildung 9.17.: Algorithmus zur Division (Erste Version) Im Gegensatz zu einem Menschen ist ein Rechner nicht schlau genug, um im Voraus zu wissen, ob der Divisor kleiner als der Dividend ist. Also muss er im ersten Schritt den DiSchleife

Schritt

Quotient

Divisor

Rest

0

Anfangswerte

0000

0010 0000

0000 0111

1

2

3

4

5

1: Rest = Rest − Div

0000

0010 0000

1110 0111

2b: Rest < 0 ⇒ +Div, Quotient linksschieben, Q0 = 0

0000

0010 0000

0000 0111

3: Div rechtschieben

0000

0001 0000

0000 0111

1: Rest = Rest − Div

0000

0001 0000

1111 0111

2b: Rest < 0 ⇒ +Div, Quotient linksschieben, Q0 = 0

0000

0001 0000

0000 0111

3: Div rechtschieben

0000

0000 1000

0000 0111

1: Rest = Rest − Div

0000

0000 1000

1111 1111

2b: Rest < 0 ⇒ +Div, Quotient linksschieben, Q0 = 0

0000

0000 1000

0000 0111

3: Div rechtschieben

0000

0000 0100

0000 0111

1: Rest = Rest − Div

0000

0000 0100

0000 0011

2a: Rest ≥ 0 ⇒Quotient linksschieben, Q0 = 0

0001

0000 0100

0000 0011

3: Div rechtschieben

0001

0000 0010

0000 0011

1: Rest = Rest − Div

0001

0000 0010

0000 0001

2a: Rest ≥ 0 ⇒Quotient linksschieben, Q0 = 0

0011

0000 0010

0000 0001

3: Div rechtschieben

0011

0000 0001

0000 0001

Tabelle 9.14.: Beispiel Division (Erste Version)

Technische Informatik I

165

9. Rechnerarithmetik visor subtrahieren. Ist das Ergebnis positiv, dann ist der Divisor kleiner oder gleich dem Dividenden, also erzeugen wir eine 1 im Quotienten (Schritt 2a) in Abbildung 9.17. Wenn das Ergebnis negativ ist, besteht der nächste Schritt darin, den Originalwert durch Addition des Divisors zum Rest wiederherzustellen und eine 0 im Quotienten zu erzeugen (Schritt 2b). Der Divisor wird nach rechts verschoben und wir wiederholen die Schleife. Der Rest und der Quotient sind dann in den entsprechenden Registern zu finden, nachdem alle Iterationen abgeschlossen sind. Sehen wir uns dies am Beispiel einer 4-Bit-Division an, indem wir 710 durch 210 teilen, also 000001112 durch 00102 (Tabelle 9.14). Auch hierbei stellen die fettgedruckten Stellen die zu untersuchenden Bits dar. Das Ergebnis ist 310 mit dem Rest 110 . Man beachte, dass der Test auf positiven oder negativen Rest im zweiten Schritt lediglich das Vorzeichenbit des Restes überprüft. Eine überraschende Eigenschaft des Algorithmus ist, dass er n + 1 (bei Wortlänge n ) Durchläufe benötigt, um das entsprechende Ergebnis zu liefern.

9.10.2. Zweite Version einer Divisionshardware Wieder einmal erkannten die sparsamen Rechnerpioniere, dass höchstens die Hälfte des Divisors nützliche Information war und dass man deswegen den Divisor und die ALU möglicherweise um die Hälfte verkleinern könnte. Verschiebt man den Rest nach links anstatt den Divisor nach rechts, erreicht man die gleiche Ausrichtung aneinander und hat das Ziel, ALU- und Divisor-Hardware zu vereinfachen, erreicht. In Abbildung 9.18 ist das Schema für die vereinfachte Hardware dargestellt.

Abbildung 9.18.: Hardware zur Division (Zweite Version) Eine weitere Änderung folgt aus der Bemerkung, dass der erste Schritt des momentanen Algorithmus keine 1 im Quotienten-Bit erzeugen kann; denn wenn dem so wäre, würde der Quotient zu groß für das Register sein (da diese 1 sonst bei der 33. Iteration verloren gehen würde). Wenn man die Reihenfolge umdreht, sodass zuerst verschoben und dann subtrahiert wird, kann man eine Iteration des Algorithmus wegfallen lassen. Wenn der Algorithmus endet, steht der Rest in der linken Hälfte des Rest-Registers.

9.10.3. Finale dritte Version einer Divisionshardware Mit der gleichen Einsicht und Motivation wie bei der dritten Version des Multiplikationsalgorithmus sahen die Rechnerpioniere, dass das Quotientenregister abgeschafft werden

166

Technische Informatik I

9.10. Ganzzahldivision kann, wenn man die Bits des Quotienten anstatt der in den vorherigen Versionen verwendeten Nullen in das Restregister verschiebt (Abbildung 9.19).

Abbildung 9.19.: Hardware zur Division (dritte Version) Wir beginnen den Algorithmus (Abbildung 9.20) wie üblich mit dem Linksschieben des Restregisters. Daraufhin besteht die Schleife nur noch aus zwei Schritten, da das Verschieben des Restregisters den Rest in die linke Hälfte und den Quotient in die rechte Hälfte bewegt. Die Konsequenz aus der Zusammenlegung der beiden Register und der neuen Reihenfolge der Operationen in der Schleife ist, dass der Rest einmal zu oft nach links verschoben wird. Deswegen muss ein letzter Korrekturschritt daraus bestehen, dass nur die linke Hälfte des Registers um eine Ziffer nach rechts zurückzuverschieben ist. Zeigen wir auch dies am Beispiel der Division von 710 durch 210 (Tabelle 9.15). Ist der Rest negativ, wird der Algorithmus schneller, indem der Dividend nicht sofort zurück addiert wird, sondern zum verschobenen Rest im folgenden Schritt, da

(r + d ) · 2 − d = 2r + 2d − d = 2r + d

Schleife

Schritt

Divisor

Rest

0

Anfangswerte

0010

0000 0111

Rest um 1 nach links schieben

0010

0000 1110

2: Rest = Rest − Divisor

0010

1110 1110

Rest nach links schieben, R0 = 0

0010

0001 1100

2: Rest = Rest − Divisor

0010

1111 1100

0010

0011 1000

1

3b: Rest < 0 ⇒ +Divisor

2

3b: Rest < 0 ⇒ +Divisor, Rest nach links schieben, R0 = 0 3

4

5

2: Rest = Rest − Divisor

0010

0001 1000

3a: Rest ≥ 0 ⇒Rest nach links schieben, R0 = 1

0010

0011 0001

2: Rest = Rest − Divisor

0010

0001 0001

3a: Rest ≥ 0 ⇒Rest nach links schieben, R0 = 1

0010

0010 0011

Linke Resthälfte um 1 nach links schieben

0010

0001 0011

Tabelle 9.15.: Beispiel zur Division (dritte Version)

Technische Informatik I

167

9. Rechnerarithmetik

Abbildung 9.20.: Algorithmus zur Division (dritte Version)

Dieser Algorithmus heißt nicht-wiederherstellender Divisionsalgorithmus.

9.10.4. Division mit Vorzeichen Es gibt keine einfachen Algorithmen, um Division mit Vorzeichen so auszuführen, wie wir sie von der Multiplikation mit Vorzeichen kennen (z. B. Booth). Bei der Division können die Operanden jedoch vorverarbeitet werden, indem man sie in positive Werte umwandelt. Nachdem man dann einen der besprochenen Algorithmen angewandt hat, wandelt man das Ergebnis in einen korrekten vorzeichenbehafteten Wert um, falls nötig. Dabei gilt folgende Regel: Der Dividend und der Rest müssen das gleiche Vorzeichen haben, unabhängig von den Vorzeichen von Divisor und Quotient. Dazu ein Beispiel: +7 ÷ (−2) = −3 (Quotient), +1 (Rest) −7 ÷ (−2) = +3 (Quotient), −1 (Rest)

Also muss ein korrekter Algorithmus für die Division mit Vorzeichen den Quotienten negieren, falls die Vorzeichen der Operanden unterschiedlich sind und weiterhin das Vorzeichen des von Null verschiedenen Restes an den Dividenden anpassen.

168

Technische Informatik I

9.11. Festkommazahlen

9.11. Festkommazahlen Bevor wir Gleitkommazahlen einführen, wollen wir untersuchen, wie Festkommazahlen (fixed point numbers) von einem Rechner behandelt werden. Wir haben bereits demonstriert, dass man Wörter miteinander verkettet, wenn man große Zahlen auf einer Maschine mit kleiner Wortlänge darstellen will. Allerdings haben wir uns noch nicht angesehen, wie ein Rechner mit binären Brüchen umgeht. Glücklicherweise stellt ein binärer (oder ein dezimaler) Bruch kein Problem dar. Man stelle sich die folgenden zwei Rechnungen in der Dezimalarithmetik vor: 1. Fall: Ganzzahlarithmetik 7632135 + 1794821 9426956 2. Fall: Festkommaarithmetik 763,2135 + 179,4821 942,6956 Obwohl Fall 1 Ganzzahlarithmetik und Fall 2 Brucharithmetik verwendet, sind die Rechnungen gänzlich identisch. Wir können dieses Prinzip auf die Rechnerarithmetik ausweiten. Alles, was der Programmierer zu tun hat, ist sich zu erinnern, wo das Binärkomma liegt. Jegliche Eingaben des Rechners werden skaliert, damit sie dieser Vereinbarung nachkommen – genauso wie die Ausgaben. Die internen Operationen werden weiterhin so ausgeführt, als handele es sich um ganze Zahlen. Dieses Arrangement nennt man Festkommaarithmetik, da das Binärkomma immer an derselben Position angenommen wird. Das heißt, es gibt immer gleich viele Ziffern vor und nach dem Binärkomma. Der Vorteil der Festkommaarithmetik: Es wird keine spezielle komplexe Soft- oder Hardware benötigt, um sie zu implementieren. Ein einfaches Beispiel soll die Idee hinter der Festkommaarithmetik verdeutlichen. Man stelle sich eine 8 Bit lange Festkommazahl vor, deren vier vordere Bits den Ganzzahlteil realisieren und die vier hinteren den gebrochenen Teil. Was passiert nun, wenn wir die beiden Zahlen 3,625 und 6,5 addieren und das Ergebnis ausgeben lassen möchten? Ein Eingabeprogramm konvertiert dieses Zahlen zunächst in binärer Form: 3, 62510 = 11, 1012

→ 0011, 10102

6, 510 = 110, 12

→ 0110, 10002

Rechts sind die Zahlen wie im Rechner in Acht-Bit-Darstellung dargestellt. Der Rechner speichert diese Zahlen als 00111010 und 01101000. Das Binärkomma wird nur implizit gespeichert; in diesem Fall legt man vorher fest, dass die acht Bit sich zu gleichen Teilen auf Vor- und Nachkommastellen aufteilen. Diese Zahlen addiert man nun wie gehabt. 00111010 +01101000 =10100010

Technische Informatik I

(= 16210 )

169

9. Rechnerarithmetik Das Ausgabeprogramm nimmt nun dieses Ergebnis und teilt es in einen Ganzzahlteil 1010 und einen gebrochenen Teil ,0010 auf und gibt damit das dezimal korrekte Ergebnis 10,125 aus. Man beachte, dass eine Festkommazahl über mehrere Wörter verteilt werden kann um einen größeren Wertebereich als bei Nutzung eines einzelnen Wortes zu ermöglichen. Festkommazahlen haben ihre Einschränkungen. Denken wir nur an den Astrophysiker, der das Verhalten der Sonne untersucht. Solch ein Astrophysiker wird mit Größen wie der Masse der Sonne konfrontiert (1 990 000 000 000 000 000 000 000 000 000 000 g), ebenso wie mit der Masse eines Elektrons (0,000 000 000 000 000 000 000 000 000 910 956 g). Wenn Astrophysiker an Festkommazahlen gebunden wären, würden sie eine riesig große Anzahl von Bytes benötigen, um einen großen Bereich von Zahlen darzustellen. Ein einzelnes Byte kann Werte zwischen 0 und 255 annehmen, also rund von 0 bis 14 Tausend. Wenn unser Physiker nun mit astronomisch großen und mikroskopisch kleinen Zahlen arbeiten will, würde er grob geschätzt 14 Bytes für den Ganzzahlteil und 12 Bytes für den gebrochenen Teil brauchen – ein 26-Byte-Wort (208 Bit). Nun gibt es aber zwei wichtige Parameter für die Güte einer Zahlendarstellung: 1. Der Wertebereich (range) einer Zahl sagt uns, wie klein oder wie groß sie sein kann. Im Falle des Astrophysikers würde es sich dabei um Zahlen in der Größenordnung von 2 · 1033 bis hin zu winzigen Werten wie 9 · 10−28 handeln. Dies sind 61 Dekaden, also ein Wertebereich von 1061 . Der Wertebereich von Zahlen, die in digitalen Rechnern dargestellt werden, muss ausreichend für die riesige Mehrheit von möglichen Berechnungen sein. Wenn der Rechner in einer bestimmten Anwendung eingesetzt wird, ist der Wertebereich der zu behandelnden Daten bekannt und meistens klein, sodass sich der Wertebereich der gültigen Zahlen beschränken lässt. Dadurch vereinfachen sich auch die Anforderungen an Hard- und Software. 2. Die Präzision (precision) einer Zahl ist ein Maß ihrer Exaktheit oder Genauigkeit und hängt mit der Anzahl der zur Darstellung notwendigen signifikanten Ziffern zusammen. Zum Beispiel lässt sich die Konstante π als 3,142 oder 3,141592 schreiben. Letztere Form ist präziser als die erste, da mehr Ziffern von π angegeben sind. Diese beiden Parameter sollten unabhängig voneinander sein. Natürlich lässt sich durch die Verkettung von Worten ein genügend großer Wertebereich schaffen. Aber dieser enorme Aufwand wäre in diesem Fall extrem ineffizient, da mit diesem Mittel automatisch auch eine Erhöhung der Präzision einhergeht, die gar nicht zu erreichen ist. Die Masse der Sonne ist nicht nur mir einer Genauigkeit von fünf Ziffern bekannt, sondern mit 57 Ziffern. In der Praxis lassen sich nur wenige Messungen jeglicher Art auf 62 Stellen genau machen (meist ist dies auch überhaupt nicht nötig). Obwohl es möglich ist, alle Zwischenergebnisse auf 62 Stellen genau zu behalten und bei der Ausgabe 50 oder 60 Stellen zu ignorieren, ist dies eine Verschwendung von CPU-Zeit und Speicher.

9.12. Gleitkommazahlen Was wir brauchen, ist ein System zur Zahlendarstellung, in dem der Wertebereich der Zahlen unabhängig von der Anzahl der signifikanten Ziffern (sprich: der Genauigkeit) ist. Im Folgenden wollen wir so ein System besprechen. Es basiert auf einer wissenschaftlichen Notation, wie sie in der Physik, der Chemie und den Ingenieurswissenschaften üb-

170

Technische Informatik I

9.12. Gleitkommazahlen lich ist. Die computerarithmetische Entsprechung der wissenschaftlichen Notation sind Gleitkommazahlen (floating-point numbers). Zahlen werden dabei als n = a ×re

geschrieben, wobei a die Mantisse (oder Argument), r die Basis (oder Radix) und e der Exponent ist. Ein Rechner speichert gebrochene Zahlen in solch einer Form, indem er die binäre Sequenz in zwei Felder aufteilt: Exponent e

Mantisse a

Dies repräsentiert n = a × 2e . Es wird aufgrund des binären Zahlensystems immer die Basis 2 verwendet und muss deshalb nicht explizit gespeichert werden. Durch diese explizite Trennung in zwei Felder wird nun auch erreicht, dass das Komma nicht mehr starr an einer bestimmten Stelle steht, sondern gleitet, je nach Definition des Exponenten. Die normalisierte Darstellung ist n = 1, xx . . . x 2 × 2 y y...y . Mantissen werden dabei einheitlich mit dem Binärkomma hinter dem ersten von 0 verschiedenen Bit dargestellt. Zum Beispiel wird so aus 0, 00001010 × 20 die Darstellung 1, 010 × 2−5 . Da das höchstwertige Bit per Definition 1 ist, kann es bei der Darstellung eingespart werden. Das frei werdende Bit ermöglicht, eine Stelle mehr von der Mantisse abzuspeichern. Dadurch wird die Genauigkeit erhöht. Weiterhin ist ein Vorzeichenbit notwendig. Gleitkommazahlen werden also folgendermaßen dargestellt: (−1)s × a × 2e mit 110 ≤ a ≤ 210

9.12.1. Mögliche Systeme für Gleitkommazahlen Um eine Darstellungsform für Gleitkommazahlen in einem bestehenden Rechner zu schaffen, muss ein Entwickler die folgenden fünf Entscheidungen treffen: 1. Die Anzahl der benutzen Wörter (und damit auch die Gesamtanzahl der Bits) 2. Die Art der Darstellung der Mantisse (Zweierkomplement etc.) 3. Die Art der Darstellung des Exponenten 4. Die Anzahl der Bits, die für Mantisse und Exponent aufgewendet werden 5. Die Anordnung von Mantisse und Exponent im Wort (Mantisse zuerst oder Exponent zuerst) Der vierte Punkt ist einige weitere Gedanken wert. Sobald der Entwickler sich für eine Gesamtanzahl von Bits für die Darstellung der Gleitkommazahl entschieden hat (ein ganzzahliges Vielfaches der einzelnen Wortlänge), muss er diese Darstellung in Mantisse und Exponent aufteilen. Wenn er sich entscheidet, viele Bits dem Exponenten zu widmen, resultiert daraus eine Gleitkommazahl mit einem großen Wertebereich. Diese Exponentenbits wurden allerdings auf Kosten der Mantisse gewonnen, sodass die Präzision der Gleitkommazahl reduziert wird. Andersherum reduziert eine große Mantisse den Wertebereich, sorgt aber für eine höhere Präzision. Aufgrund der oben genannten fünf Kriterien hatten Rechnerhersteller bis ungefähr 1980 jeweils ein eigenes Gleitkomma-Zahlenformat. Es ist wohl unnötig hinzuzufügen, dass

Technische Informatik I

171

9. Rechnerarithmetik diese Formate alle verschieden waren. Weitaus schlimmer war, dass manche dieser Formate eine fehlerhafte Arithmetik besaßen, da die Gleitkommaarithmetik manche Tücke besitzt, die dem durchschnittlichen Hardwareentwickler nicht bekannt ist. Um diesen Missstand zu beseitigen, setzte das IEEE (Institute of Electrical and Electronics Engineers) in den späten Siebzigern des zwanzigsten Jahrhunderts ein Komitee zusammen, welches die Gleitkommaarithmetik standardisieren sollte. Das Ziel war nicht nur, dass Gleitkommadaten zwischen verschiedenen Rechnern ausgetauscht werden können, sondern auch, Hardwareentwicklern ein Modell zu liefern, dass man als korrekt kennt. Das Ergebnis führte zum IEEE-Standard 754 (IEEE, 1985). Die meisten der heutigen CPUs (inklusive Intel und SPARC) beinhalten Gleitkommainstruktionen, die dem GleitkommaStandard der IEEE entsprechen. Dieser Standard hat sowohl die Einfachheit der Portierung von Gleitkommaprogrammen als auch die Qualität der Rechnerarithmetik entscheidend verbessert. Die Entwickler von IEEE 754 wollten auch eine Gleitkommadarstellung, die schnell und einfach Ganzzahlvergleiche ausführen konnte, was z. B. ein allgemeines Kriterium beim Sortieren ist. Dieser Drang erklärt die Position des Vorzeichenbits (an höchstwertiger Stelle im Wort), durch die ein Test auf „größer gleich“, „kleiner gleich“ oder „gleich Null“ schnell ausgeführt werden kann. Die Platzierung des Exponenten vor der Mantisse vereinfacht auch das Sortieren von Gleitkommazahlen mit Instruktionen zum Ganzzahlvergleich, da Zahlen mit höheren Exponenten auch immer größer als Zahlen mit kleinerem Exponenten sind – solange beide Zahlen das gleiche Vorzeichen haben (es ist jedoch ein wenig komplizierter als ein einfacher Ganzzahlvergleich, da diese Notation im Wesentlichen aus Vorzeichen und Mantisse besteht anstatt aus dem Zweierkomplement). Eine Gleitkommazahlendarstellung muss positive und negative Exponenten und Mantissen einschließen. Die Mantisse kann hierbei problemlos durch das Zweierkomplementverfahren dargestellt werden. Negative Exponenten stellen hingegen eine Herausforderung für vereinfachtes Sortieren dar. Wenn wir das Zweierkomplement oder eine andere Notation nutzen, in der negative Zahlen eine 1 im höchstwertigen Bit haben, sieht ein negativer Exponent wie eine große Zahl aus. Eine +110 wäre als 8-Bit-Exponent 000000012 , der negierte Wert −110 dagegene würde als 111111112 dargestellt. Als Ausweg wurde die so genannte Exzess-Darstellung gewählt. Bei dieser addiert man einen konstanten Wert, der der Hälfte des Wertebereichs entspricht, auf den Exponenten (der so genannte „bias“). Dadurch wird selbst die kleinste darstellbare Zahl positiv (oder 0). Sind aber alle Zahlen positiv, ist der Größenvergleich trivial, da hierbei die gleich Ordnung gilt wie bei positiven Ganzzahlen. Hinter „Exzess“ wird üblicherweise der Verschiebungsbetrag notiert. Bei acht verfügbaren Bits bedeutet das, dass 256 Werte darstellbar sind. Die Hälfte minus Eins (warum, wird gleich gezeigt) ist der Verschiebungsbetrag, also 127. Folglich würde die Darstellung Exzess-127 heißen. IEEE 754 benutzt eine Verschiebung um den Wert 127 für die Darstellung mit einfacher Genauigkeit, sodass −1 im Binärmuster durch den Wert −1 + 127 = 12610 = 011111102 und +1 durch den Wert +1 + 127 = 12810 = 100000002 dargestellt wird. Die Extremwerte in der Exzess-Darstellung, also bei acht Bit die 000000002 und die 111111112 , haben Spezialbedeutungen und stehen deshalb für die Wertebelegung des Exponenten nicht zur Verfü-

172

Technische Informatik I

9.12. Gleitkommazahlen gung. Der Exponent e liegt daher im Wertebereich [0, 255]. Dies bedeutet, dass der reale Exponent aus dem Bereich −126 ≤ e ≤ 127 sein muss. Abbildung 9.21 zeigt die 32-Bit-Darstellung der Gleitkommazahl im IEEE-754-Format.

Darstellung entspricht: (−1)S · 1, M · 2E −127 Abbildung 9.21.: Gleitkommazahl einfacher Genauigkeit nach dem IEEE-Standard 754 Das Vorzeichen der Zahl wird durch das erste Bit gegeben, gefolgt von der Darstellung des Exponenten (zur Basis 2) in Exzess-127-Darstellung. Die letzten 23 Bit repräsentieren die Mantisse. Wie in der Einleitung dieses Abschnitts beschrieben, wird sie normalisiert, wodurch immer genau eine 1 vor dem Komma steht. Diese Eins kann man dadurch weglassen und das frei werdende Bit für eine höhere Genauigkeit bei den Nachkommastellen verwenden. Die 23 gespeicherten Bits sind also der gebrochene Teil der Mantisse (den man auch Signifikant nennt). Die Mantisse ist somit eigentlich 24 Bits lang. Ein Beispiel für eine solche Gleitkommazahl zeigt Abbildung 9.22. 0

00101000

0010100 . . . 0

Darstellung entspricht: (−1)0 · 1, 0010100 . . . 0 · 2−87 Abbildung 9.22.: Beispiel für eine Gleitkommazahl einfacher Genauigkeit Der eben besprochene 32-Bit-Standard wird als einfache Genauigkeit bezeichnet, da er sich £ ¤ durch ein einzelnes 32-Bit-Wort darstellen lässt. Der Wertebereich ist 2−126 , 2127 , was ungefähr 10±38 entspricht. Die 24-Bit-Mantisse bietet ungefähr die gleiche Genauigkeit wie ein 7-stelliger Dezimalwert. Um mehr Präzision und einen größeren Wertebereich zu bieten, spezifiziert der IEEE-Standard auch ein Format mit doppelter Genauigkeit (Abbildung 9.23).

Darstellung entspricht: (−1)S · 1, M · 2E −1023 Abbildung 9.23.: Gleitkommazahl doppelter Genauigkeit nach dem IEEE-Standard 754 Diese doppelte Genauigkeit hat vergrößerte Exponent- und Mantissenbereiche. Der 11Bit-Exponent (mit einer Verschiebung von 1023) liegt im Intervall 0 < E < 2047 für normale Werte, wobei 0 und 2047 selbst wie zuvor Spezialfällen vorbehalten sind. Dadurch liegt

Technische Informatik I

173

9. Rechnerarithmetik der echte Exponent e im Bereich [−1022, 1023] und bietet damit einen Wertebereich von £ −1022 1023 ¤ 2 ,2 (ungefähr 10±308 ). Die 53-Bit-Mantisse bietet eine Genauigkeit wie etwa 16 Dezimalziffern.// Es gibt vier Spezialfälle, die durch die Extremwerte im Exponenten und einer Mantissenbelegung, die gleich oder ungleich Null ist, signalisiert werden. Diese vier Fälle sind: 1. Denormalisierung: Sobald der kleinste Exponent, also zum Beispiel 2−126 bei einfacher Genauigkeit, zu groß ist für eine Zahl, die dargestellt werden soll, würde diese Zahl auf Null gerundet werden. Die Entwickler des IEEE-754-Standards drückten diese Grenze weiter gegen Null, indem sie als Spezialfall eine denormalisierte Mantisse zuließen; diese wird durch eine Null im Exponenten signalisiert. Der Signifikant ist in dem Fall ein Bitmuster ungleich Null und hat statt der implizieten 1 links des Kommas in dem Fall eine 0. 2. Null: Die Null wird durch eine Null im Exponenten und eine Null im gesamten Signifikanten dargestellt. Man beachte, dass es eine positive und eine negative Null gibt. 3. Unendlichkeit: Der „Nichtwert“ Unendlichkeit wird signalisiert, indem der Exponent auf den Maximalwert und der Signifikant auf 0 gesetzt wird. 4. keine Zahl (NaN): Dividiert man beispielsweise „Unendlich“ durch „Unendlich“, so ist das Ergebnis undefiniert. Für diesen Fall ist die Bitbelegung „Exponent auf Maximalwert, Signifikant ungleich Null“ vorgesehen und signalisiert Not a Number (NaN). Jede kommerzielle Implementierung eines Rechners muss mindestens die Darstellung mit einfacher Genauigkeit beherrschen, um dem IEEE-Standard zu entsprechen. Die Darstellung mit doppelter Genauigkeit ist optional. Sie kann aber auch für die Darstellung von Zwischenwerten in einer Serie von Berechnungen benutzt werden. Zum Beispiel kann das Kreuzprodukt zweier Zahlenvektoren ausgerechnet werden, indem man die Summen der Produkte mit erweiterter Genauigkeit zusammenrechnet. Die Eingaben werden dabei mit einfacher Genauigkeit erfolgen und das Endergebnis wird auf genau diese einfache Genauigkeit zurechtgestutzt. Die Verwendung von erweiterten Formaten reduziert die Größe der Rundungsfehler in einer Sequenz von Berechnungen.

9.12.2. Akkurate Arithmetik Im Gegensatz zu Ganzzahlen, welche jede Zahl in ihrem Wertebereich exakt darstellen können, sind Gleitkommazahlen normalerweise nur Annäherungen an eine Zahl, die sie nicht wirklich darstellen können. Der Grund dafür ist, dass zwischen – zum Beispiel – 010 und 110 unendlich viele reelle Zahlen existieren, aber nicht mehr als 253 davon (bei doppelter Genauigkeit) exakt dargestellt werden können. Man quantisiert daher die gewünschte Zahl so, dass sie auf die ihr am nahe kommenste Gleitkommazahl abgebildet wird. Im Mittel sollte eine solche Operation fehlerneutral sein, d. h. die Abweichung von der tatsächlichen Zahl sollte im Mittel Null sein. Diesen Vorgang des Quantisierens nennt man Runden. Da sich verschiedene Rundungsmethoden gibt, die unterschiedliche Stärken und Schwächen besitzen, bietet IEEE 754 mehrere Rundungsschemata an, um dem Programmierer die gewünschte Annäherung wählen zu lassen.

174

Technische Informatik I

9.12. Gleitkommazahlen In den vorangegangenen Beispielen waren unsere Aussagen über die Anzahl der Bits, die ein Zwischenergebnis einnehmen kann, nur sehr ungenau. Es sollte jedoch klar sein, dass es keine Möglichkeit zum Runden gäbe, wenn jedes Zwischenergebnis auf die exakte Anzahl von Ziffern gerundet würde. IEEE 754 nutzt deswegen zwei zusätzliche Bits während der Zwischenrechnungen, die man guard und round nennt. Addieren wir beispielsweise 2, 99 · 100 zu 2, 34 · 102 und nehmen wir dazu an, dass wir drei signifikante Dezimalziffern haben. Es können jedoch nur Zahlen mit gleichem Exponenten addiert werden. Daher muss zunächst das Komma der kleineren Zahl zuerst nach links verschoben werden, um die Exponenten anzugleichen. So wird aus 2, 99 · 100 die Zahl 0, 0299 · 102 . Da wir die Zusatzstellen für guard und round haben, brauchen wir die entstandene Zahl nicht zu runden. Die Summe berechnet sich wie folgt:

2, 3400 +0, 0299 =2, 3699

Das Aufrunden auf 2,37 liegt nahe. Ohne die zwei zusätzlichen Ziffern wäre 0, 0299 auf 0, 02 gerundet worden, bevor das Ergebnis 2,36 berechnet worden wäre:

2, 34 +0, 02 =2, 36

Wie man sieht, verursacht dieses vorzeitige Runden also einen Fehler, der wie im Beipiel sehr groß sein kann. Das Entfernen der zusätzlichen Bits beim Erzeugen des Endergebnisses wirft ein wichtiges Thema auf. Ein Binärbruch muss gerundet werden, damit er im Computer darstellbar bleibt. Wie erwähnt, ist das Rundungsergebnis eine mehr oder weniger schlechtere Annäherung des längeren Wertes. Dieses Problem tritt auch anderswo auf, zum Beispiel beim Konvertieren von Dezimalbrüchen in eine binäre Darstellung. Das Runden kann auf mehreren Wegen erfolgen. Der einfachste Weg ist, die zusätzlichen Bits einfach zu entfernen und keine Änderungen in den verbleibenden Bits zu machen. Diese Methode wird als Abrunden bzw. Abschneiden (truncation oder chopping) bezeichnet. Nehmen wir an, wir wollen einen Bruch mit sechs Bits auf drei Bits runden. Alle Brüche im Bereich 0, b−1 b−2 b−3 000 bis 0, b−1 b−2 b−3 111 werden dabei auf 0, b−1 b−2 b−3 reduziert. Der Fehler im 3-Bit-Ergebnis hat eine Größe von 0 bis 0, 000111. In anderen Worten: Der Fehler beim Runden reicht von 0 bis fast 1 in der niederwertigsten Position der verbleibenden Bits. In unserem Beispiel ist das die Position b−3 . Das Abschneiden ist eine nicht erwartungstreue Funktion: Die Abweichung führt zu einer verzerrten Schätzung der eigentlichen Zahl; die verursachten Fehler verteilen sich nicht gleichmäßig und symmetrisch um Null. Haben wir eine arithmetische Operation, bedeutet das Abrunden das gleiche, als wenn man auf die zusätzlichen Bits verzichtet. Die nächste Rundungsmethode ist das so genannte Von-Neumann-Runden. Wenn die zu entfernenden Bits alle gleich Null sind, werden sie einfach ohne weitere Änderungen an

Technische Informatik I

175

9. Rechnerarithmetik den verbleibenden Bits verworfen. Wenn aber nur ein Bit der zu entfernenden gleich 1 ist, wird das niederwertigste der verbleibenden Bits auf 1 gesetzt. In unserem Beispiel würden also alle 6-Bit-Brüche mit b−4 b−5 b−6 6= 000 auf 0, b−1 b−2 1 gerundet werden. Der Fehler bei dieser Methode reicht von −1 bis +1 in der niederwertigsten Position der verbleibenden Bits. Das Von-Neumann-Runden ist erwartungstreu, d. h. verzerrt den Erwartungswert nicht, da sich Auf- und Abrunden im Mittel aufhebt. Die dritte Methode ist das kaufmännische Runden. Kaufmännisches Runden rundet, indem es 0, 5 auf die zu rundende Stelle addiert, also die die Hälfte der letzten stehenbleibenden Stelle. Für Binärzahlen bedeutet das, dass (um beim Beispiel zu bleiben) auf die Stelle b 4 eine 1 addiert und danach abgeschnitten wird, sodass nur noch 0, b −1 b −2 b −3 stehen bleiben. Mit anderen Worten: 0, b−1 b−2 b−3 1 . . . wird auf 0, b−1 b−2 b−3 + 0, 001 gerundet und 0, b −1 b −2 b −3 0 . . . auf 0, b −1 b −2 b −3 . Sind die Nachkommastellen b 4 b 5 b 6 . . . = 100 . . ., so ist die zu rundende Zahl genau gleich weit entfernt von zwei gerundeten Zahlen. In diesem Fall wird immer aufgerundet; darum ist das kaufmännische Runden leicht positiv verzerrt, was jedoch nur bei sehr vielen aufeinander aufbauenden Rundungen feststellbar ist. Der Ausweg ist das so genannte mathematische Runden, bei dem immer zur nächsten geraden Zahl gerundet wird. Dieses Runden ist unverzerrt und wird im IEEE-754-Standard standardmäßig verwendet.

9.12.3. Vereinfachte Algorithmen zur Berechnung von Gleitkommazahlen Die folgenden Algorithmen beschreiben grob das Vorgehen beim Rechnen mit Gleitkommazahlen. • Addition und Subtraktion – Rechtsverschiebung auf der Mantisse des kleineren Operanden zur Angleichung der Exponenten – Exponent der Summe (bzw. Differenz) := Exponent des größeren Operanden – Addition (bzw. Subtraktion) der Mantissen und Bestimmung des Vorzeichens – Wenn nötig, Normalisierung des Ergebnisses • Multiplikation und Division – Addiere (bzw. Subtrahiere) die Exponenten und subtrahiere (bzw. addiere) 12710

– Multipliziere (bzw. Dividiere) die Mantissen und bestimme das Vorzeichen – Wenn nötig, Normalisierung des Vorzeichens Die explizite Addition (bzw. Subtraktion) von 127 bei der Multiplikation und Division trifft bei Gleitkommazahlen einfacher Genauigkeit zu; bei doppelter Genauigkeit muss entsprechend 102310 addiert oder subtrahiert werden. Dies ist notwendig, weil der Exponent in Exzess-Darstellung gespeichert wird, d. h. der kleinstmögliche (negative) Wert wird um den Exzess-Wert (127 oder 1023) verschoben, sodass er nichtnegativ wird. Addiert man zwei Zahlen in dieser Darstellung, so fließt der Exzess-Wert doppelt in die Summe ein und muss dementsprechend einmal subtrahiert werden. Ist der Exzess x und die (eigentlichen) Exponenten a und b sollen addiert werden, so lautet die Rechnung (a + x)+(b + x) = a +b +2x . Entsprechendes gilt für die Subtraktion ((a + x)−(b + x) =

176

Technische Informatik I

9.13. Zusammenfassung Computerarithmetik a − b ), bei der der Exzess mit subtrahiert wird und dementsprechend im Anschluss wie-

der addiert werden muss, damit auch das Ergebnis in Exzessdarstellung steht. In Abbildung 9.24 ist ein Schaltplan für die Hardware-Implementierung einer GleitkommaAdditions-Subtraktions-Einheit dargestellt.

Abbildung 9.24.: Hardware für die Addition und Subtraktion von Gleitkommazahlen Ohne sich in Einzelheiten zu verlieren, sieht man die Komplexität und erkennt in den einzelnen Blöcken die Aktionen wieder, die schon diskutiert wurden. Es wird jedenfalls einleuchtend, dass in Hochleistungsprozessoren ein maßgebender Teil der verfügbaren Chipfläche für Gleitkommzahlenoperationen verwendet wird. Bevor es möglich war, diese Fähigkeiten im Prozessorchip zu implementieren, beinhalteten frühere Rechner einen speziellen Gleitkomma-Koprozessor. Dieser hatte die gleiche Komplexität wie der Hauptprozessor.

9.13. Zusammenfassung Computerarithmetik Ein Computer kann nur mit endlichen Zahlen rechnen. Dies beschränkt seine Möglichkeiten. Selbst der IEEE-754-Standard für die Darstellung von Gleitkommazahlen ist wie jede andere Darstellung fast immer nur eine Approximation der realen Zahlen. Daher müssen Rechnersysteme dafür sorgen, den Unterschied zwischen menschlicher und Rechnerarithmetik zu minimieren. In dem vorangegangenen Abschnitt wurden verschiedene Techniken vorgestellt, die dabei eingesetzt werden sollen: • Carry-lookahead-Techniken für Addierer mit großer Leistungsfähigkeit und Geschwindigkeit • Für schnelle Multiplizierer: Booth-Algorithmus und Bitpaar-Recoding zur Reduzierung der Anzahl der für die Erzeugung des Produkts benötigten Rechenschrit-

Technische Informatik I

177

9. Rechnerarithmetik te, Carry-save-Addition zur nochmaligen substantiellen Reduzierung auf letztlich zwei zu addierenden Summanden (mittels Carry-Lookahead) Programmierer eines Rechners sollten sich dieser Zusammenhänge bewusst sein.

178

Technische Informatik I

10. Codes In einem Computer findet man, sobald von den analogen Bausteinen auf die digitale Ebene abstrahiert wurde, im Wesentlichen Nullen und Einsen vor. Diese steuern die internen Abläufe oder stellen Daten dar, je nachdem, wo sie im Rechner auftauchen und wie sie interpretiert werden. Damit aber überhaupt aus Nullen und Einsen Daten oder Befehle werden, muss sichergestellt werden, dass die entsprechenden Geräte der gleichen Interpretation der Bitmuster folgen. Ein n -Bit-Wort kann eins von 2n unterschiedliche Bitmustern beinhalten. Diese Bitmuster haben von sich aus keine Bedeutung; diese bekommen sie erst durch eine entsprechende Programmierung, die die Syntax (die Anordnung von Einsen und Nullen) mit einer Semantik A (die Bedeutung bestimmter Anordnungen) verknüpft. Die Abbildungsvorschrift C , die diese Verknüpfung vornimmt, nennt man Code. C definiert sich also wie folgt: C : {0, 1} → A . Der Rechner selbst kann keine Aussage über die Bedeutung eines Wortes treffen; er behandelt es lediglich so, wie der Programmierer es vorschreibt. Dadurch ist es möglich, Buchstaben zu „multiplizieren“, auch wenn dies in den meisten Fällen ohne Sinn ist. Es gibt wohl unendlich viele Möglichkeiten, einem Bitmuster eine Bedeutung beizumessen. Zwei typische, häufig anzutreffende Vertreter sind: 1. Die Instruktion: Instruktionen sind Befehle eines Programms, die dem Prozessor mitteilen, was er als nächstes tun soll. Da Menschen besser mit Zeichenfolgen und Computer besser mit Zahlen umgehen können, werden bestimmten Bitfolgen Befehle zugeordnet. 2. Die Zahl: Zahlen können auf viele Arten im Computer auf Binärebene dargestellt werden. Dazu gehören binäre Ganzzahlen mit oder ohne Vorzeichen, Gleitkommazahlen, binär codierte Dezimalzahlen (BCD) und andere, wie im Kapitel 9 gezeigt. Die Zuordnung der Semantik ist auf dieser Ebene hardwareabhängig. In diesem Kapitel wird auf verschiedene Arten der Interpretation eingegangen.

10.1. Zeichencodes Jeder Rechner hat eine Menge von Zeichen (Alphabet genannt), mit der er arbeitet. Als ein grobes Minimum beinhaltet diese Menge die 26 Großbuchstaben, die 26 Kleinbuchstaben, die zehn Ziffern von 0 bis 9 und eine Menge von Sonderzeichen, wie z. B. Leerzeichen, Punkt, Minus, Komma, Semikolon, und Steuerzeichen wie den Zeilenwechsel; also Zeichen, die man auf einer Tastatur vorfindet. Um diese Zeichen in einen Rechner zu übertragen, wird jedem von ihnen eine Zahl zugewiesen: Zum Beispiel: a = 1, b = 2, . . . z = 26, + = 27, − = 28 Die Zuweisung von Zeichen zu Ganzzahlen nennt man Zeichencode.

179

10. Codes Sollen Rechner miteinander kommunizieren, so ist es wichtig, dass sie denselben Code verwenden. Aus diesem Grund wurden Standards entwickelt. Im Folgenden werden wir zwei der wichtigsten Standards näher betrachten.

10.1.1. ASCII oder „ISO 7-bit character code“ Einer der ältesten Codes, der weite Verbreitung fand und auch heute noch Auswirkungen hat, ist der ASCII-Code (American Standard Code for Information Interchange). Jedes ASCII-Zeichen wird mit sieben Bits beschrieben, was 128 verschiedene Zeichen erlaubt. Tabelle 10.1 stellt den ASCII-Code dar. Die Codewörter 00 bis 1F (hexadezimal) sind nicht druckbare Steuerzeichen. Die Steuerzeichen sind historisch bedingt: Der Code sollte nicht nur die druckbaren Zeichen enthalten, sondern auch beispielsweise Drucker ansteuern. Dies sieht man zum Beispiel am Zeichen 10 (0A hex), welches für Zeilenvorschub (line feed) steht und das Papier um eine Zeile weiterschieben soll. Ein weiterer Einsatzzweck war die Datenfernübertragung, wie man an den Zeichen 1–4 sehen kann (SOH=Start of Header, STX=Start of Text, ETX=End of Text, EOT=End of Transmission). Steuerzeichen spielen heute nur noch eine Nebenrolle. Zeichen werden normalerweise zu Zeichenketten (Strings) kombiniert, welche eine variable Anzahl von Zeichen beinhalten können. Es gibt drei Möglichkeiten, eine Zeichenkette darzustellen: 1. Die erste Position des Strings ist für eine Angabe der Länge der Zeichenkette reserviert. 2. Eine dazugehörige Variable beinhaltet die Länge der Zeichenkette. 3. Die letzte Position der Zeichenkette wird durch ein spezielles Zeichen angegeben, die Zeichenkette wird also durch dieses Zeichen terminiert (beendet). Die zweite Methode findet z. B. bei Headern von Netzwerkpaketen Anwendung, um anzugeben, wie lang das angehängte Datenfragment ist. Die dritte Methode wird in der Programmiersprache C eingesetzt; ein String wird dabei durch ein Byte mit dem Wert 0 terminiert. Demzufolge würde die Zeichenkette „Cal“ in C durch vier Bytes mit den dezimalen Werten 67, 97, 108, 0 dargestellt werden (43, 61, 6C, 00 hexadezimal).

10.1.2. ASCII-Fortentwicklung Als Code des amerikanischen Standardisierungsinstituts nimmt ASCII keine Rücksicht auf Sonderzeichen der Schriftsprache, die in nicht-englischen Sprachen anwendung finden. So fehlen die deutschen Umlaute und das Eszett, aber auch beispielsweise die französischen Zeichen mit Akzent. Schriftalphabete, die eine ganz andere Basis haben, wie zum Beispiel Russisch oder Arabisch, werden ebenfalls nicht berücksichtigt. Der erste Versuch, ASCII zu erweitern, war der ISO 646, welcher ASCII um ein Bit erweiterte und so weitere 128 Zeichen hinzufügte. Dieser Code wird auch Latin-1 genannt. Die zusätzlichen Zeichen waren meist lateinische Buchstaben mit Akzent- oder Umlautmarkierungen. Der nächste Schritt war ISO 8859, welcher das Konzept der Codeseiten einführte. Eine Codeseite ist eine Menge von 256 Zeichen für eine bestimmte Sprache oder eine Gruppe von Sprachen. ISO 8859-1 ist Latin-1, ISO 8859-2 bedient slawische Sprachen mit lateinischen Buchstaben (z. B. Tschechisch, Polnisch und Ungarisch). ISO

180

Technische Informatik I

10.1. Zeichencodes

ASCII-Code

Zeichen

ASCII-Code

Zeichen

ASCII-Code

Zeichen

ASCII-Code

00

NUL

20

SP

01

SOH

21

!

02

STX

22

03

ETX

04

EOT

05

Zeichen

40

@

60

`

41

A

61

a



42

B

62

b

23

#

43

C

63

c

24

$

44

D

64

d

ENQ

25

%

45

E

65

e

06

ACK

26

&

46

F

66

f

07

BEL

27



47

G

67

g

08

BS

28

(

48

H

68

h

09

TAB

29

)

49

I

69

i

0A

LF

2A

*

4A

J

6A

j

0B

VT

2B

+

4B

K

6B

k

0C

FF

2C

,

4C

L

6C

l

0D

CR

2D

-

4D

M

6D

m

0E

SO

2E

.

4E

N

6E

n

0F

SI

2F

/

4F

O

6F

o

10

FLE

30

0

50

P

70

p

11

DC1

31

1

51

Q

71

q

12

DC2

32

2

52

R

72

r

13

DC3

33

3

53

S

73

s

14

DC4

34

4

54

T

74

t

15

NAK

35

5

55

U

75

u

16

SYN

36

6

56

V

76

v

17

ETB

37

7

57

W

77

w

18

CAN

38

8

58

X

78

x

19

EM

39

9

59

Y

79

y

1A

SUB

3A

:

5A

Z

7A

z

1B

ESC

3B

;

5B

[

7B

{

1C

FS

3C




5E

^

7E

~

1F

US

3F

?

5F

_

7F

DEL

Tabelle 10.1.: ASCII-Tabelle

Technische Informatik I

181

10. Codes 8859-3 beinhaltet die Zeichen, die man für Türkisch, Maltesisch, Esperanto oder Gälisch benötigt, usw. An dieser Stelle war zwar das Problem mit den Akzenten und gelegentlichen Sonderzeichen gelöst, allerdings wurde es dadurch notwendig, sich nicht nur auf einen bestimmten Code, sondern zusätzlich auf eine bestimmte Seite zueinigen, um fehlerfrei kommunizieren zu können. Das Problem komplett anderer Alphabetschemata wie Japanisch oder Chinesisch war an dieser Stell noch gar nicht angegangen worden.

10.1.3. Unicode Unicode entstand aus der Idee heraus, einen Code zu etablieren, der die meisten Schreibsysteme der Welt enthält. Dadurch sollte es möglich werden, auf einem Rechner zugleich in mehreren Sprachsystemen arbeiten zu können, zum Beispiel Deutsch und Koreanisch. Die Grundidee hinter Unicode war ursprünglich, jedem Zeichen und Symbol einen einzigartigen und dauerhaften 16-Bit-Wert zuzuweisen, der Codepunkt genannt wurde. Mit einer Symbolbreite von 16 Bits hat Unicode 216 = 65536 mögliche Codepunkte. Um rückwärtskompatibel zu bleiben und die Akzeptanz zu steigern, wurde der Latin-1-Zeichensatz unverändert in den Anfang des Unicodes übernommen. Es stellte sich aber schnell heraus, dass diese Codepunktkapazität zu gering für das Vorhaben ist. Daher wurde mit Unicode 2.0 das Konzept der Planes (Ebenen) eingeführt, bei der jedes Plane 65536 Zeichen enthält. Es wurden zunächst 17 Ebenen eingefügt, sodass insgesamt 1.114.112 Zeichen speicherbar sind. Bei dem im April 2008 erschienenen Unicode 5.1 waren davon 100.713 Codepunkte vergeben. Jede Sprache bekommt mehr Codepunkte zugewiesen, als sie Buchstaben hat. Diese Entscheidung beruht zum Teil auf der Tatsache, dass viele Sprachen mehrere Formen eines Buchstaben haben. Zum Beispiel unterscheiden wir im lateinischen Alphabet zwischen GROSSBUCHSTABEN und kleinbuchstaben. Andere Sprachen haben drei oder mehr verschiedene Formen, je nachdem, ob ein Buchstabe am Anfang, in der Mitte oder am Ende eines Wortes steht. Zusätzlich zu den Alphabeten wurden weitere Codepunkte zugewiesen; unter anderem für Umlautzeichen, Satzzeichen, Hoch- und Tiefzeichen, Währungssymbole, geometrische Formen und mathematische und grafische Symbole.

10.2. Bildcodierung Neben der Verwendung zur Zeichen- und Befehlscodierung können Bitfolgen auch Bilder repräsentieren. Der einfachste Ansatz ist die direkte Umsetzung von Schwarzweißbildern in Bitfolgen: Ordnet man einer Null einem schwarzen Bildpunkt und einer Eins einen weißen Bildpunkt zu, so lassen sich zweifarbige Bilder recht einfach binär darstellen. Diese Art der Bildcodierung nennt sich Bitmap. Zusätzlich zu den reinen Bildinhalten müssen dabei Metadaten über das Bild gespeichert werden, zum Beispiel die Höhe und Breite. Diese Darstellungsart benötigt relativ viel Speicher im Verhältnis zu den dargestellten Informationen. Ein möglicher Ausweg ist, anstatt von Bildrasterpunkten vordefinierte Symbole zu referenzieren, die größer sind als die zur Referenzierung notwendigen Bits. Dieser Ansatz wird beispielsweise beim Videotext verwendet. Auf diese Weise lassen sich jedoch nur grobe Bilder übertragen.

182

Technische Informatik I

10.3. Ungewichtete Codes Bilder bestehen aber meistens nicht aus lediglich zwei Farben und sind – gerade bei Fotografien – aus sehr vielen Pixeln zusammengesetzt. Ein Beispiel: Ein Digitalfoto aus einer aktuellen Digitalkamera hat 10 Megapixel; das entspricht einer Auflösung von 3648×2736 Bildpunkten (Breite mal Höhe).1 Jeder Bildpunkt wird, da für die drei Grundfarben Rot, Grün und Blau je acht Bit verwendet werden, mit einer Farbauflösung von 24 Bit gespeichert. Um die reinen Bilddaten ohne Metadaten zu speichern, benötigt man folglich 3648 × 2736 × 24 = 239.542.272 Bit, was umgerechnet etwa 28,5 Megabyte Speicherplatz entspricht. Dieser hohe Speicherplatzbedarf ist aber glücklicherweise reduzierbar. Es gibt komplexe Techniken, um die Speichermenge zu komprimieren. Solche Techniken suchen beispielsweise Felder mit konstanter Farbe und Intensität und speichern die Form und Position dieses Feldes und seine Farbe. Dieses Prinzip wird beispielsweise beim GIF-Format (Graphics Interchange Format) genutzt und realisiert eine verlustfreie Komprimierung. Andere Kompressionsalgorithmen nutzen Transformationen, um Ähnlichkeiten in Bildteilen zu beschreiben und weniger wichtige Details wegzulassen. Solche Kompressionsalgorithmen können verlustbehaftet ausgeführt sein, d. h. kleine Details werden ignoriert, um den Speicherbedarf zu minimieren. Ein Beispiel hierzu ist das JPEG-Verfahren, bei dem die diskrete Kosinustransformation genutzt wird, sowie der JPEG2000-Algorithmus, das auf eine Wavelet-Transformation zurückgreift. Abseits der Bildcodierung sind Codes allgemein weiter unterteilbar, wie im Folgenden gezeigt.

10.3. Ungewichtete Codes Der Binärcode wird oft auch als reiner binärer oder natürlich-binärer Code bezeichnet, oder auch als 8421-gewichteter Code. Die Zahlen 8, 4, 2 und 1 stellen hierbei die Wichtung jeder einzelnen Spalte im positionellen Code dar. Es gibt viele weitere positionelle Codes, die keine natürlich-binäre Wichtung haben. Einige dieser Codes nennt man ungewichtet, da der Wert eines einzelnen Bits nicht von seiner Position innerhalb der Zahl abhängt. Jedoch hat jeder dieser Codes spezielle Eigenschaften, die ihn für eine spezifische Anwendung geeignet machen. Ein solcher ungewichteter Code ist der Einheitsdistanzcode. Bevor wir einen Einheitsdistanzcode definieren, müssen wir die Idee der Distanz zwischen zwei binären Werten einführen. Die Hamming-Distanz zwischen zwei (binären Wörtern) ist die Anzahl der Stellen, in denen sie sich unterscheiden. Dies wird an Beispielen in Tabelle 10.2 verdeutlicht werden. Wort 1 Wort 2 Unterschiedliche Stellen Hamming-Distanz

00101101 00101100 X 1

00101101 11101100 XX X 3

00101101 11101101 XX 2

00101101 00100101 X 1

Tabelle 10.2.: Hamming-Distanz Bei einem Einheitsdistanzcode ist die Distanz aufeinanderfolgender Codewörter konstant und gleich Eins. Das heißt, zwei aufeinanderfolgende Wörter unterscheiden sich 1 eigentlich entsprechen 3648 × 2736 = 9980928, aber das ist der Industrie nah genug an 10 Megapixel, dass

sie rundet

Technische Informatik I

183

10. Codes in genau einer Position. Natürlich-binäre Zahlen sind keine Form eines Einheitsdistanzcodes: Zum Beispiel haben die binären Zahlen 0111=710 und 1000=810 eine Hammingdistanz von vier (größer geht es auch nicht mehr, d. h. die maximale Hammingdistanz zweier n -Bitwörter ist n ). Der am meisten verbreitete Einheitsdistanzcode ist der Gray-Code, dessen Belegung für vier Bit in Tabelle 10.3 dargestellt sind. Der Gray-Code wird oft mit optischen Kodierern in Verbindung gebracht. Das sind Mechanismen, welche die Winkelstellung einer Welle oder Scheibe in einen binären Wert umwandeln. Dezimal 0 1 2 3 4 5 6 7

Natürlich-Binär 0000 0001 0010 0011 0100 0101 0110 0111

Gray-Code 0000 0001 0011 0010 0110 0111 0101 0100

Dezimal 8 9 10 11 12 13 14 15

Natürlich-Binär 1000 1001 1010 1011 1100 1101 1110 1111

Gray-Code 1100 1101 1111 1110 1010 1011 1001 1000

Tabelle 10.3.: Gray-Code für vier Bit Ein optischer Kodierer erlaubt es, die Winkelstellung der Welle elektronisch zu bestimmen, ohne eine physische Verbindung zwischen der Welle und der Messausrüstung zu haben. An dem einen Ende der Welle ist eine Glas- oder Kunststoffscheibe mit einer Anzahl von konzentrischen Spuren angebracht, welche jeweils für ein Bit des binären Wortes stehen, was die Stellung der Welle darstellt. Ein Drei-Bitcode könnte zum Beispiel ausreichend sein, um eine Windrichtungsanzeige einer Wetterfahne zu realisieren. Ein Zehn-Bitcode hingegen könnte nötig sein, um die Position einer Welle in einer Maschine anzugeben.

Abbildung 10.1.: Codescheibe mit natürlichbinärem Code Jede dieser Spuren ist in eine Anzahl von Sektoren aufgeteilt, welche entweder undurchlässig oder transparent sind (Abbildung 10.1). Auf einer Seite der Scheibe gibt es je eine Punktlichtquelle pro Spur. Auf der anderen Seite der Scheibe befinden sich entspre-

184

Technische Informatik I

10.3. Ungewichtete Codes chend viele fotoelektrische Sensoren, die den einzelnen Lichtquellen gegenüber angebracht sind. Ob nun Licht von einer Lichtquelle zum Sensor gelangt, hängt von der dazwischenliegenden Codierungsscheibe ab, genauer: von dem Sektor zwischen Lichtquelle und Sensor. Für die verschiedenen Radien gibt es entsprechende Sensorergebnisse. Diese Kombination lässt sich als Binärcode (0 = Kein Licht, 1 = Licht) darstellen. Dabei ist jedem Kreisausschnitt ein Bitmuster zugeordnet. Sektor 0 1 2 3 4 5 6 7

Winkel 0–45 45–90 90–135 135–180 180–225 225–270 270–315 315–360

Binärcode 000 001 010 011 100 101 110 111

Graycode 000 001 011 010 110 111 101 100

Tabelle 10.4.: Gegenüberstellung Binärcode–Graycode Der natürliche Binärcode kann dabei unter bestimmten Umständen Probleme erzeugen, da sich mehr als ein Ausgangsbit ändert, wenn die Scheibe von Code zu Code rotiert. Da die photoelektrischen Zellen nicht perfekt ausgerichtet werden können, sind die Lichtquellen keine Punktquellen. Darüber hinaus sind die Kanten der Sektoren keine perfekten geraden Kanten, sodass sich ein Bit vor dem anderen ändern kann, wenn sich zwei Bits gleichzeitig ändern sollen. Zum Beispiel kann der Wechsel von 001 auf 010 als Sequenz von 001 → 000 → 010 angesehen werden. Da sich das letzte Bit vor dem mittleren ändert, wird für einen kurzen Augenblick der falsche Code 000 generiert. Bei einigen Anwendungen kann dies sehr problematisch sein. Aus Abbildung 10.2 ist ersichtlich, dass eine mit dem Gray-Code codierte Scheibe die Eigenschaft hat, dass sich nur ein Bit zu einer bestimmten Zeit ändert, was das Problem in dem natürlich-binären System löst (Tabelle 10.4). Nachdem der Gray-Code einmal in ein digitales System eingelesen wurde, kann er in den natürlich-binären Code umgewandelt werden, um die Daten normal weiterzuverarbeiten.

Abbildung 10.2.: Codescheibe mit Gray-Code

Technische Informatik I

185

10. Codes Gray-Codes können in ein natürlich-binäres Format umgewandelt werden (und umgekehrt), indem man Schaltungen aus XOR-Gattern wie in Abbildung 10.3 einsetzt. Die verwendete Logik ist einfach und regulär.

Abbildung 10.3.: Schaltungen für die Umwandlung von natürlich-binärem Code in GrayCode und zurück

10.4. Huffman-Codierung Nachdem mit dem Gray-Code ein ungewichteter Code vorgestellt wurde, soll nun auf gewichtete Codes eingegangen werden. Diese kurze Einführung in die Huffman-Codierung wurde in die Vorlesung mit aufgenommen, um zu zeigen, dass es nicht nur einen Weg gibt, Codierungen für Quellworte zu finden. Die Huffman-Codierung unterscheidet sich insofern von allen bisher erwähnten Kodierungstechniken, weil sie eine variable Codewortlänge haben. Die Idee hinter der Huffman-Codierung hingegen ist nicht neu. Als Samuel Morse seinen Morse-Code entwickelte, schickte er seinen Assistenten zu Drucksetzern bei Zeitungen, um die Anzahl der Druckformen für die Buchstaben A bis Z zu bestimmen. Zu dieser Zeit wurden Zeitungsseiten noch per Hand zusammengestellt und Drucksetzer verfügten über einen Schrank mit jeweils einem Fach pro Buchstaben, die unterschiedlich stark gefüllt waren. Da zum Beispiel der Buchstabe E so häufig in der englischen Sprache auftritt, war das Fach für den Buchstaben E recht voll. Gleichzeitig gab es nur wenige Q im entsprechenden Fach. So schuf Samuel Morse seinen Code, wobei häufig benutzte Buchstaben kurze Codewörter (oder besser Symbole) hatten, während selten verwendete Buchstaben durch lange Symbole dargestellt waren. Zum Beispiel hatte der Buchstaben E das Morsesymbol ·, während Q das Symbol − − ·− hatte. Eine solche Vereinbarung kann auf binäre Codes erweitert werden. Dabei muss vorausgesetzt werden, dass die Huffman-Codierung nur auf Informationen angewendet wird, bei denen manche Buchstaben oder Buchstabengruppen häufiger als andere auftreten. Normaler Text (egal ob deutsch, englisch usw.) ist so ein Fall. Der Huffman-Code wird nun an einem einfachen Beispiel demonstriert: Angenommen, es sollen möglichst wenig Bit verwendet werden, um das Wort SALATBLATT abzuspei-

186

Technische Informatik I

10.4. Huffman-Codierung chern. Wie man sieht, kommen in diesem Wort fünf verschiedene Zeichen vor. Folglich müssen (mindestens) fünf verschiedene binäre Symbole (d. h. Bitreihen) definiert werden, die jeweils einem der Buchstaben entsprechen. Mit zwei Bit kann man vier unterschiedliche Symbole darstellen, das ist zu wenig; drei Bit dagegen ermöglichen die Darstellung von acht verschiedenen Symbolen. Somit sind drei Bit notwendig und hinreichend für die Anforderung. Zurück zum SALATBLATT-Beispiel: Ordnet man jedem Buchstaben ein Symbol aus drei Bit zu, zum Beispiel S = 000, A = 001 usw., so benötigt die Symboldarstellung des zehn Zeichen langen Wortes 10 × 3 Bit Speicherplatz, also 30 Bit. Man bemerke, dass drei der möglichen Symbole nicht benötigt werden und quasi brach liegen. Dieser Überschuss wird hierbei in Kauf genommen und treibt den Speicherbedarf in die Höhe. Zudem wird nicht berücksichtigt, dass Zeichen wie das T oder das A viel häufiger vorkommen als zum Beispiel das S. Der Huffman-Code reduziert den Speicherbedarf, indem er die relative Häufigkeit der Buchstaben in dem Wort berücksichtigt. Bei genauer Betrachtung ergeben sich die in Tabelle 10.5 aufgeführten relativen Häufigkeiten. Zeichen S A L T B

relative Häufigkeit 1 10 3 10 2 10 3 10 1 10

Tabelle 10.5.: Relative Zeichenhäufigkeiten im Wort SALATBLATT Der Huffman-Code wird algorithmisch erstellt: Man zieht die beiden Elemente mit den geringsten Werten der relativen Häufigkeit zusammen zu einem neuen Zwischensymbol, welches als Wert die Summe der Elemente erhält. Zum Beispiel wird als erstes das S 1 und das B, beide mit der relativen Häufigkeit 10 , zu einem Zwischensymbol zusammen1 2 gezogen, welches als Wert 2· 10 = 10 erhält. Die Ursprungssymbole S und B werden damit ersetzt und der Algorithmus so lange wiederholt, bis nur noch ein Symbol vorhanden ist, das die Häufigkeit 10 10 = 1 haben muss. Als letztes werden nun die Kanten zwischen den Symbolen beliebig jeweils mit 0 oder 1 bezeichnet. Das Ergebnis zeigt Abbildung 10.4. Geht man nun vom Schlussknoten (Symbol) an den Kanten entlang zu den Zeichen, so erhält man die Bitfolgen, die den einzelnen Buchstaben zugeordnet werden. Das T erreicht man beispielsweise mit der Zeichenfolge 11, es ist auch eines der häufigsten Zeichen im Wort. Um dagegen zum B zu kommen, muss man die Kanten 001 durchlaufen; das Zeichen kommt aber nur einmal vor. Tabelle 10.6 zeigt alle im Beispiel zugeordneten Codewörter. Multipliziert man die Häufigkeit mit der Codewortlänge und summiert das Ergebnis aller Buchstaben auf, so erhält man den so benötigten Speicherbedarf: 1 · 3 + |{z} 3 · 2 + |{z} 2 · 2 + |{z} 3 · 2 + |{z} 1 · 3 = 22 |{z} S

A

L

T

B

Das bedeutet, dass pro Zeichen 2,2 Bit benötigt werden – gegenüber den drei Bit pro Zeichen, die der ungewichtete Code benötigt, eine echte Verbesserung. Wenn man noch

Technische Informatik I

187

10. Codes

Abbildung 10.4.: SALATBLATT als Huffman-Baum Zeichen S A L T B

relative Häufigkeit 1 10 3 10 2 10 3 10 1 10

Huffman-Codewort 000 10 01 11 001

Tabelle 10.6.: Huffman-Symbole für die Buchstaben in SALATBLATT größere Ungleichheiten im zu kodierenden Material hat, so wird die benötigte Menge an Bits pro Zeichen weiter sinken. Das Salatblatt sieht huffmancodiert so aus: 000 10 01 10 11 001 01 10 11 11. Zur besseren Lesbarkeit wurden hier Leerzeichen gesetzt, die im Computer natürlich nicht existieren. Jemand, der das so codierte Wort und die Tabelle (oder den Baum) erhält, kann es auslesen: Er startet im Baum beim obersten Knoten und geht entsprechend der Bits zu den Blattknoten hinunter. Wie man leicht nachvollziehen kann, ist ein versehentlich gekipptes Bit ein Problem: alles darauf folgende kann nicht mehr ausgelesen werden. Beim ungewichteten Code ist das anders!

10.5. Fehler erkennende Codes Wann immer digitale Signale über große Entfernungen durch ein Kabel übertragen werden, wird ihre Signalstärke schwächer, was es möglich macht, dass äußere Störsignale die digitalen Signale beeinträchtigen oder sogar vernichten. Der Effekt von elektromagnetischem Rauschen ist den meisten Menschen bekannt, da sie wissen, wenn sie Fernseher oder Radios einschalten, der Empfang von entfernteren Sendern von schlechterer Qualität als der von lokalen Sendern ist. Es handelt sich also um einen Fehler bei der Kommunikation (Informationsübermittlung) und Informationsspeicherung. Wann immer ein Fehler beim Empfang von digitalen Signalen auftritt, ist es wichtig, dass dieser erkannt werden kann, damit eine Anfrage nach erneuter Versendung der vernichteten

188

Technische Informatik I

10.5. Fehler erkennende Codes Daten gemacht werden oder der Fehler gleich vor Ort behoben werden kann (Was in militärischen Anwendungen sehr sinnvoll sein kann, wenn der Empfänger nicht lokalisiert werden will.) Fehlererkennende Codes (error detection codes = EDC) und fehlerkorrigierende Codes (error correction codes = ECC) werden auch in der Datensicherungstechnologie benötigt. Viele der Techniken, um digitale Daten zu sichern, sind sehr anfällig für Fehler (obwohl die Fehlerwahrscheinlichkeit sehr klein ist). Folglich werden EDCs und ECCs häufig zur Entscheidung verwendet, ob Daten während des Prozesses der Sicherung und Rückholung vernichtet wurden. Die Korrektur-Algorithmen sind sehr rechenaufwendig, sodass heute vorzugsweise spezielle integrierte Schaltkreise eingesetzt werden. Anwendungsgebiete sind: • stark gestörte Übertragungswege, auf denen die effektive Übertragungsrate durch zu hohe Blockwiederholraten zu sehr gesenkt würde, • militärische Datenübertragungen auf Funkwegen, bei denen sich ein Quittungsund Wiederholdialog aus Gründen der Peilung verbietet, • Arbeitsspeicher und Plattenspeicher in Rechenanlagen, um Lesefehler zunächst ohne Wiederholvorgang zu beheben. Eine weit verbreitete Methode zur Fehlererkennung ist die Implementierung, bei der man die Übertragung der gewünschten digitalen Informationen (d. h. dem Quellwort) um ein oder mehrere Prüfbits erweitert, die sich aus einer Funktion über den Informationsbits errechnen. Da Prüfbits keine zusätzliche Information beinhalten, nennt man sie redundante (überflüssige) Bits. Am Empfänger-Ende einer Datenverbindung werden die Informationsbits verwendet, um die Prüfbits lokal zu berechnen. Wenn die empfangenen Prüfbits gleich denen der lokal generierten sind, kann die Übertragung als fehlerfrei angesehen werden, ansonsten muss der Empfänger dem Sender eine Nachricht zukommen lassen, in der er um wiederholte Zusendung der Daten bittet. Wenn der Fehler in einem gespeicherten Wort auftritt, kann es nicht korrigiert werden, in dem der Rechner gefragt wird, was es war, da es keine weitere Kopie des Wortes gibt. Folglich muss das Betriebssystem über den Fehler informiert werden, um anschließend entsprechende Aktionen einzuleiten (üblicherweise die momentane Aufgabe abbrechen). Glücklicherweise setzen einige Speicher fehlerkorrigierende Codes ein und sind somit in der Lage, den durch den Fehler entstandenen Schaden zu beheben, bevor das Datenwort zum Rechner übertragen wird. Als Beispiel für eine Anwendung von Prüfbits denke man sich eine einfache zweistellige Dezimalzahl, mit einer einzelnen dezimalen Prüfziffer. Diese Prüfziffer errechnet sich aus der Summe der beiden Quellziffern modulo zehn (was hierbei einfach nur bedeutet, dass wir einen eventuellen Übertrag bei der Addition der Ziffern ignorieren – der „modulo zehn“-Wert von 6 + 7 = 3). Wen die beiden Quellziffern 4 und 9 sind, wäre das Codewort 493 (3 ist die Prüfziffer). Nehmen wir an, dass bei der Übertragung das Codewort verstümmelt und zu 463 wurde. Wenn wir die Prüfziffer nun erneut ausrechnen, erhalten wir 4+6 = 10 und 10 mod 10 = 0. Da die empfangene Prüfziffer aber 3 ist, wissen wir, dass ein Fehler aufgetreten sein muss.

10.5.1. Arbeitsweise Fehler erkennender Codes Die Idee hinter EDCs ist, dass ein m-Bit-Quellcode 2m einzigartige Nachrichten übermitteln kann. Wenn r Prüfbits zu den m Nachrichtenziffern hinzuaddiert werden, um ein

Technische Informatik I

189

10. Codes n -Bit-Codewort zu schaffen, dann gibt es 2n = 2m+r mögliche Codewörter. Von diesen Codewörtern sind nur 2m gültig. Sollte ein Codewort empfangen werden, welches nicht eins dieser 2m Werte ist, so ist ein Fehler aufgetreten.

Nachdem wir die Hamming-Distanz bereits erklärt haben, soll nun eine genaue Definition folgen. Seien x und y Codewörter im R n2 (Die Menge aller n -Bit-Codewörter, wobei ein Bit aus {0,1} ist). Dann heißt die Funktion d (x, y) := Anzahl der Bitstellen, an denen sich x und y unterscheiden

Hamming-Distanz von x und y . Die Hamming-Distanz des gesamten Codes (Menge aller Codewörter) ist gleich der minimalen Hamming-Distanz zwischen zwei Codewörtern dieses Codes. Seien x und y Elemente der Menge O , und d : O × O → R eine Funktion auf O . das heißt Metrik, wenn für alle x, y, z ∈ O gilt: • d (x, y) = 0 genau dann, wenn x = y • d (x, y) = d (y, x) • d (x, y) ≤ d (x, z) + d (z, y) (Dreiecksungleichung) → Die Hamming-Distanz ist also eine Metrik auf R n2 .

Abbildung 10.5.: Ein Fehler erkennender Code mit zwei Nutzbits Abbildung 10.5 zeigt einen fehlererkennenden Code mit n = 3. Jedes der 23 = 8 möglichen Codewörter wird durch eine Ecke des dreidimensionalen Würfels dargestellt. Von diesen acht möglichen Codewörtern wurden nun vier als gültig markiert (schwarz eingefärbt: 000, 011, 101, 110). Wenn wir ein Codewort aus diesem Raum aussuchen, können wir sofort entscheiden, ob es ein gültiges Wort ist oder nicht. Es ist gültig, wenn es eine gerade Anzahl von Einsen enthält. Kein benachbartes Wort (Hamming-Distanz = 1) eines Codewortes ist wieder ein Codewort, weil die Hamming-Distanz zwischen zwei Codewörtern 2 ist. Ein 1-Bit-Fehler resultiert in einem unkorrekten Codewort und kann erkannt werden. In Abbildung 10.6 haben wir einen anderen dreidimensionalen Würfel gezeichnet, allerdings wird in diesem Fall nur ein Bit mit Nutzinformation genutzt (m = 1 und r = 2). Hier gibt es nur zwei gültige Codeworte, 000 und 111. Man beachte, dass jedes gültige Codewort vom anderen durch eine Hamming-Distanz von drei getrennt ist. Wenn ein ungültiges Codewort erkannt wurde, können wir versuchen, den Fehler zu korrigieren, indem wir das zum fehlerhaften Wort nächstgelegene gültige Codewort nehmen. Wenn also das

190

Technische Informatik I

10.5. Fehler erkennende Codes

Abbildung 10.6.: Ein Fehler erkennender Code mit einem Nutzbit ungültige empfangene Codewort 001 wäre, würden wir annehmen, dass der korrekte Code 000 wäre (da 001 dichter an 000 als an 111 liegt). Alle fehlererkennenden Codes arbeiteten nach diesem Prinzip, bei dem gültige Codewörter durch eine Hamming-Distanz von mindestens drei getrennt sind. Folglich können wir eine Strategie zur Fehlerkorrektur entwickeln: Wenn wir ein ungültiges Codewort entdecken, korrigieren wir den Fehler, indem wir das fehlerhafte Wort durch das nächstgelegene gültige Wort ersetzen. Da gültige Codewörter voneinander durch mindestens drei Einheiten voneinander getrennt sind, bewegt ein einzelner Fehler ein Codewort um eine Einheit von seinem korrekten Wert weg, aber es bleibt so mindestens zwei Einheiten zu anderen korrekten Wörtern entfernt. Man sieht leicht den erhöhten Aufwand (d. h. erhöhte Redundanz), denn nur noch zwei der insgesamt acht möglichen Wörter sind wirklich Codewörter, es werden also drei Bits verbraucht, wo nur eins notwendig wäre. Sei C ⊆ R n2 ein Code. C heißt auf p-Bit-Fehler prüfbar (erkennbar), wenn für jedes x ∈ C gilt: K (x, p) ∩C = {x}, wobei K (x, p) := x|d (y, x) = p C heißt auf p-Bit-Fehler korrigierbar, wenn für alle x, y ∈ C gilt: K (x, p) ∩ K (y, p) = {} → C ist genau dann auf p-Bit-Fehler prüfbar, wenn für alle x, y ∈ C gilt: d (x, y) = p + 1 → C ist genau dann auf p-Bit-Fehler korrigierbar, wenn für alle x, y ∈ C gilt: d (x, y) = 2p + 1

Wollen wir nun einen konkreten Code definieren, müssen wir angeben, wie die Kontrollbits bestimmt werden.

10.5.2. Fehlererkennung durch Parität Einer der einfachsten Fehler erkennenden Codes prüft, ob in einem binären Wort fester Länge die Anzahl der Einsen gerade oder ungerade ist, und fügt ein Paritätsbit genanntes Bit mit dieser Information an: Eine 1, wenn die Anzahl der Einsen ungerade ist, oder eine 0, wenn die Einzenmenge gerade ist. Hierbei spricht man übrigens von einer geraden Parität, da das ursprüngliche Wort zusammen mit dem Paritätsbit immer eine gerade Anzahl an Einsen enthält. Ebenso ist aber auch eine ungerade Parität möglich. Dabei

Technische Informatik I

191

10. Codes wird eine 0 angefügt, wenn die Anzahl der Einsen ungerade ist, oder eine 1, wenn die Einzenmenge gerade ist. Ein Beispiel dazu wird in Tabelle 10.7 dargestellt. An welcher Position das Paritätsbit eingefügt wird, ist unerheblich für die Fehlererkennung; es muss jedoch beim Codieren und Dekodieren an gleicher Stelle verwendet werden. Quellwort 000 001 010 011 100 101 110 111

Gerade Parität 0000 0011 0101 0110 1001 1010 1100 1111

Ungerade Parität 0001 0010 0100 0111 1000 1011 1101 1110

Tabelle 10.7.: Paritätscodes mit drei Nutzbits (Paritätsbit rechts angefügt) Nehmen wir an, dass das Codewort 0101101 versendet und verstümmelt als 00101100 empfangen wird, weil sich das letzte Bit von 1 auf 0 geändert hat. Die Parität des Wortes, die auf der Empfängerseite berechnet wurde, ist ungerade (es gibt drei Einsen); da gerade Parität ausgemacht war, ist anscheinend ein Fehler aufgetreten. Wir können aus dem empfangenen Wort nicht schließen, in welchem Bit der Fehler auftrat und können ihn somit auch nicht korrigieren. Man beachte, dass, wenn es zwei Fehler gegeben hätte, keine Paritätsverletzung erkannt worden wäre und die Fehler somit unbemerkt geblieben wären. Einzelne Paritätsprüfbits sind folglich nur zweckmäßig, wenn die Fehler relativ selten und vereinzelt auftreten. Wie beschrieben können mit dem Paritätsbit nur Fehler erkannt, aber nicht korrigiert werden. Um diesen Ansatz um Fehlerkorrektur zu erweitern, geht man in die zweite Dimension: Bei den so genannten Block-EDCs (Matrix-EDCs) wird jede Spalte (also jede Bitposition) um ein Paritätsbit ergänzt, zusätzlich zu dem Paritätsbit des Wortes. In diesem Zustand hätte man mehr Paritätsbits als Nutzbits. Um die ganze Sache etwas effizienter zu machen, wird die Spaltenparität über mehrere Worte gebildet. Tabelle 10.8 zeigt ein Beispiel mit sechs Wörtern, die um Paritätsbits ergänzt werden sollen.

Wort 1 Wort 2 Wort 3 Wort 4 Wort 5 Wort 6

D2

D1

D0

1 1 0 1 1 0

1 0 0 1 0 1

0 1 1 0 1 0

Tabelle 10.8.: Sechs Quellwörter, die als Eingabe für das Paritätsbeispiel dienen Nun werden zunächst die einzelnen Wörter um Paritätsbits ergänzt (Tabelle 10.9). Danach werden die Spalten um Paritätsbits ergänzt, auch die Paritätsspalte des ersten Schritts (Tabelle 10.10).

192

Technische Informatik I

10.5. Fehler erkennende Codes

Wort 1 Wort 2 Wort 3 Wort 4 Wort 5 Wort 6

D2

D1

D0

1 1 0 1 1 0

1 0 0 1 0 1

0 1 1 0 1 0

Parität 0 0 1 0 0 1

Tabelle 10.9.: Die sechs Quellwörter, die mit Paritäten ergänzt wurden.

Wort 1 Wort 2 Wort 3 Wort 4 Wort 5 Wort 6 Parität

D2

D1

D0

1 1 0 1 1 0 0

1 0 0 1 0 1 1

0 1 1 0 1 0 1

Parität 0 0 1 0 0 1 0

Tabelle 10.10.: Vertikale Paritäten an den sechs Beispielquellwörtern Wenn nun ein einzelne Fehler in einem bestimmten Wort auftritt, können wir über das horizontale Paritätsbit erkennen, welches Wort fehlerhaft ist. Weiterhin sagt uns das vertikale Paritätsbit, welches Bit im Wort fehlerhaft ist (d. h. ein Fehler in D 0 oder D 1 etc.). Nun, da wir die Spalte und die Zeile kennen, in welcher der Fehler auftritt, können wir die exakte Position des Fehlers bestimmen (Tabelle 10.11). Und da wir wissen, welches Bit verstümmelt wurde, können wir es kippen und somit den Fehler korrigieren.

Wort 1 Wort 2 Wort 3 Wort 4 Wort 5 Wort 6 Parität Prüfung

D2

D1

D0

1 1 0 1 1 0 0

1 0 1 1 0 1 1

0 1 1 0 1 0 1

p

×

p

Parität 0 0 1 0 0 1 0

Prüfung p p × p p p p

p

Tabelle 10.11.: Fehlererkennung im Beispiel: Ein Haken zeigt, dass gerade Parität vorgefunden wurde, ein Kreuz zeigt die ungerade Parität (ein Fehler) Der Redundanzaufwand bei diesem Verfahren ist sehr hoch: Ein Block von m Worten der Länge n benötigt m + n + 1 Paritätsbits. In unserem Beispiel brauchten unsere sechs Wörter der Länge drei insgesamt zehn Paritätsbits, d. h. 10 von 28 Bits waren redundant und somit keine Nutzinformation. Der Paritätsblockcode kann einzelne Fehler erkennen und korrigieren und er kann bestimme Kombinationen von Fehlern erkennen – diese dann aber nicht korrigieren. Sol-

Technische Informatik I

193

10. Codes che fehlererkennenden und -korrigierenden Codes findet man in Datenübertragungssystemen oder -speicherungssystemen. Das Fehlerkorrekturvermögen eines Fehler korrigierenden Codes lässt sich aus der Hamming-Distanz ableiten. Wie viele Prüfbits wir für einen einfachen fehlerkorrigierenden Code benötigen, ist abhängig von der Länge des Quellwortes. Stellen wir uns vor, dass wir einen Code entwickelt wollen, der es erlaubt, alle Ein-Bit-Fehler zu korrigieren. Der Code habe m Nutzbits und r Prüfbits; jedes Wort daraus hat die Länge n = m + r . Jedem Quellwort der Länge m ist ein Codewort der Länge n zugeordnet. Zu jedem Codewort existieren n Wörter gleicher Länge, die keine Codewörter sind und eine Hamming-Distanz von 1 zum Codewort haben. Diese entstehen, wenn man systematisch jedes der n Bits im n -Bit-Codewort invertiert. Das bedeutet, dass jedes der 2m Quellwörter n + 1 ihm zugehörige Bitmuster erfordert (n mögliche falsche Bitmuster und das korrekte Codewort). Da die gesamte Anzahl der Bitmuster 2n ist, muss gelten: (n + 1)2m ≤ 2n . Setzt man auf der rechten Seite n = m + r ein, erhält man (m + r + 1)2m ≤ 2m+r , d. h. (m + r + 1)2m ≤ 2m · 2r und vereinfacht m + r + 1 ≤ 2r bzw. 0 ≤ 2r − r − m − 1. Gelingt es, Werte derart zu finden, dass die Ungleichung der Gleichung entspricht (also 0 = 2r − r − m − 1), so spricht man von einem dicht gepackten Code. Dieser hat die Eigenschaft, dass jedes erzeugbare Wort eindeutig auf ein und nur ein Codewort zurückführbar ist. Tabelle 10.12 zeigt die minimale Anzahl der nötigen Prüfbits für verschiedene Wortgrößen. Deutlich sieht man, dass die Größe der Redundanz mit steigender Wortlänge immer kleiner wird. Quellwortlänge m 8 16 32 64 128 256 512

Prüfbitanzahl r 4 5 6 7 8 9 10

Gesamtlänge n 12 21 38 71 136 265 522

Redundanz in % 50 31 19 11 6 4 2

Tabelle 10.12.: Redundanzaufwand bei unterschiedlichen Wortlängen

10.5.3. Zyklische Redundanzcodes oder CRC Obwohl die Parität ein für die Praxis ausreichendes Verfahren sein mag, ist eine andere Methode zur Fehlererkennung viel stärker verbreitet: Polynominalcodes, auch bekannt als zyklische Redundanzcodes (cyclic redundancy codes = CRC). Polynominalcodes basieren auf der Behandlung von Bitfolgen als binäre Polynome, die allein mit den Koeffizienten 0 und 1 arbeiten. Ein k Bit langes Wort wird also als die Koeffizientenliste eines Polynoms mit k Summanden, von x k−1 bis hin zu x 0 , behandelt. Solch ein Polynom hat den Grad k − 1. Das höchstwertige Bit des Worts ist der Koeffizient von x k−1 , das nächste Bit ist der Koeffizient von x k−2 und so weiter. Polynominalarithmetik wird modulo zwei durchgeführt, den Regeln für algebraische Körper entsprechend. Es gibt keinen Übertrag bei der Addition oder der Subtraktion.

194

Technische Informatik I

10.5. Fehler erkennende Codes Beide Operation (Addition und Subtraktion) sind dabei identisch zum exklusiven Oder (XOR). Zum Beispiel: 10011011 00110011 11110000 01010101 +11001010 +11001101 -10100110 -10101111 01010001 11111110 01010110 11111010 Die Division auf solchen Polynomen wird genauso ausgeführt wie bei den binären Entsprechungen (siehe Division im Kapitel 9). Man sagt, dass der Divisor in den Dividenden übergeht, wenn der Dividend genauso viele Bits wie der Divisor hat. Wenn der Polynominalcode verwendet wird, müssen sich Sender und Empfänger vorher auf einen Generatorpolynom G(x) einigen. Bei diesem müssen sowohl das höchstwertige als auch das niederwertigste Bit 1 sein. Um eine Prüfsumme für ein m -Bit-Quellwort zu berechnen (welches zu dem Polynom M (x) gehört), muss das Wort länger als das Generatorpolynom sein (m > g ). Die Idee ist, eine Prüfsumme in der Weise an das Quellwort anzuhängen, dass das dadurch erzeugte Bitmuster (d. h. das dazugehörige Polynom) durch G(x) teilbar ist. Wenn der Empfänger das neue Bitmuster erhält, versucht er, es durch G(x) zu teilen. Wenn es dabei einen Rest ungleich 0 gibt, trat ein Übertragungsfehler auf; ist der Rest dagegen 0, verlief die Übertragung mit hoher Wahrscheinlichkeit fehlerfrei. Der Algorithmus für die Berechnung der Prüfsumme ist wie folgt: 1. Sei r der Grad von G(x). Hänge r Nullbits an das Bitmuster des Quellwortes an, sodass es nun m + r Bits lang ist und zu dem Polynom x r M (x) gehört. Damit ist sichergestellt, dass das Wort nun länger als G(x) ist (andernfalls wäre eine Division nicht möglich). 2. Teile das Wort, welches x r M (x) entspricht, per modulo-2-Division durch das Bitmuster von G(x) 3. Subtrahiere den Rest (welcher immer höchstens r bit lang ist) von dem Bitmuster, welches x r M (x) entspricht mittels modulo-2-Subtraktion. Das Ergebnis ist die zu übertragende, mit einer Prüfsumme versehende Bitfolge T (x) Abbildung 10.7 illustriert das Verfahren für ein Wort 1101011011 und den Generator 10011 (G(x) = x 4 + x + 1). Es sollte klar sein, dass T (x) durch G(x) teilbar ist (modulo 2). Bei jeder Division ist zu fordern, dass der „Dividend minus Rest“ durch den Divisor teilbar ist. Ein Beispiel mit Zahlen zur Basis 10: Der ganzzahlige Rest der Division von 210278 durch 10941 ist 2399. Subtrahiert man von 210278 diesen Rest 2399, so ist der Ergebnis (207879) durch 10941 teilbar. Betrachten wir nun die Mächtigkeit dieser Methode. Welche Arten von Fehlern werden erkannt? Stellen wir uns vor, dass ein Übertragungsfehler auftritt, sodass statt der Bitfolge für T (x) die Bitfolge für T (x) + E (x) eintrifft. Jedes Bit in E (x), welches 1 ist, gehört zu einem Bit, dass sich umgedreht hat. Wenn es k Bits mit dem Wert 1 in E (x) gibt, dann sind k Einzelbitfehler aufgetreten. Man spricht von einem Impulsfehler, wenn er mit einer 1 beginnt, gefolgt von einem Mix von 0 und 1, und mit einer letzten 1 endet, wobei die verbleibenden Bits alle 0 sind. Der Empfänger erhält nun die mit der Prüfsumme versehende Bitfolge und teilt sie durch G(x), d. h. er berechnet T (x) + E (x) G(x)

Technische Informatik I

195

10. Codes

Abbildung 10.7.: CRC-Berechnungsbeispiel T (x)/G(x) ist gleich Null, sodass das Ergebnis der Berechnung einfach E (x)/G(x) ist. Die Fehler, die zu Polynomen gehören, welche G(x) als Faktor beinhalten, gehen dabei durch, alle anderen Fehler werden erkannt. Wenn es einen Einbitfehler gab, ist E (x) = x i , wobei i bestimmt, welches Bit fehlerhaft ist. Wenn G(x) zwei oder mehr Terme beinhaltet, wird es niemals E (x) teilen können. Somit werden alle Einbitfehler erkannt. Wenn es zwei getrennte Einbitfehler gab, ist E (x) = x i + x j , wobei i > j . Alternativ kann man dies als E (x) = x j (x i − j + 1) schreiben. Wenn wir annehmen, dass G(x) nicht durch x teilbar ist, ist es eine hinreichende Bedingung für die Erkennung aller Doppelfehler, dass x k + 1 nicht durch G(x) teilbar ist für jedes k ≤ i − j (d. h. bis zur maximalen Wortlänge). Es sind Polynome mit kleinem Grad bekannt, die Sicherheit für große Wortlängen geben. Zum Beispiel wird x 15 + x 14 + 1 kein x k + 1 bis zu einem Wert k = 32768 teilen. Wenn es eine ungerade Anzahl von fehlerhaften Bits gibt, besteht E (x) aus einer ungeraden Anzahl von Summanden (z. B. x 5 + x 2 +1, aber nicht x 2 +1). Interessant ist dabei, dass es kein Polynom mit einer ungerade Anzahl von Summanden gibt, welche x +1 als Faktor im modulo-2-System haben. Indem wir x + 1 zu einem Faktor von G(x) machen, können wir alle Fehler abfangen, die aus einer ungeraden Zahl von verdrehten Bits bestehen. Um zu verstehen, dass kein Polynom mit einer ungeraden Anzahl von Summanden durch x + 1 teilbar ist, nehmen wir vorerst an, dass dem so wäre, alsodass E (x) eine ungerade Anzahl von Summanden hat und durch x + 1 teilbar ist. Zerlegen wir E (x) in die Faktoren (x + 1) · Q(x). Nun berechnen wir E (1) = (1 + 1) · Q(1). Da 1 + 1 = 0 (modulo 2) ist, muss E (1) immer Null sein. Wenn E (x) eine ungerade Anzahl von Summanden hat, wird die Substitution der Variablen x durch 1 immer auch 1 als Ergebnis liefern. Dies ist offensichtlich ein Widerspruch. Also kann es kein Polynom mit ungerader Summandenzahl geben, welches durch x + 1 teilbar ist. Die wichtigste Eigenschaft ist aber, dass ein Polynominalcode mit r Prüfbits alle Impulsfehler der Länge kleiner/gleich r erkennen wird. Ein Impulsfehler der Länge k kann ¡ k−1 ¢ i durch x x + 1 . . . + 1 dargestellt werden, wobei i bestimmt, wie weit der Impuls vom

196

Technische Informatik I

10.6. Fehlerkorrektur durch Hamming-Codierung hinteren Ende des Wortes entfernt ist. Wenn G(x) den Summanden x 0 enthält, wird es nicht x i als Faktor haben können. Der Rest kann also, wenn der Grad des geklammerten Ausdrucks kleiner als der Grad von G(x) ist, nie Null sein. Wenn die Länge des Impulses r +1 ist, wird der Rest der Division durch G(x) genau dann Null sein, wenn der Impuls identisch zu G(x) ist. Laut der Definition des Impulses muss das erste und das letzte Bit jeweils 1 sein, also ist die Identität mit G(x) abhängig von den r − 1 mittleren Bits. Wenn alle Kombination als gleichverteilt angesehen werden, ist die Wahrscheinlichkeit, dass ein solches Bitmuster akzeptiert wird (d. h. durch den CRC nicht als Fehler erkannt wird), gleich 2r1−1 . Ebenso kann man zeigen: Die Wahrscheinlichkeit eines nicht als fehlerhaft erkannten Bitmusters 21r ist, wenn ein Fehlerimpuls der Länge r + 1 auftritt oder mehrere kürzere Impulse auftreten (wobei man annimmt, dass alle Kombinationen gleichverteilt sind). Drei Polynome haben sich u.a. als internationale Standards etabliert. Alle drei beinhalten x+1 als einen Primfaktor. CRC-12 wird benutzt, wenn die Zeichenlänge 6 Bits ist (d. h. der entsprechende Code benötigt 6 Bits zur Codierung eines Zeichens). Die anderen beiden sind für 8-Bit-Zeichen. Eine 16-Bit-Prüfsumme wie bei CRC-16 oder CRC-CCITT erkennt alle Einzel- und Doppelfehler, alle Fehler mit einer ungeraden Anzahl von Bits, alle Impulsfehler mit einer Impulslänge von bis zu 16 Bits, 99,997% aller 17-Bit-Impulse und 99,998% aller Impulse von 18 Bits oder mehr. 1961 wurde gezeigt, dass sich Prüfsummen unter Einsatz eines einfachen Schieberegisterschaltkreises berechnen lassen. Jahrzehntelang nahm man an, dass mit Prüfsummen versehende Bitmuster zufällige Bits enthalten. Alle Analysen von Prüfsummenalgorithmen wurden basierend auf dieser Annahme durchgeführt. Eine erst vor kurzem erfolgte Inspektion von realen Daten hat gezeigt, dass die Annahme gänzlich falsch ist. Daraus folgt, dass unter bestimmten Umständen unerkannte Fehler gewöhnlicher sind, als man bisher dachte.

10.6. Fehlerkorrektur durch Hamming-Codierung Die bei der Parität in Abschnitt 10.5.2 erwähnte theoretische untere Grenze für die Anzahl von Prüfbits kann durch eine Methode erreicht werden, die auf Richard Hamming (1950) zurückgeht. Wir erinnern uns, dass die Hamming-Distanz zwischen zwei gültigen Codewörtern mindestens drei ist, damit man die Metrikeigenschaften ausnutzen kann. Eine in nur einem Bit gestörte Übertragung erzeugt dann aus einem Codewort x ein fehlerhaftes Wort w , das sich in nur einer Stelle von x unterscheidet, also die Hamming-Distanz d (x, w) = 1 hat, während der Abstand zu allen anderen Codewörtern mindestens zwei beträgt, d. h. d (y, w) ≥ 2. Die Korrektur erfolgt nun so, dass w in das – unter den gemachten Voraussetzungen eindeutig bestimmte – Codewort x mit überführt wird. Für ein 4-Bit-Wort sieht das Hamming-Verfahren (Hamming-Codierung) demnach drei Kontrollbits vor (m = 4, r = 3). Die Nutzinformation (in Beispiel sei sie 1011) wird in dem Verfahren um Kontrollinformation ergänzt ergeben. Die Prüfbits stehen dabei an den Stellen j ( j ist eine Zweierpotenz, also 1, 2, 4, 8 usw.) und bilden die Parität über die Bits, welche an Positionen k (das j -te Bit von k in Binärdarstellung ist gleich 1) stehen. Man könnte auch anders herum sagen, dass das Bit b von den Bits b1 , b2 , . . . , bi geprüft wird,

Technische Informatik I

197

10. Codes sodass b = b1 + b2 + . . . + bi . Diese Parität ergibt sich aus einem schrittweise ausgeführtem XOR über diese Bits. Die Prüfbits errechnen sich also wie folgt:

r0

= m 3 ⊕ − − ⊕m 1 ⊕ m 0

r1

= m 3 ⊕ m 2 ⊕ − − ⊕m 0

r2

= m 3 ⊕ m 2 ⊕ m 1 ⊕ −−

r x entspricht dadurch dem geraden Paritätsbit zu den drei berücksichtigten Quellwort-

stellen. Im Beispiel mit m3 m2 m1 m0 = 1011:

r0 =

1 ⊕ − − ⊕1 ⊕ 1 = 1

r1 =

1 ⊕ 0 ⊕ − − ⊕1 = 0

r2 =

1 ⊕ 0 ⊕ 1 ⊕ −− = 0

Da die Prüfbits sich an den Stellen j = 2x (mit x als Index des Prüfbits) befinden, wird das Codewort also nach der Vorschrift m3 m2 m1 r 2 m0 r 1 r 0 gebildet. Diese Vorschrift ist bei der Codierung und Dekodierung notwendig, um festzustellen, welches die eigentlichen Quellwortbits sind und in welcher Reihenfolge sie angeordnet sind. Es ergibt sich das Codewort c = 1010101. Dieses kann zum Beispiel versendet werden. Auf der Empfängerseite werden die Prüfbits über die Nutzbits neu errechnet; es werden dabei also nur die ersten drei und das fünfte Bit berücksichtigt. Ergibt sich dabei ein Unterschied an den Prüfbitstellen, ist ein Fehler aufgetreten. Unter der Annahme, dass maximal ein Fehler aufgetreten ist, kann man aus diesem Unterschied die Stelle des gekippten Bits errechnen und den Fehler so korrigieren. Dabei ist es unerheblich, ob ein Nutzbit oder ein Prüfbit gekippt ist. Nehmen wir an, dass statt unserem korrekten Codewort das Wort 1000101 empfangen wurde, dass also das Nutzbit m1 gestört wurde. Es ergibt sich bei der Neubildung der Prüfbits folglich das Bitmuster r 2 r 1 r 0 = 100 anstatt 001. Um den Fehler zu lokalisieren, führt man ein bitweises XOR dieser Muster durch. Es ergibt sich 101. Dies entspricht der binären 5 – und genau die fünfte Stelle von rechts ist im Wort m3 m2 m1 r 2 m0 r 1 r 0 gekippt worden (m1 ). Kippt man dieses Bit zurück auf 1, so ergibt sich das ursprüngliche Codewort, aus dem nun die eigentlichen Nutzbits, nämlich 1011, ausgelesen werden können. Es sei darauf hingewiesen, dass der hier beobachte Redundanzaufwand von 75% (3 Prüfbits für 4 Nutzbits) deutlich niedriger wird, wenn größere Codewortlängen vorliegen. So ist er bei 16 + 5 Bits bzw. 64 + 7 Bits nur noch 32% bzw. 11%. Betrachten wir zum Verständnis noch ein zweites Beispiel mit 16 Nutzbits und 5 Prüfbits. Die Bits 1, 2, 4 8 und 16 sind hierin Prüf- oder Paritätsbits, der Rest die Nutzbits. Ingesamt ist das Wort also 21 Bits lang. Wir werden im Beispiel durchgängig gerade Parität verwenden. Jedes Paritätsbit prüft bestimmte Bits, das Paritätsbit selber wird dann so gesetzt, dass die Anzahl der Einsen in den geprüften Positionen gerade ist. Tabelle 10.13 gibt an, welche Prüfbits in diesem Beispiel welche Bits prüfen.

198

Technische Informatik I

10.6. Fehlerkorrektur durch Hamming-Codierung Bit 0

2 =1 21 = 2 22 = 4 23 = 8 24 = 16

. . . kontrolliert die Bits

1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21 2, 3, 6, 7, 10, 11, 14, 15, 18, 19 4, 5, 6, 7, 12, 13, 14, 15, 20, 21 8, 9, 10, 11, 12, 13, 14, 15 16, 17, 18, 19, 20, 21

Tabelle 10.13.: Prüfbits an Binärpositionen bis 21 Auch hier kann wieder allgemein gesagt werden, dass das Bit b von den Bits b1 , b2 , . . . , bi geprüft wird, sodass b = b1 + b2 + . . . + bi . Zum Beispiel wird das Bit 5 durch die Bits 1 und 4 geprüft, da 1 + 4 = 5 ist. Man beachte, dass die Kontrollbits sich immer selber prüfen: 1 = 1, 2 = 2, usw. Das Wort und die Prüfbits sind in Abbildung 10.8 dargestellt.

Abbildung 10.8.: Beispiel zur Hamming-Codierung mit 21 Stellen, darin 5 Prüfbits Nehmen wir nun an, dass Bit 5 umgedreht wurde: 001001100000101101110. Wir können der Tabelle 10.13 entnehmen, dass das fünfte Bit für die Prüfbits 1 und 4 relevant ist. Auf Empfängerseite wird man folglich feststellen, dass diese beiden Bits nicht korrekt sind. Die Schnittmenge von geprüften Nutzbits dieser beiden Prüfbits besteht jedoch nicht nur aus der 5, sondern ebenfalls aus 7, 13, 15 und 21. Letztere sind jedoch ebenfalls unter Kontrolle anderer Prüfbits, z. B. prüft Bit 2 ebenfalls die 7 und 15, wird aber korrekt berechnet. Geht man alle Möglichkeiten durch, ergibt sich, dass der Fehler nur an Position 5 aufgetreten sein kann. Wird das Bit dort gekippt, ergibt sich ein fehlerfreies Codewort. Eine einfachere Art, den Fehler zu lokalisieren, ist die bereits besprochene Art, die Paritätsbits neu zu berechnen, um anschließend die Addition modulo zwei mit den empfangenen Paritätsbits durchzuführen. Die Summe gibt, als (binäre) Zahl gelesen, die Position des Fehlers an.

10.6.1. Weitere Eigenschaften von Hamming-Codes • Die komponentenweise gebildete Summe (modulo zwei) zweier Codewörter ist wieder ein Codewort. Damit kann man den der Code als einen Vektorraum über einem Körper mit zwei Elementen auffassen. Dafür gibt es mit der „Linearen Codetheorie“ eine mathematische Theorie. • Die Kugeln mit Radius n um Codewörter sind nicht nur disjunkt, sondern enthalten alle möglichen Wörter der Länge n . → Jedes Wort der Länge n kann decodiert werden. Ein Code mit dieser Eigenschaft heißt perfekt. • Geht man von x 1 x 2 . . . x n über zu x 2 . . . x n x 1 (Rotation), erhält man wieder ein Codewort. Ein Code mit dieser Eigenschaft heißt zyklisch.

Technische Informatik I

199

Teil III.

Rechnerarchitektur II

201

11. Implementierung der Kontrolleinheit Der genaue Vorgang, mit dem ein Digitalrechner einen Maschinencodebefehl interpretiert, ist recht komplex. Wir werden hier einen Überblick über die Arbeit der Steuereinheit schaffen und anschließend demonstrieren, wie sie es der CPU ermöglicht, Befehle im Befehlsregister auszuführen. Das heißt, wir werden zeigen, wie ein Bitmuster im Anweisungsregister benutzt wird, um eine Folge von Aktionen zu generieren. Diese Aktionen finden dann während der Ausführung des Befehls statt. Für die Entwicklung einer Steuereinheit gibt es zwei grundsätzlich voneinander verschiedene Ansätze. Der erste besteht darin, aus der Steuereinheit selbst einen Rechner im Rechner zu machen und so jede Maschinenanweisung in eine Folge von noch einfacheren Mikroanweisungen umzuwandeln. Der alternative Ansatz ist, sich zu fragen, welche Folge von logischen und arithmetischen Operationen benötigt wird, um eine Anweisung auszuführen und anschließend zur Realisierung dieser Operationen eine entsprechende Logikschaltung zu entwerfen.

11.1. Mikroprogrammierbare Steuereinheiten Bevor wir uns die Steuereinheit näher ansehen, klären wir einige Begriffe: Befehle, Mikrobefehle und Interpretation. Die natürliche Sprache (die „Muttersprache“) eines Rechners ist seine Maschinensprache, deren symbolische Darstellung auch Assemblersprache genannt wird. Assemblerbefehle heißen Befehle. Jeder Befehl wird durch eine Anzahl einfacher Mikrobefehle dargestellt. In anderen Worten: Es gibt eine Sprache, die noch primitiver als die Maschinensprache selbst ist. Um die bisher betrachteten Befehle besser von den Mikrobefehlen zu unterscheiden, werden wir statt Befehl auch Makrobefehl verwenden. Ein Mikrobefehl ist das kleinste Ereignis, dass in einem Rechner stattfinden kann und kann schon daraus bestehen, ein Flip-Flop zu takten oder Daten von einem Register zum nächsten zu bewegen. Der Prozess, bei dem ein Makrobefehl durch eine Serie von Mikrobefehlen ausgeführt wird, nennt sich Interpretation. Das CPU-Modell, das wir benutzen werden, um die Arbeit der Steuereinheit zu verdeutlichen, ist in Abbildung 11.1 dargestellt. Die CPU ist sehr einfach strukturiert, um die Komplexität gering zu halten. Des Weiteren sind alle unwichtigen Details aus der CPU entfernt worden, damit deren interne Arbeitsweise einfacher verständlich ist. Die interne Struktur der Primitiv-CPU in Abbildung 11.1 unterscheidet sich von den vorherigen Abbildungen darin, dass Abbildung 11.1 den Mechanismus enthält, mit dem Daten innerhalb der CPU bewegt werden. Jedes der Register (Programmzähler, Speicheradressregister, Datenregister etc.) besteht aus D-Flipflops. Wenn ein Takteingang eines Registers mit einem Impuls versehen (d. h. getaktet) wird, werden die Daten an den DEingangsleitungen des Registers zu seinen Ausgangsleitungen übertragen und konstant gehalten, bis das Register erneut getaktet wird. Die Verbindungen zwischen den Regis-

203

11. Implementierung der Kontrolleinheit

Takt PC

Q D

PC

G1

Takt MAR

MAR

D Q

Aktivieren Adresse G2 R/W

HauptSpeicher Daten

Inkrementer

ein

aus

G6

G7 G4

G3 G8 Q D

IR

Takt MBR

MBR

Q D

G9 SteuerEinheit

G10

… G1G2

Takt PC

Takt D0

D0

Q D

ALU G12

G11

Abbildung 11.1.: Detaillierteres CPU-Schema

tern sind m Bit breite Datenbusse. In Abbildung 11.1 sind solche Datenbusse durch einfache Linien dargestellt. Der Ausgang der Steuereinheit in der Abbildung besteht aus zwölf Signalen, welche die Tri-State-Gatter G 1 bis G 12 aktivieren, zwei Signalen für die Steuerung des Hauptspeichers und fünf Taktsignalen, die die Takteingänge des Programmzählers, des Speicheradressregisters, des Speicherpufferregisters, des Befehlsregisters und des Datenregisters D 0 (Akkumulator) steuern (also alle Register, die aus Flip-Flops bestehen). Die verwendeten Tri-State-Gatter besitzen gegenüber normalen Gattern die Besonderheit, dass sie neben den zwei logischen Zuständen (0 und 1) noch einen dritten Zustand besitzen, bei denen der Ausgang elektrisch offen ist, also keine elektrische Verbindung zum Ausgang des Gatters besteht. Man kann sich das so vorstellen, dass hier ein elektrischer Schalter in die Ausgangsleitung eingebaut wurde, der mit Hilfe des Steuereinganges betätigt wird (Abbildung 11.2). So ist es möglich, dass die Ausgänge mehrerer Tri-State-Gatter auf einem Bus zusammengeschaltet werden können. Voraussetzung ist, dass zu jedem Zeitpunkt nur ein Gatter aktiv sein kann. Die Ausgänge der anderen TriState-Gatter auf diesem Bus müssen dann elektrisch offen sein. Stellen wir uns vor, wir möchten unseren Rechner einen Lese-Ausführ-Zyklus durchlaufen lassen, in dem der Befehlscode zu einer Addition von zwei Bytes gehört:

204

Technische Informatik I

11.1. Mikroprogrammierbare Steuereinheiten

e

y s

s 0 0 1 1

e 0 1 0 1

y x x 0 1

Abbildung 11.2.: Tri-State-Gatter und Wahrheitstabelle (0, 1 – logisch 0, 1; x – elektrisch offen) ADD.B ,D0 Der Befehl soll somit den Inhalt der Speicherposition, die durch angegeben wird, zu den Inhalten des Datenregisters D 0 addieren und das Ergebnis im Register D 0 hinterlegen.1 Wir können die Folge der Operationen, die während der Ausführung dieses Befehls stattfinden, nicht nur in Registertransfersprache aufschreiben, sondern zusätzlich auch in Ausdrücken, welche die Aktivierung von Gattern und die Taktsteuerung von Flipflops beschreiben. Die Tabelle 11.1 stellt die Folge der Mikrobefehle während des Lese-Ausführ-Zyklus eines ADD.B-Befehls dar. Es sollte hervorgehoben werden, dass die Lese-Phase aller Befehle eines Rechners identisch ist und lediglich die Ausführ-Phase entsprechend der Art des während der Lese-Phase gelesenen Befehlscodes variiert. Schritt 1 1a 2 3

[MAR] ← [PC] INC ← [PC] [PC] ← [INC] [MBR] ← [Speicher([MAR])]

Registertransfersprache

4 4a 5 6

[IR] ← [MBR] CU ← [IR(Anweisung)] [MAR] ← [IR(Adresse)] [MBR] ← [Speicher([MAR])]

7 7a 8

ALU ← [MBR] ALU ← [D 0 ] [D 0 ] ← ALU

Benötigte Operationen Aktiviere G 1 , takte MAR Aktiviere G 2 , takte PC Aktiviere Speicher lesend, aktiviere G 6 und G 11 , takte IR Aktiviere G 7 , takte IR Aktiviere G 3 , takte MAR Aktiviere Speicher lesend, aktiviere G 6 und G 11 , takte IR Aktiviere G 7 Aktiviere G 10 Aktiviere G 12 , takte Datenregister

Tabelle 11.1.: Ein Lese-Ausführ-Zyklus In Tabelle 11.1 wurde bei der Verwendung der Registertransfersprache bewusst die in englischer Fachliteratur verwendeten Abkürzungen für die Registernamen gewählt, um die Verwendung dieser Sekundärliteratur zu erleichtern. Folgende weitere Bemerkungen sind zu der Tabelle 11.1 zu machen: 1. Bei Zeilen, bei denen das Feld „Benötigte Operationen“ leer ist, werden diese Operationen automatisch ausgeführt. Zum Beispiel ist der Ausgang des Programmzählers immer mit dem Eingang des Hochzählers verbunden und es ist somit keine explizite Operation nötig, um den Inhalt des Programmzählers in den Hochzähler zu bewegen. 1 Die Anweisung wurde dem Befehlssatz des Motorola-68000-Mikroprozessors entnommen.

Technische Informatik I

205

11. Implementierung der Kontrolleinheit 2. Jedes Tri-State-Gatter, welches nicht explizit erwähnt wird, ist deaktiviert. 3. Die Schritte 1 und 1a, 4 und 4a sowie 7 und 7a werden jeweils gleichzeitig ausgeführt. Wenn durch Aktivieren des entsprechenden Signals auf den Hauptspeicher zugegriffen wird, findet ein Speicherlese- oder Speicherschreibzyklus statt. Das Lese/Schreib-Signal (Read/Write oder kurz R/W ) bestimmt die Art des Speicherzugriffs. Wenn das R/W Signal deaktiviert ist (= 0), wird ein Schreibzyklus ausgeführt. Ist R/W = 1 (aktiviert), wird aus dem Speicher gelesen. Das Signal wird ignoriert ( X ), wenn der Speicher nicht aktiviert ist (Enable= 0). Tabelle 11.2 stellt die 19 Ausgänge der Steuereinheit als eine Folge von Binärwerten dar, die während der Lese- und Ausführungsphasen des ADD.B-Befehls erzeugt werden.

G4

G5

G6

G7

G8

G9

G 10

G 11

G 12

Enable

R/W

PC

MAR

MBR

D0

IR

Registertakte

G3

Hauptspeicher

G2

Gattersteuersignale G1

Schritt

1

1

0

0

0

0

0

0

0

0

0

0

0

0

X

0

1

0

0

0

2

0

1

0

0

0

0

0

0

0

0

0

0

0

X

1

0

0

0

0

3

0

0

0

0

0

1

0

0

0

0

1

0

1

1

0

0

1

0

0

4

0

0

0

0

0

0

1

0

0

0

0

0

0

X

0

0

0

0

1

5

0

0

1

0

0

0

0

0

0

0

0

0

0

X

0

1

0

0

0

6

0

0

0

0

0

1

0

0

0

0

1

0

1

1

0

0

1

0

0

7

0

0

0

0

0

0

1

0

0

1

0

0

0

X

0

0

0

0

0

8

0

0

0

0

0

0

0

0

0

0

0

1

0

X

0

0

0

1

0

Tabelle 11.2.: Steuersignale bei Lese- und Ausführphasen eines ADD.B-Befehls Wenn für jeden der acht Schritte in Tabelle 11.2 die 19 Signale in die verschiedenen Teile der CPU aus Abbildung 11.1 überführt werden, wird ein Lese-Zyklus ausgeführt. In einem realen Rechner werden weit mehr als 19 Signale durch die Steuereinheit erzeugt. Ein typischer Wert könnte im Bereich 64 bis 200 liegen. Einer der wesentlichsten Unterschiede zwischen einem Mikro- und einem Makrobefehl ist, dass erstgenannter aus mehr Feldern besteht und mehrere Operanden benutzen kann, während der Makrobefehl häufig nur einen (Makro-) Befehlscode und ein oder zwei Operanden angibt. Die acht Schritte in Tabelle 11.2 stellen ein Mikroprogramm dar, das eine Lesephase interpretiert, gefolgt von einer Ausführungsphase des ADD.B-Befehls. Wir haben gezeigt, dass ein Makrobefehl durch eine Folge von Mikrobefehlen interpretiert wird und dass es ein Mikroprogramm für jeden Makro-Befehlscode der CPU gibt. Wir werden nun einen Blick auf das Mikroprogramm selbst werfen und uns überlegen, welche Hardware benötigt wird, um es auszuführen. Das Mikroprogramm selbst wird durch einen Mechanismus ausgeführt, welcher dem für die Ausführung des Makrobefehls (d. h. des Maschinencodes) sehr ähnlich ist. Zum Beispiel hat die mikroprogrammierbare Steuereinheit einen Mikroprogrammzähler statt eines Programmzählers (Abbildung 11.3). Die mikroprogrammierbare Steuereinheit besteht aus Adressumsetzer (address mapper), Inkrementer (incrementer), Mikroprogrammzähler (microprogram counter), Mikroprogrammspeicher (microprogram memory), Mikroprogrammregister (microinstruction register) und Multiplexer. Über den Adressumsetzer wird der Mikroprogrammzähler mit der Startadresse des für den aktuellen Befehlscode benötigten Mikroprogramms geladen. Diese

206

Technische Informatik I

11.1. Mikroprogrammierbare Steuereinheiten

Befehlsregister

Befehlscode

AdressUmsetzer

Adresse

Inkrementer

Mikroprogrammzähler

Takt

Adresse MikroprogrammSpeicher Daten

Nächste Mikrobefehlsadresse

LadeZustands- CPU-Steuer.-Feld Steuer. Auswahl

Microbefehlsregister

Mikroprogrammsteuersignale zu allen Teilen der CPU

Multiplexer

Springe bei Null Springe bei Nicht-Null Springe nie (logisch Null) Springe immer (logisch Eins)

Abbildung 11.3.: Grundstruktur einer mikroprogrammierbaren Steuereinheit Adresse wird an die Adresseingänge des Mikroprogrammspeichers angelegt und der Datenausgang des Speichers wird in das Mikrobefehlsregister gelesen. Die Information des Mikrobefehlsregisters ist in vier Felder aufgeteilt: Adresse des nächsten Mikrobefehls, ein Steuerungsfeld für den Mikroprogrammzähler, Zustandsfeld und CPU-Steuerungsfeld. Die meisten Bits des Mikrobefehlsregisters gehören zum CPU-Steuerungsfeld und steuern den Informationsfluss innerhalb der CPU, indem sie Tri-State-Gatter aktivieren und Register takten. In unserem Beispiel sind dies die 19 Signale aus Tabelle 11.2, die die Tri-State-Gatter und Flipflops in Abbildung 11.3 takten.

11.1.1. Steuerung der Mikrobefehlsfolge Die Abarbeitung eines Mikroprogramms erfolgt im Normalfall sequentiell, d. h. die Mikroprogrammbefehle werden wie in Tabelle 11.1 beschrieben abgearbeitet. Dabei wird die Adresse des Mikroprogrammzählers nach dem Laden eines Mikrobefehls um 1 erhöht. Alternativ ist es möglich, einen Sprung zu einer anderen Mikroprogrammadresse vorzunehmen, indem die Adresse des nächsten Mikrobefehls vom Mikrobefehlsregister in den Mikroprogrammzähler geladen wird. Die Frage ist nun, wie die Mikrobefehlsfolge gesteuert wird. Zur Steuerung der Mikrobefehlsfolge werden die drei linken Felder des Mikrobefehlsregisters genutzt (Abbildung 11.3). Das Steuerungsfeld für den Mikroprogrammzähler

Technische Informatik I

207

11. Implementierung der Kontrolleinheit (load control) bestimmt, ob die Adresse des als nächstes auszuführenden Befehls einfach durch Inkrementieren des Mikroprogrammzählers, durch Laden einer neuen Adresse aus dem Mikrobefehlsregister (next microinstruction address) oder durch den Adressumsetzer bereitgestellt wird. Zur Realisierung einer Sprungbedingung dient das Zustandsfeld (condition select), das mit einem Multiplexer zur Auswahl der Sprungbedingung verbunden ist. Die Sprungbedingungen werden aus den Zustandsbits (z. B. Z, N, C und V) gewonnen, die aufgrund von Operationen in der ALU gesetzt werden. Wenn der Ausgang des Multiplexers gültig, d. h. 1 ist, wird ein Sprung zu der Adresse durchgeführt, die durch den Inhalt des Feldes für die nächste Mikrobefehlsadresse angegeben wird. Im Beispiel in Abbildung 11.3 werden zwei der Bedingungen aus dem Zustandsregister geladen und zwei Bits sind wahr oder falsch. Eine falsche Bedingung bedeutet „keine Verzweigung“ (d. h. weitermachen) und eine wahre Bedingung impliziert „verzweigen“ (d. h. springen). Ein Sprung auf der Makrobefehlsebene wird durch einen Sprung auf der Mikrobefehlsebene realisiert, indem zu einem Programmteil verzweigt wird, der die Manipulation des Befehlszählers vornimmt, oder nicht. Die Abarbeitung eines Mikrobefehls erfolgt wie folgt: Die Adresse des aktuellen Mikrobefehls im Mikroprogrammzähler wird an den Mikroprogrammspeicher gelegt. Die dort gespeicherten Daten werden in das Mikrobefehlsregister geladen. Die enthaltenen Bits für das CPU-Steuerungsfeld steuern den Informationsfluss in der CPU. Dabei bedeuten die drei Felder, die am weitesten links stehen, wie sich die Adresse des als nächstes auszuführenden Mikrobefehls ergibt. Der Mikroprogrammzähler wird darauf folgend inkrementiert und mit einer Sprungadresse oder mit einer Adresse aus dem Adressumsetzer geladen. Diese Prozedur beginnt dann von vorn. Bei der Abarbeitung eines Mikroprogramms wird zunächst eine Adresse aus dem Adressumsetzer in den Mikroprogrammzähler geladen. Damit beginnt die Abarbeitung eines Mikroprogramms, das Befehlsfolgen (einschließlich Sprüngen) enthält. Ist die Befehlsfolge abgearbeitet, so wird dies durch ein Bit im Steuerungsfeld für den Mikroprogrammzähler signalisiert, sodass die Adresse für den nächsten Mikrobefehl vom Adressumsetzer bereitgestellt wird. Man kann sich die Abarbeitung eines Mikroprogramms auch so vorstellen, als würde zur Abarbeitung eines Befehls eine Subroutine (also das Mikroprogramm) aufgerufen, dessen Adresse durch den Adressumsetzer bereitgestellt wird. Wenn die Subroutine abgearbeitet ist, erfolgt ein Rücksprung, der dafür sorgt, dass der nächste Befehl abgearbeitet werden kann. Der Rücksprung entspricht dem Setzen des Kontrollbits, das dafür sorgt, dass die Startadresse zur Abarbeitung des Mikroprogramms für den nächsten Befehl vom Adressumsetzer in den Mikroprogrammzähler geladen wird.

11.1.2. Implementierung des Lese-Ausführ-Zyklus Der erste Teil eines jeden Mikroprogramms, das durch die Steuereinheit ausgeführt wird, gehört zur Lesephase eines Makrobefehls und endet damit, dass der Befehlscode des Makrobefehls im Befehlsregister hinterlegt wird. Der Befehlscode wird an den Adressumsetzer übergeben, einer Tabelle, die die Startadresse des Mikroprogramms für jeden möglichen Befehlscode enthält. Das heißt, der Adressumsetzer übersetzt das willkürliche Bitmuster des Befehlscodes in die Position des dazugehörigen Mikroprogramms, das diesen Befehlscode ausführen wird.

208

Technische Informatik I

11.2. Hartverdrahtete Steuereinheiten Die obige Beschreibung für eine mikroprogrammierbare Steuereinheit ist natürlich grob vereinfacht: In der Praxis beinhaltet ein Mikroprogramm normalerweise Teildienste für die Behandlung von Unterbrechungen (Interrupts), Zugriff auf den Hauptspeicher und der Ein- und Ausgabe. Einer der großen Vorteile einer mikroprogrammierbaren Steuereinheit ist die Möglichkeit, den Inhalt des Mikroprogrammspeichers verändern zu können und Flexibilität hinsichtlich des Befehlssatzes zu erreichen. Tatsächlich ist es möglich, eine Menge von Mikroprogrammen zu entwerfen, die den Maschinencode eines gänzlich anderen Rechners ausführen. In diesem Fall spricht man davon, dass der Rechner einen anderen Rechner emuliert. So ein Teildienst ist nützlich, wenn man einen älteren Rechner gegen einen neuen eintauscht, dessen eigener Maschinencode zu dem der alten Programme inkompatibel ist. Emulation findet Anwendung, wenn Programme in Binärformat vorliegen und auf der neuen Maschine ausgeführt werden sollen. Man entwickelt Mikroprogramme auf der neuen Maschine, die den Maschinencode der alten Maschine interpretieren. So ist es möglich, „alte Software“ in neuer technologischer Umgebung zu nutzen. Die mikroprogrammierbare Steuereinheit interpretiert also einen Maschinenbefehl mit Hilfe eines Mikroprogramms, welches in einem ROM gespeichert ist.

11.2. Hartverdrahtete Steuereinheiten R

System-Bus Lesen

Hauptspeicher Adresseingang

Daten

Schreiben

W CMAR

GMSR GMSW

MAR

CMBR

MBR

GMBR

IR

GIR

EMBR

CIR

EIR CU CPC CD0

PC

GPC

D0

GD0

EPC ED0

F0 F1

ALU

P Q f(P,Q)

GALU EALU

Abbildung 11.4.: Struktur einer primitiven CPU Abbildung 11.4 stellt die Struktur einer primitiven CPU für diesen Ansatz dar. Sie ist primitiv, da die Anzahl der Busse und funktionalen Einheiten auf das bloße Minimum

Technische Informatik I

209

11. Implementierung der Kontrolleinheit reduziert wurden. Dies macht die CPU zwar billig in der Herstellung, verringert aber ihre Geschwindigkeit. Da es nur einen internen Bus gibt, können mehrere Mikrobefehle nicht gleichzeitig ausgeführt werden, um so parallel Operationen auszuführen. Zum Beispiel gibt es keinen eigenen Zähler für den Inhalt des Programmzählers, was daraus hinausläuft, dass die ALU zum Erhöhen des PC genutzt werden muss. Folglich stehen die ALU und die mit ihr verbundenen Datenpfade nicht für andere Operationen zur Verfügung, während der Programmzähler erhöht wird. In Abbildung 11.4 ist ein einzelner Bus mit allen Registern, dem Hauptspeicher und der ALU verbunden, was den Transfer von nur einem Datenwort gleichzeitig von einer Quelle zu einem Ziel ermöglicht. Der Hauptspeicher empfängt die Adresse einer Speicherposition, auf die zugegriffen werden soll, direkt aus dem Speicheradressregister (MAR), dessen Ausgang permanent mit dem Adresseingang des Hauptspeichers verbunden ist. Eine feste oder zugewiesene Verbindung zwischen Speicheradressregister und Hauptspeicher ist möglich, da der Hauptspeicher niemals von anderen Quellen als dem Speicheradressregister Adressinput erhalten wird. Eine permanente Verbindung wie diese ist eine gute Sache, da sie die Notwendigkeit für Buskontrollschaltkreise eliminiert. Der bidirektionale Datenbus vom Hauptspeicher ist mit dem Systembus durch die TriState-Gatter GMSR und GMSW verbunden. Während eines Speicherschreibzyklus (W = 1) werden Daten vom Systembus über das Gatter GMSW zum Speicher übertragen. Bei einem Lesezyklus (R = 1) werden Daten vom Speicher über das Gatter GMSR zum Systembus übertragen. Speicherpufferregister, Datenregister, Programmzähler und Befehlsregister sind jeweils genauso mit Rücksicht auf den internen Bus angeordnet. Wenn eines dieser Register Daten auf den Bus legen will, wird sein Tri-State-Gatter aktiviert. Andererseits werden die Daten vom Bus in ein Register kopiert, indem dieses Register getaktet wird. Man beachte, dass das Anweisungsregister so angeordnet ist, dass es Daten direkt aus dem Hauptspeicher empfangen kann, ohne dass diese Daten das Speicherpufferregister passieren müssen. Die ALU empfängt Daten aus zwei Quellen: dem internen Bus und dem Datenregister. Sie platziert den eigenen Ausgang auf den Systembus. Diese Tatsache wirft folgende Frage auf: Wenn die ALU ihre Daten vom Systembus bekommt, wie kann sie Daten zur selben Zeit auf genau diesen Bus legen? Es gibt zwei mögliche Lösungen für dieses Problem. Die erste besteht darin, ein Register in die ALU einzubauen, welches die Inputdaten konstant hält, während der Systembus mit dem Ausgang der ALU verbunden wird. Das heißt, die ALU wird mit ihrem eigenen „Speicherpufferregister“ ausgestattet. Eine andere Lösung verlässt sich auf einen Effekt, der bisher als schädlich angesehen wurde. Es ist die Signalverzögerung, die beim Transfer von digitalen Signalen auftritt, wenn diese sich durch ein Logiknetzwerk bewegen. Werden Daten an den Eingängen der ALU zur Verfügung gestellt, bewegen diese sich durch die Gatter der ALU, um anschließend nach einer Verzögerung von 50 Nanosekunden an den Ausgängen der ALU zur Verfügung zu stehen. Nehmen wir an, dass die Inhalte des Speicherpufferregisters in die ALU überführt wird, indem das Gatter GMBR aktiviert wird und dass genügend Zeit verstrichen ist, sodass sich die Ausgänge der ALU einpendeln konnten. Wenn nun das Gatter GMBR deaktiviert wird, sind die Eingänge der ALU nicht länger gültig. Wie auch immer, die Ausgänge der ALU können sich aufgrund der Signalverzögerungen nicht sofort verändern. Wenn das Gatter GALU nun aktiviert und das Datenregister getaktet wird,

210

Technische Informatik I

11.2. Hartverdrahtete Steuereinheiten kann der Systembus für Datentransfer benutzt werden, bevor sich der Ausgang der ALU verändert. Dies ist eine Art digitaler Jongliertrick. Die ALU wird durch einen 2-Bit-Code kontrolliert, welcher die Funktion bestimmt, wie in Tabelle 11.3 dargestellt ist. Dies sind Repräsentationen von realen Befehlen, obwohl eine reale ALU typischerweise 16 verschiedene Funktionen implementiert. F1

F0

0 0 1 1

0 1 0 1

Bedeutung Addiere P zu Q Subtrahiere Q von P Erhöhe Q Verringere Q

Tabelle 11.3.: Bedeutung der ALU-Kontrollcodes Um das Design der Steuereinheit für die wahlfreie Logik so einfach wie möglich zu halten, werden wir einen drei Bit breiten Befehlscode entwickeln, was uns insgesamt acht Operationen ermöglicht. Das Repertoire der Befehle in Tabelle 11.4 stellt tatsächlich eine Menge von sehr einfachen Operationen dar, beinhaltet dennoch viele Befehlstypen, die auch in realen Prozessoren zu finden sind. Bitmuster 000 001 010 011 100 101 110 111

Symbol LOAD STORE ADD SUB INC DEC BRA BEQ

M M M M M M M M

Ausgeführte Operation [D 0 ] ← [Speicher(M )] [Speicher(M )] ← [D 0 ] [D 0 ] ← [D 0 ] + [Speicher(M )] [D 0 ] ← [D 0 ] − [Speicher(M )] [Speicher(M )] ← [Speicher(M )] + 1 [Speicher(M )] ← [Speicher(M )] − 1 [PC] ← M If Z = 1 Then [PC] ← M

Tabelle 11.4.: Einfache Anweisungsmenge für die CPU aus Abbildung 11.4 Nachdem die Befehlsmenge entworfen wurde, besteht der nächste Schritt darin, jeden Befehl in Ausdrücken der Registertransfersprache zu interpretieren und die Folge der Operationen zu bestimmen, die notwendig sind, um die Anweisung auf dem Rechner aus Abbildung 11.4 auszuführen. Tabelle 11.5 listet alle Taktsignale (Notation: CRegister ) und alle dreiwertigen Bussteuersignale (Notation: EDatenquelle ) auf, die für die Ausführung jeder CPU-Anweisung notwendig sind. Das Symbol Z (Zero) ist die Markierung für ein Null-Ergebnis im Zustandsregister, welches selbst als Teil der ALU angenommen wird. Tabelle 11.5 könnte auch benutzt werden, um eine mikroprogrammierbare Steuereinheit zu entwickeln, wobei die Zeilen der Tabelle aufeinanderfolgende Datenworte im SteuerROM wären. Verdrahtet man es dagegen hart, kommt man zu einem Ergebnis, wie es in Abbildung 11.10 dargestellt ist. Dies ist unser Ziel, welches wir jetzt wie ein Puzzle zusammensetzen.

Technische Informatik I

211

11. Implementierung der Kontrolleinheit Befehl

Bits

Ausgeführte Operation

Aktionen für die Steuerung

Fetch

--

[MAR] ← [PC]

EPC = 1

CMAR

[IR] ← [Speicher([MAR])]

R=1

CIR

ALU ← [PC]

EPC = 1

F 1 , F 0 = 1, 0

LOAD

000

STORE

001

ADD

SUB

INC

DEC

010

011

100

101

[PC] ← ALU

EALU = 1

CPC

[MAR] ← [IR]

EIR = 1

CMAR

[D 0 ] ← [Speicher([MAR])]

R=1

CD 0

[MAR] ← [IR]

EIR = 1

CMAR

[Speicher([MAR])] ← [D 0 ]

ED 0 = 1

W=1

[MAR] ← [IR]

EIR = 1

CMAR

[MBR] ← [Speicher([MAR])]

R=1

CMBR

ALU ← [MBR]

EMBR = 1

F 1 , F 0 = 0, 0

[D 0 ] ← ALU

EALU = 1

CD 0

[MAR] ← [IR]

EIR = 1

CMAR

[MBR] ← [Speicher([MAR])]

R=1

CMBR

ALU ← [MBR]

EMBR = 1

F 1 , F 0 = 0, 1

[D 0 ] ← ALU

EALU = 1

CD 0

[MAR] ← [IR]

EIR = 1

CMAR

[MBR] ← [Speicher([MAR])]

R=1

CMBR

ALU ← [MBR]

EMBR = 1

F 1 , F 0 = 1, 0

[MBR] ← ALU

EALU = 1

CD0

[Speicher([MAR])] ← [MBR]

EMBR = 1

W=1

[MAR] ← [IR]

EIR = 1

CMAR

[MBR] ← [Speicher([MAR])]

R=1

CMBR

ALU ← [MBR]

EMBR = 1

F 1 , F 0 = 1, 1

[MBR] ← ALU

EALU = 1

CD 0

[Speicher([MAR])] ← [MBR]

EMBR = 1

W=1 CPC

BRA

110

[PC] ← [IR]

EIR = 1

BEQ

111

If Z = 1 Then [PC] ← [IR]

If Z = 1 Then EIR = 1, CPC

Tabelle 11.5.: Interpretation der Befehlsmenge aus Tabelle 11.4

11.2.1. Vom Befehlscode zur Ausführung Um einen Befehl auszuführen, müssen wir zwei Dinge tun: Die drei Bit breiten Befehlscodes in eine von acht möglichen Aktionsfolgen umsetzen und dann veranlassen, dass diese Aktionen stattfinden. Abbildung 11.5 zeigt, wie die Befehle über einen Anweisungsdekodierer entschlüsselt werden. Für jede der acht möglichen Befehlscodes gibt es genau einen Ausgang, der gleich 1 sein wird. Wenn zum Beispiel der Befehlscode, der zu ADD (d. h. 010) gehört, während einer Lesephase in das Befehlsregister geladen wird, wird der zweite Ausgang des UNDGatter-Feldes gleich 1 sein, während alle anderen UND-Gatter-Ausgänge bei 0 bleiben. Es hat allerdings noch keinen Nutzen, einen bestimmten Befehl zu erkennen und zu entschlüsseln. Die Steuereinheit muss die Folge von Mikrobefehlen ausführen, um die Makroanweisung (den Maschinenbefehl) zu realisieren. Um dies zu tun, benötigt sie eine Quelle von Signalen, um alle notwendigen Mikrobefehle aufzulösen.

212

Technische Informatik I

11.2. Hartverdrahtete Steuereinheiten Operandenadresse

Op-Code Bit 3

Bit 2

Befehlsregister

Bit 1

1

1

1

&

000

MOVE.B M,D0 (d.h. laden)

001

MOVE.B D0,M (d.h. speichern)

010

ADD.B M,D0

111

BEQ N

&

&

... &

Abbildung 11.5.: Anweisungsdekodierer Eine Schaltung, die eine Reihe von Auslösersignalen erzeugt, heißt Sequenzer. Abbildung 11.6 zeigt das Logikdiagramm eines passenden Acht-Schritt-Sequenzers. Die Ausgänge von drei JK-Flipflops sind als 3-Bit-Binärzähler angeordnet und mit acht UNDGattern (mit jeweils drei Eingängen) verbunden, um Zeitsignale T0 bis T7 zu erzeugen. Zähler rücksetzen in Status T0

1 J K

CLR

Q

J

Q

K

CLR

Q

J

Q

K

CLR

Acht Ausgänge

Q

Q &

T0 &

T1 &

T2 &

T3 &

T4 &

T5 &

T6 &

T7

Abbildung 11.6.: Schaltung eines einfachen Sequenzers Abbildung 11.7 zeigt die Zeitimpulse, die durch diese Schaltung erzeugt werden. Man beachte, dass der Zeitdekodierer ähnlich dem Befehlsdekodierer aus Abbildung 11.5 ist. Da nicht alle Maschinenbefehle die gleiche Anzahl von Mikrobefehlen zur Interpretation benötigen, hat der Sequenzer in Abbildung 11.6 einen Reset-Eingang, um ihn direkt auf den Zustand T0 zurückzusetzen. Der Sequenzer aus Abbildung 11.7 ist eher illustrativ als praktisch, da aufgrund der Verwendung eines asynchronen Zählers unechte Impulse an den Zeitimpulsausgängen erzeugt werden können. Die Ausgänge eines asynchronen Zählers ändern ihren Zustand nicht alle auf einmal und das Bitmuster an den Ausgängen kann deswegen mehrere Zu-

Technische Informatik I

213

11. Implementierung der Kontrolleinheit

T0 T0

Zeitimpulsgenerator

T1 T1 T2 T2 T3 T3 T4 T4 T5 T5 T6 T6 T7 T7

Ein Maschinenzyklus (acht Mikrozyklen)

Abbildung 11.7.: Ausgänge des Sequenzers stände durchlaufen, bevor es sich auf seinen endgültigen Wert festlegt. Unglücklicherweise können diese Störungen lange genug andauern, um ein falsches Zeitsignal zu erzeugen, das unerwünschte Aktivitäten in der Steuereinheit verursachen kann. Eine Lösung für dieses Problem ist, die Ausgänge für den Zeitpulsgenerator zu deaktivieren, bis sich der Zähler eingependelt hat. Der nächste Schritt bei der Entwicklung der Steuereinheit besteht darin, die Signale vom Befehlsdekodierer mit den Zeitsignalen vom Sequenzer zu verbinden, um so die eigentlichen Steuersignale zu erzeugen. Abbildung 11.8 zeigt einen möglichen Ansatz. Für jede Ausführung der acht Maschinenbefehle (incl. Lesephase) befindet sich eine der vertikalen Linien vom Anweisungsdekodierer im 1-Zustand und aktiviert die UND-Gatter, mit denen es verbunden ist. Wenn die Zeitsignale T0 bis T7 entstehen, erzeugen die UNDGatter, die durch den aktuellen Befehl aktiviert wurden, die Steuersignale, die für die Implementierung der Steuereinheit benötigt werden. Jeder Ausgang eines UND-Gatters, der zu einem einzelnen Mikrobefehl (z. B. CMAR ) gehört, ist mit einem ODER-Gatter verbunden, dessen Ausgang den eigentlichen Mikrobefehl auslöst. Wie wir bereits hervorgehoben hatten, benötigen nicht alle Makrobefehle acht Taktzyklen zur Ausführung. In Abbildung 11.8 sind die UND-Gatter, die einen Ausführen-Eingang besitzen, Teil des Decoders aus Abbildung 11.5. Für jeden Mikrobefehl wird eine bestimme Menge von Steuersignalen zu einer angegebenen Zeit aktiviert. So kann jedes Steuersignal von mehreren Maschinenbefehle (möglicherweise während verschiedenen Taktzyklen) aktiviert und durch ODER-Verknüpfung der verschiedenen Aktionen erzeugt werden (wie in Abbildung 11.10 gezeigt).

11.2.2. Das Lese-Ausführ-Flip-Flop Bisher haben wir einen Mechanismus zur Interpretation eines jeden Maschinenbefehls entworfen, aber nicht das Problem des zweiphasigen Lese-Ausführ-Zyklus angepackt. Da sich die Steuereinheit immer in einem von zwei Zuständen befindet (Lesen oder Ausführen), stellt ein RS-Flipflop einen bequemen Weg dar, von einem Zustand in den anderen umzuschalten. Wenn Q gleich 0 ist, ist die aktuelle Arbeitsweise die Lesephase, und wenn Q gleich 1 ist, wird eine Ausführphase durchlaufen. Abbildung 11.9 ist eine Erweiterung von Abbildung 11.8 und zeigt, wie der Befehlsdekodierer durch den Q-Ausgang des Lese-AusführFlipflops und der Lesedekodierer durch den Q-Ausgang aktiviert wird.

214

Technische Informatik I

11.2. Hartverdrahtete Steuereinheiten Op-code

Op-code

Holen

Ausführen

T0 &

T1 &

T2 &

T3

Holen

EPC=1, CMAR R=1, CIR

&

&

&

&

T0 &

T1

EIR=1, CMAR R=1, CD0

&

T0 &

T1

EIR=1, CMAR ED0=1, W=1

EPC=1, F1, F0=1 EALU=1, CPC

MOVE (Adresse),D0 (Laden)

MOVE D0,(Adresse) (Speichern)

Abbildung 11.8.: Verdrahtete Steuereinheit

Am Ende jeder Lesephase setzt ein Taktimpuls vom Zeitgenerator das Lese-AusführFlipflop und erlaubt somit, dass der aktuelle Befehl entschlüsselt und ausgeführt wird. Der Sequenzer wird ebenfalls am Ende jeder Lesephase zurückgesetzt. Am Ende der Ausführphase hingegen wird das Lese-Ausführ-Flipflop gelöscht und der Sequenzer zurückgesetzt, sodass die nächste Lesephase beginnen kann. Tabelle 11.6 am Ende des Kapitels zeigt, wie Maschinenbefehle als zeitgesteuerte Signale dargestellt werden können. Dabei entsprechen die Zeitschritte einer Sequenz von Mikrobefehlen, die die Gatter für die Busaktivierung, die Impulsgeneratoren, die ALUFunktions-Bits, die Hauptspeicherzugriffsart (R und W) und die Reset- und Set-Eingänge für das Lese-Ausführ-Flipflop steuern. Für jedes Steuersignal können wir einen booleschen Ausdruck bestimmen, der die sie beeinflussenden Mikrobefehle enthält. Schauen wir uns die Ausdrücke für EMBR , EIR und CMAR an. EMBR EIR CMAR

= ADD.T1 + SUB.T1 + INC.T1 + INC.T3 + DEC.T1 + DEC.T3 = Fetch.T4 + BRA.T0 + BEQ.T0 = Fetch.T0 + Fetch.T4

Daten aus dem MBR werden in vielen Mikrobefehlen verwendet, z. B. in Schritt 1 für Addition und Subtraktion. Entsprechend müssen die Enable-Signale gesetzt werden, wenn die Daten in einen der Befehle benötigt werden. Ebenso müssen die Flip-Flops getaktet werden (z. B. mittels CMAR ), wenn Daten in einem Register gespeichert werden sollen. Die Bedingungen werden durch die ODER-Gatter rechts in der Abbildung 11.10 realisiert. Eine detailliertere Schaltung für eine hartverdrahtete Steuereinheit, die den Befehlssatz aus Tabelle 11.4 interpretiert, ist in Abbildung 11.10 am Ende des Kapitels dargestellt und setzt die Teile aus den bisherigen Abbildungen zusammen. Wir sollten natürlich beachten, dass diese CPU stark vereinfacht ist und nur die Natur einer hartverdrahteten Steuereinheit abbildet statt ihres exakten Aufbaus.

Technische Informatik I

215

11. Implementierung der Kontrolleinheit

>=1

...

R

Lese-AusführFlipflop

Reset von anderen 7 Operationen

Q

Op-code

Ausführen Zu allen Op-code-Dekodern

Holen

&

S

Q

&

T0 & &

T0

T1 &

T2 &

T3 &

T4 (Setzt das Lese-Ausführ-Flipflop auf den T4-Slot des Hole-Zyklus)

(Setzt das Lese-Ausführ-Flipflop zurück am Ende vom Laden, bereit für das nächste Holen)

Holen

Laden

Abbildung 11.9.: Lese-Ausführ-Flipflop

11.3. Hartverdrahtet oder mikroprogrammierbar? Die beiden Ansätze zur Entwicklung einer Steuereinheit, die wir besprochen haben, sind gänzlich voneinander verschieden und jeder Entwickler muss sich für eine von beiden entscheiden. Wir können uns hier nicht weiter in die detaillierte Entwicklung einer Steuereinheit vertiefen und werden aus diesem Grund die wichtigsten Vorteile und Merkmale von mikroprogrammierbaren und hartverdrahteten Steuereinheiten aufführen. 1. Hartverdrahtete Steuereinheiten sind schneller als ihre mikroprogrammierbaren Gegenstücke. Das muss auch so sein, da die hartverdrahtete Steuereinheit für ihre bestimmte Anwendung optimiert ist. Außerdem wird eine mikroprogrammierbare Steuereinheit durch die Notwendigkeit, eine Mikroanweisung aus dem Mikroprogrammspeicher zu lesen, verlangsamt. Speicherzugriffe sind allgemein langsamer als grundlegende boolesche Operationen. 2. Mikroprogrammierung bietet die Möglichkeit für eine flexiblere Entwicklung. Da das Mikroprogramm im (schreibgeschützten) Speicher liegt, kann es leicht während der Entwicklung und auch während der Produktion verändert werden. Eine betriebsbereite hartverdrahtete Steuereinheit hingegen kann nicht einfach angepasst werden, um so neue Fähigkeiten (z. B. zusätzliche Maschinenanweisungen) aufzunehmen und es ist manchmal sehr schwer, Entwicklungsfehler ohne deutliche Änderungen an der Hardware zu entfernen.

216

Technische Informatik I

D0

ALU

MAR

MBR

IR

PC

D0

F1

F0

Speicher

FF S

PC

ALU

R

IR

Taktimpuls

W

Zeit

Fetch

T0

0

0

1

0

0

1

0

0

0

0

X

X

T1

0

0

0

0

0

0

0

1

0

0

X

X

1

0

0

0

T2

0

0

1

0

0

0

0

0

0

0

1

0

0

0

0

0

T3

0

0

0

0

1

0

0

0

1

0

X

X

0

0

0

0

T4

0

1

0

0

0

1

0

0

0

0

X

X

0

0

0

1

LOAD

T0

0

0

0

0

0

0

0

0

0

1

X

X

1

0

1

0

STORE

T0

0

0

0

1

0

0

0

0

0

0

X

X

0

1

1

0

ADD

T0

0

0

0

0

0

0

1

0

0

0

X

X

1

0

0

0

T1

1

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

T2

0

0

0

0

1

0

0

0

0

1

X

X

0

0

1

0

SUB

Aktiviert

R

Befehl

MBR

11.3. Hartverdrahtet oder mikroprogrammierbar?

0

0

0

0

T0

0

0

0

0

0

0

1

0

0

0

X

X

1

0

0

0

T1

1

0

0

0

0

0

0

0

0

0

0

1

0

0

0

0

T2

0

0

0

0

1

0

0

0

0

1

X

X

0

0

1

0

T0

0

0

0

0

0

0

1

0

0

0

X

X

1

0

0

0

T1

1

0

0

0

0

0

0

0

0

0

1

0

0

0

0

0

T2

0

0

0

0

1

0

1

0

0

0

X

X

0

0

0

0

T3

1

0

0

0

0

0

0

0

0

0

X

X

0

1

1

0

T0

0

0

0

0

0

0

1

0

0

0

X

X

1

0

0

0

T1

1

0

0

0

0

0

0

0

0

0

1

1

0

0

0

0

T2

0

0

0

0

1

0

1

0

0

0

X

X

0

0

0

0

T3

1

0

0

0

0

0

0

0

0

0

X

X

0

1

1

0

BRA

T0

0

1

0

0

0

0

0

0

1

0

X

X

0

0

1

0

BEQ

T0

0

1

0

0

0

0

0

0

Z

0

X

X

0

0

1

0

INC

DEC

Tabelle 11.6.: Interpretation der Maschinencodebefehle

Technische Informatik I

217

11. Implementierung der Kontrolleinheit

IR IR0

IR0

IR0 IR1

IR1

ODER-GatterSchaltmatrix

IR1 IR2

IR2

S Q

Fetch

R Q

Execute

IR2 Execute Fetch

Fetch/ execute flip-flop

EPC CMAR

CD0 Read Fetch

ED0 Write Fetch

CMBR Read

CIR Read

EMBR

EALU CD0 Fetch

EPC F1=1

CMBR Read

EMBR F0=1

EALU CD0 Fetch

EALU CPC

CIR Read

CIR Read

EMBR F1=1

EMBR F0=1 F0=1

EIR CPC Fetch

EPC CPC

EIR CPC =Z Fetch

T0

EALU

EIR

F0

CIR

F1

EMBR

Read

CMBR

Write

ED0

Fetch

CD0

Execute

CMAR

T1 EALU CMBR

EALU CMBR

EMBR Write Fetch

EMBR Write Fetc h

T2

T3

EIR CMAR Execute

T4 Master clock

Counter

FETCH LOAD

STORE ADD

SUB

INC

DEC

BRA

BEQ

Abbildung 11.10.: Die hartverdrahtete Steuereinheit

218

Technische Informatik I

12. RISC-Prozessoren Ein wesentlicher Faktor, der in den 70er Jahren die Architektur beeinflusste, war das Ziel, dem Applikationsprogrammierer bzw. Compilerbauer das Leben durch immer größere und komplexere Befehlssätze einfacher machen zu wollen. In dieser Zeit kamen Mikroprozessoren auf, auf die sich diese Entwicklung auswirkte. Einige Beispiele für komplexe Maschinenbefehle: 1.

hier wird der Inhalt von D 0 zum Inhalt der Speicherzelle addiert, auf die Adressregister A 5 zeigt. Danach wird das Register A 5 erhöht. 2. MOVE 12(A3,D0),D2 die Summe der Inhalte von A 3 und D 0 sowie des Literals 12 ergibt eine effektive Adresse, deren Inhalt nach D 2 zu transferieren ist. Solche Befehle erzeugen eine komplexe Abfolge von Aktionen (d. h. Mikrobefehlen) aus einem einzigen Maschinenbefehl. Prozessoren, die nach diesem Prinzip arbeiten, werden deshalb als Complex Instruction Set Computer (CISC, Computer mit komplexem Befehlssatz) bezeichnet. ADD

D0,(A5)+

So komfortabel die Programmierung von Prozessoren der CISC-Klasse auch ist, sie bringt Nachteile mit sich. Durch die gestiegene Komplexität steigt der Speicherplatzbedarf eines Befehls auf dem Prozessor-Die. Komplexe Befehle müssen im Prozessor in kleine Einzelschritte unterteilt werden, die auch ihre Zeit benötigen, um abgearbeitet zu werden. Dadurch benötigt die Abarbeitung von komplexen Befehlen entsprechend viel Zeit. Um CISC entgegenzuwirken, wurde RISC entworfen. RISC steht für Reduced Instruction Set Computer (Computer mit verkleinertem Befehlssatz). Man fragte sich, welche Aktionen die meiste CPU-Zeit benötigen und wie und mit welchen Befehlen die Programme ausgeführt werden, um die Abarbeitung optimieren zu können. Der Name RISC stammt von David Patterson und David Ditzel von der kalifornischen Berkeley-Universität. Der erste RISC-Prozessor war der IBM 801 (Radin), dem u.a. der SPARC (SUN), der Alpha (DEC), der PowerPC (Motorola) und der MIPS (Silicon Graphics) folgten.

12.1. Befehlsklassen Seit mehr als einem Jahrzehnt wurde umfangreiche Forschung von mehreren unabhängigen Informatikern durchgeführt, die die Art und Weise der Programmausführung untersuchten. Ihre Studien zeigten, dass die relative Häufigkeit, mit der bestimmte Klassen von Anweisungen ausgeführt werden, nicht einheitlich sind und dass einige Arten von Befehlen weit häufiger ausgeführt werden als andere. Statistiken über den Gebrauch von Befehlen gibt es zuhauf. Die hier verwendeten wurden von John Fairclough zusammengestellt, der die Befehle entsprechend ihrer Art in acht verschiedene Gruppen eingeteilt hat.

219

12. RISC-Prozessoren Befehlsklasse Zuweisung bzw. Datenbewegung Programmablauf (Sprünge etc.) Arithmetik Vergleich Logik Shifts Bitmanipulation Ein-/Ausgabe und Verschiedenes

Mittlere Ausführungshäufigkeit 45,28 % 28,73 % 10,75 % 5,92 % 3,91 % 2,93 % 2,05 % 0,44 %

Tabelle 12.1.: Befehlsklassen und Ausführungshäufigkeit nach Fairclough Die Daten in Tabelle 12.1 zeigen, dass die häufigste Anweisungs- oder Befehlsklasse die ursprüngliche Zuweisung oder Datenbewegung ist (in der Form P = Q in einer Hochsprache oder MOVE P,Q bzw. LOAD P in einer Sprache auf Assemblerebene). Ähnlich verhält es sich mit der Klasse der Programmsteuerungsbefehle, die bedingte und unbedingte Verzweigungen gemeinsam mit Unterprogrammaufrufen und -rücksprüngen beinhaltet und die zweithäufigste Gruppe von Anweisungen darstellt. Zusammen genommen bilden die Klassen Datenbewegung und Programmablaufsteuerung über 74 Prozent aller Befehle. Daraus folgt, dass wir bei einem großen Programm erwarten können, nur 26 Prozent an Befehlen vorzufinden, die nicht Zuweisungs- oder Programmablaufprimitive sind.

12.2. Beispiele für den RISC-Ansatz Eine unausweichliche Folgerung aus diesen Ergebnissen ist, dass Entwickler von Prozessoren besser dran wären, wenn sie ihre Zeit dafür opfern würden, die Art und Weise, in der Maschinen die Befehle der ersten beiden Gruppen ausführen, zu optimieren, anstatt nach neuen „mächtigen“ Befehlen zu suchen. In den frühen Tagen der Mikroprozessoren schlugen Chiphersteller den Weg ein, spezielle und mit ihrem Produkt einzigartige Befehle zu bieten. Diese Befehle wurden dann durch die Marketing- und Verkaufsabteilung über alle Maßen angepriesen. Heutzutage können wir sehen, dass sie ihre Bemühungen besser stattdessen auf die Optimierung der am häufigsten benutzten Befehle ausgerichtet hätten. RISC-Architekturen wurden entwickelt, um die Programmierumgebung auszunutzen, in der die meisten Befehle für Zuweisungen, Datenbewegungen oder Programmablauf zuständig sind. Als ein Beispiel für die Herangehensweise an die Entwicklung von RISC-Architekturen überlegen wir uns, wie ein konstanter Operand kodiert wird (d. h. wie viele Bits im Befehlsformat dafür reserviert werden). Tanenbaum fand heraus, dass 56 Prozent aller konstanten Werte von Operanden im Intervall [−15; +15] und sogar 98 Prozent im Intervall [−511; +511] liegen. Folglich würde die Einbeziehung eines 5-Bit-Konstantenfeldes in einer Anweisung nur für die Hälfte aller auftretenden Literale ausreichen. RISC-Architekturen haben eine ausreichend große Anweisungslänge, um ein Konstantenfeld aufzunehmen, welches genügend Platz für die Mehrheit aller Konstanten bietet. Messungen von Tanenbaum und von Halbert und Kessler zeigten, dass für 95 Prozent aller dynamisch aufgerufenen Prozeduren ( d. h. Unterprogramme/Subroutinen) zwölf

220

Technische Informatik I

12.3. Wünschenswerte Eigenschaften einer RISC-Architektur Speicherworte ausreichend sind, um ihre Parameter und lokalen Variablen aufzunehmen. Das heißt, wenn eine Rechnerarchitektur ca. zwölf Worte an Registern auf dem Chip selbst hat, sollte sie in der Lage sein, alle von den meisten Prozeduren benötigten Operanden zu handhaben, ohne auf den Hauptspeicher zugreifen zu müssen. Dies alles reduziert den Busverkehr zwischen Prozessor und Speicher und erlaubt kleine Adressfelder im Befehlsformat. Außerdem sollte eine wirklich effiziente Architektur Bestimmungen für jede Prozedur machen, wie diese den zwölf Registern zugewiesen werden sollten. Der Datentransfer zwischen den Registern sollte sehr schnell sein. Damit entfällt die Notwendigkeit, Daten zwischen Prozeduren zu bewegen. Nachdem wir nun einige der Bestandteile einer effizienten Architektur beschrieben haben, besteht der nächste Schritt darin, die Eigenschaften der ersten Generationen von RISC-Prozessoren anzusehen, um anschließend die Eigenschaften von RISC-Prozessoren im Detail zu beschreiben.

12.3. Wünschenswerte Eigenschaften einer RISC-Architektur 1. RISC-Prozessoren sollten ausreichend Speicher auf dem Chip (d. h. Register) haben, um die schlimmsten Effekte des Prozessor-Speicher-Flaschenhalses zu überwinden. Auf internen Speicher kann viel schneller zugegriffen werden, als auf chipexternen Hauptspeicher. Wir haben gesehen, dass Prozeduren im Mittel um die zwölf Wörter für lokale Sicherung und Parameterübergabe brauchen. Diese Tatsache deutet an, dass es in der Tat durchführbar ist, chipinternen Speicher zu nutzen. 2. Da Befehle aus der Programmablaufsteuerung auch Befehle für den Aufruf von und den Rücksprung aus Prozeduren enthalten und zudem häufig ausgeführt werden, sollte eine effektive Architektur auch Kriterien für die effiziente Übergabe von Parametern zwischen Prozeduren angeben. Dies wird durch sogenannte Registerfenster realisiert. 3. Wenn ein RISC-Prozessor schnell sein soll, muss er im Durchschnitt annähernd eine Anweisung pro Taktzyklus ausführen. Diese Anforderung bedeutet allerdings eine Begrenzung an Komplexität. 4. Beim Design von RISC-Prozessoren sollten selten benutzte Befehle nicht implementiert werden. Komplexe Befehle beanspruchen nicht nur Chipfläche sondern widersprechen auch den Anforderungen von Punkt 3 hinsichtlich der Anforderung eine Instruktion (Befehl) in möglichst einem Taktzyklus abzuarbeiten. Außerdem erhöht die Einbindung von komplexen Befehlen die Entwicklungszeit von Prozessoren. 5. Die Befehle eines RISC-Prozessors sollten, soweit möglich, Mikrobefehlen angeglichen sein. Dies ist eine Konsequenz aus Punkt 3: Damit ein (komplexer) Befehl in nur einem einzelnen Takt ausgeführt werden kann, muss er in seine Bestandteile zerlegt werden, die jeweils einen Takt dauern. Ähnlich sieht es auf einer CISC-CPU aus, auf der diese Zerlegung in der CPU geschieht bei der Umwandlung eines Makrobefehls in Mikrobefehle. Diese Umwandlung sollte aus Effizienzgründen auf RISC-CPUs entfallen. (Dies bedeutet nicht, dass sie hartverdrahtet sein muss.) 6. Ein effizienter Prozessor sollte nur ein Befehlsformat haben (oder zumindest nur einige wenige Formate). Das erlaubt die Entschlüsselung eines Befehls in ihre Komponentenfelder durch eine Kleinstmenge an Logik-Hardware. Es sind weniger Bits im Feld für den Befehlscode des Befehlsregisters nötig.

Technische Informatik I

221

12. RISC-Prozessoren 7. Ein „Pipelining“-Mechanismus, der die Überlagerung von Befehlsausführungen erlaubt. Diese Eigenschaften gilt es also etwas näher zu betrachten. Dies soll am Beispiel des Vaters alles RISC-Architekturen geschehen: Der Berkeley-RISC-Architektur. Besonderes Augenmerk soll dabei auf Befehlsformat, Registerfenster und Pipelining gelegt werden.

12.3.1. Befehlsformat Die Berkeley-RISC-Architektur nutzt ein Drei-Register-Befehlsformat in der in Abbildung 12.1 gezeigten Form. Jedes Operandenfeld erlaubt, eins von 32 internen Registern anzusprechen. 32 Bits OpCode

SCC

Ziel

Quelle 1

IM+8 Nullen

Quelle 2

7 Bits

1 Bit

5 Bits

5 Bits

9 Bits

5 Bits

Abbildung 12.1.: Befehlsformat der Berkeley-RISC-Architektur Die zwei Felder „SCC“ und „IM+8 Nullen“ sind zum Verständnis des RISC-Befehlsformats im Moment uninteressant und werden daher hier ignoriert. Nur soviel: Sie sind daran beteiligt, das Zustandsregister zu aktualisieren beziehungsweise ein Literal zur Verfügung zu stellen. Das RISC-Format erlaubt Befehle in der Form ADD X,Y,Z, wobei X, Y und Z interne Register sind. Da fünf Bits für jedes Operandenfeld zugewiesen sind, verfügt eine RISCArchitektur über 25 = 32 interne Register. Ergänzend muss gesagt werden, dass Berkeleys RISC sogar 138 für Anwender nutzbare Mehrzweckregister hat. Der Grund hierfür liegt in einem technischen Ansatz, der dem Programmierer zu einem bestimmten Zeitpunkt den Blick auf nur eine Teilmenge aller Register erlaubt.

12.3.2. Registerfenster Ein wichtiges Designmerkmal der Berkeley-RISC-Architektur besteht in einer Unterprogrammtechnik, bei der ein Satz ausgewählter Register jeder neuen Prozedur neu zugewiesen wird. Problem: Obwohl nur etwa zwölf Register von jeder aufgerufenen Prozedur benötigt werden, erhöht sich die insgesamt benötigte Registeranzahl durch verschachtelte Prozeduraufrufe, z. B. für Parameterübergabe und lokale Variablen. Man kann sich vorstellen, dass jeder Versuch, eine Menge von Registern für jede neue Prozedur einzusetzen, unpraktisch ist, da der wiederholte Aufruf von verschachtelten Prozeduren eine unbegrenzte Menge an Speicher benötigt. Glücklicherweise zeigt das durchschnittliche Verhalten von Programmen – obwohl Prozeduren bis zu einer willkürlichen Tiefe verschachtelt werden können – kein kritisches Verhalten im Hinblick auf die Prozedurenverschachtelung. In der RISC-Literatur wird oft auf Halbert und Kessler sowie Tamir und Sequin Bezug genommen, die zeigten, dass die meisten Prozeduren im Allgemeinen nicht zu einer sehr großen Tiefe verschachtelt werden. Diese Ergebnisse deuten an, dass es möglich ist, eine relativ geringe Anzahl von lokalen Registermengen für eine Folge von verschachtelten Registern zu verwenden.

222

Technische Informatik I

12.3. Wünschenswerte Eigenschaften einer RISC-Architektur Zeit (Aufrufe/Rücksprünge) t=33

Rücksprung

Aufruf

w=3 Verschachtelungstiefe

Abbildung 12.2.: Verschachtelung von Prozeduren während eines Programms Abbildung 12.2 ist eine graphische Darstellung der Ausführung eines Programms, die die Tiefe der Prozedurverschachtelung veranschaulicht. Diese Abbildung zeigt, dass es im Durchschnitt keine größeren Verschachtelungstiefen gibt, obwohl Prozeduren beliebig verschachtelt werden können. Tiefere Verschachtelung wird die CPU allerdings veranlassen, auf externen Speicherraum zurückzugreifen, um lokale Variablen abzulegen. Wir werden dies im folgenden noch erläutern. Speicherraum, der von Prozeduren benutzt wird, kann im Prinzip in vier Arten eingeteilt werden. • Globaler Raum ist eine Region des logischen Adressraums, der für alle Prozeduren direkt zugreifbar ist. Er wird benutzt, um Konstanten und Daten, die von jedem Punkt des Programms aus benötigt werden, aufzubewahren. Die meisten konventionellen Mikroprozessoren haben nur interne globale Register. • Lokaler Raum ist eine Region des logischen Adressraums, welcher der Prozedur privat zur Verfügung steht. Das heißt, keine andere Prozedur kann von außerhalb auf den lokalen Raum der aktuellen Prozedur zugreifen. Lokaler Raum wird als Arbeitsraum für die aktuelle Prozedur verwendet. • Importierter Parameterraum enthält die Parameter, welche von der aktuellen Prozedur von ihrer Vaterprozedur geerbt wurden. In der RISC-Terminologie werden diese die „hohen Register“ genannt. • Exportierter Parameterraum enthält die Parameter, welche von der aktuellen Prozedur an ihre Kinder übergeben werden. In der RISC-Terminologie werden diese die „unteren Register“ genannt.

12.3.3. Fenster und Parameterübergabe Der recht hohe Anteil von Programmsteuerungsbefehlen in einem Programm erfordert eine entsprechend häufige Parameterübergabe an Prozeduren oder die Rückgabe von Parametern. Eine Reduzierung dieses Aufwands beschleunigt die Programmausführung mit hoher Wahrscheinlichkeit merklich. Die Berkeley-RISC-Architektur löst das Problem der Parameterübergabe durch Einsatz von mehreren sich überlappenden Fenstern. Ein Fenster ist eine Menge von Registern, die für die aktuelle Prozedur sichtbar ist. Abbildung 12.3 illustriert die Struktur der sich überlappenden Fenster einer RISC-Architektur.

Technische Informatik I

223

12. RISC-Prozessoren Fenster i-1

Fenster i

Fenster i+1

R0 R9

Globale Register

R10 i-1 R15 i-1 R16 i-1 R25 i-1 R26 i-1 R31 i-1

R10 i R15 i R16 i R25 i R26 i R31 i

i Fensterzeiger

R10 i+1 R15 i+1

Parameterübergabe von aufrufender Prozedur

R16 i+1 R25 i+1

lokale Variablen

R26 i+1 R31 i+1

Parameterübergabe an nächste Prozedur

Abbildung 12.3.: Struktur von überlappenden Fenstern Nehmen wir an, dass der Prozessor momentan die Fenstermenge i nutzt. Ein Spezialregister, das als Register-Fensterzeiger eingesetzt wird (window pointer), zeigt auf das gerade aktive Fenster. In diesem Fall enthält der Fensterzeiger also den Wert i . Jedes Fenster ist in die beschriebenen vier Teile aufgeteilt: Die Register R 0 –R 9 sind die Menge der globalen Register, die Register R 10 –R 15 werden von der Prozedur genutzt, um Parameter von der Vaterprozedur zu empfangen (und dorthin zurückzuschicken), die Register R 16 –R 25 sind zehn lokale Register, die nicht direkt von anderen Prozeduren angesprochen werden können und die Register R 26 –R 31 werden von der Prozedur benutzt, um Parameter an ein Kind zu übergeben und von dort zu empfangen. Alle Fenster bestehen aus 32 Registern, R 0 –R 31 , und jedes dieser Register kann durch eine RISC-Anweisung adressiert werden, welche fünf Adressbits pro Operandenfeld enthält. Wann immer eine Prozedur von einer Anweisung der Form CALLR Rd, aufgerufen wird, wird der Inhalt des Fensterzeigers um eins erhöht und der aktuelle Wert des Programmzählers wird im Register Rd des neuen Fensters gesichert. Man beachte, dass die Berkeley-RISC-Architektur keinen konventionellen Stack im externen Hauptspeicher nutzt, um Rücksprungadressen zu sichern. Die Adresse innerhalb des Befehls ist das Ziel zu dem hingesprungen wird. Nachdem ein neues Fenster angefordert wurde (in Abbildung 12.3 ist dies das Fenster i ), sieht die neue Prozedur eine andere Registermenge als das vorherige Fenster. Die globalen Register R 0 –R 9 sind eine Ausnahme, da sie allen Fenstern gemeinsam sind. Das Fenster R 10 der Kindprozedur ist identisch mit dem Register R 26 der aufrufenden (Vater-) Prozedur. In diesem Abschnitt werden wir die mnemonischen Symbole der Berkeley-RISC-Architektur wann immer möglich nutzen, wenn wir die Arbeitsweise der RISC-Architektur beschreiben. Eine Anweisung der Form ADD R3,R12,R25 implementiert [R 25 ] ← [R 3 ] + [R 12 ], wobei R 3 aus dem globalen Adressraum des Fensters, R 12 aus seinem Importadressraum vom (oder Exportadressraum zum) Vateradressraum und R 25 innerhalb des lokalen

224

Technische Informatik I

12.3. Wünschenswerte Eigenschaften einer RISC-Architektur Adressraums ist. Man beachte, dass arithmetische oder logische RISC-Anweisungen immer 32-Bit-Werte benutzen (es gibt keine 8-Bit- oder 16-Bit-Anweisungen). RAM

wp=0 wp=1 wp=2 wp=3 wp=4 wp=5 wp=6 wp=7

R0 .. R9 R10 .. R15 R16 .. R25 R26 .. R31 R32 .. R41 R42 .. R47 R48 .. R57 R58 .. R63 R64 .. R73 R74 .. R79 R80 .. R89

Globaler Raum Importierter Parameterraum Lokaler Raum Exportierter Parameterraum

R90 .. R95 R96 .. R105 R106 .. R111 R112 .. R121 R122 .. R127 R128 .. R137 R10 .. R15

Abbildung 12.4.: Erweitertes Fenstermodell Die vollständige Struktur des RISC-Fenster-Systems wird in Abbildung 12.4 dargestellt. Auf der linken Seite des Diagrams befindet sich das eigentliche Registerfeld, welches alle allgemeinen Register auf dem Chip darstellt. Die acht Spalten, welche den Fenstern 0–7 zugeordnet sind, zeigen, wie jedes Fenster auf das physische Speichermodell abgebildet wird und wie die überlappenden Regionen organisiert sind. Man beachte, dass die Fenster ringförmig angeordnet sind, sodass Fenster 0 auf Fenster 7 folgt. Die Gesamtanzahl der benötigten Register errechnet sich wie folgt: 10 + + =

8 · 10 8·6

138

globale Register lokale Register Register zur Parameterübergabe Register insgesamt

12.3.3.1. Fensterüberlauf und -unterlauf Unglücklicherweise sind die Platzressourcen auf einem Prozessor endlich und, im Falle eines Berkeley-RISC-Prozessors, die Register auf acht Fenster begrenzt. Wenn Prozeduren tiefer als acht Ebenen verschachtelt werden, tritt der sogenannte Fensterüberlauf auf und es gibt kein neues Fenster mehr für den nächsten Prozeduraufruf. Wenn solch ein Überlauf stattfindet, bleibt als einziger Ausweg, externen Speicher wie Cache oder Hauptspeicher einzusetzen, um die Überlaufdaten aufzunehmen. In der Praxis wird dabei das älteste Fenster gesichert, nicht das durch die aufgerufene Prozedur neu geschaffene Fenster. Wenn die Anzahl der Prozedurrücksprünge minus der Anzahl der Prozeduraufrufe den Wert 8 überschreitet, tritt Fensterunterlauf auf. Fensterunterlauf ist das Gegenteil zum

Technische Informatik I

225

12. RISC-Prozessoren Fensterüberlauf und muss behandelt werden, indem das zuletzt in den Hauptspeicher gesicherte Fenster in ein Registerfenster wiedereingespeichert wird.

12.3.4. RISC-Architektur und Pipelining Wieder einmal lohnt es sich zu bemerken, dass gewöhnliche Prozessoren mit einem komplexen Befehlssatz ihre Effizienz der Komplexität opfern. Das heißt, dass ihnen die Fähigkeit mitgegeben wurde, sehr komplexe Anweisungen zu interpretieren, sie dies aber auf Kosten einer effizienten Nutzung aller Einrichtungen auf dem Chip selbst tut. Wie bereits erwähnt, kann man die Effizienz anhand des Prozessordurchsatzes messen. In diesem Abschnitt soll gezeigt werden, wie die geregelte Struktur des RISC eine parallele Ausführung einer gewissen Menge von internen Operationen erlaubt. Befehl holen

Befehl dekodieren

Operanden holen

Ausführen

Operanden speichern

Ein Befehl

Abbildung 12.5.: Phasen in einem Maschinenzyklus Abbildung 12.5 zeigt einen Maschinenzyklus eines hypothetischen, einfachen Mikroprozessors, der eine Anweisung der Form ADD P (d. h. [A] ← [A] + [M (P )]) ausführt, wobei A ein Register/Akkumulator auf dem Chip und P eine allgemeine Speicherstelle ist. Die Anweisung wird in die folgenden fünf Phasen aufgeteilt: • Holen der Anweisung (instruction fetch). Lies die Anweisung aus dem Systemspeicher und erhöhe den Programmzähler. • Dekodieren der Anweisung (instruction decode). Dekodiere die Anweisung, die während der vorigen Phase aus dem Speicher gelesen wurde. Die Beschaffenheit dieser Dekodierungsphase hängt sehr stark von der Komplexität der Anweisungskodierung ab. Eine normal kodierte Anweisung kann innerhalb weniger Nanosekunden dekodiert sein und nur zwei Gatterebenen durchlaufen, während ein komplexes Befehlsformat ein Durchsuchen in ROM Tabellen erfordert, um die Dekodierung zu ermöglichen. • Holen des Operanden (operand fetch). Der Operand, der durch die Anweisung spezifiziert wird, wird aus dem Hauptspeicher oder einem Register gelesen und in die CPU geladen. • Ausführen (execute). Die durch die Anweisung bestimmte Operation wird ausgeführt. • Speichern der Operanden (operand store). Das Ergebnis der Ausführungsphase wird unter der Zieladresse abgespeichert. Das kann sowohl ein Register auf dem Chip als auch eine Adresse im externen Speicher sein. Wenn eine Anweisung von der Phase Holen des Operanden in die Phase Ausführen übergeht, so wird dies Anweisungserteilung genannt. Jede der obigen fünf Phasen kann eine bestimmte Zeit benötigen (obwohl die gemessene Zeit im Normalfall immer ein ganzzahliges Vielfaches der Periode des Systemtaktes wäre). Diese Verzögerung wird Anweisungslatenz oder Befehlslatenz genannt. Einige

226

Technische Informatik I

12.3. Wünschenswerte Eigenschaften einer RISC-Architektur Anweisungen können weniger als fünf Phasen benötigen. Ein Beispiel dafür ist eine Vergleichsoperation wie CMP R1,R2, welche R 1 und R 2 vergleicht, indem R 1 von R 2 subtrahiert wird und anschließend die Zustandsbits im Prozessor entsprechend gesetzt werden. Hierbei ist deswegen keine Phase zum Speichern des Operanden notwendig. Die Ineffizienz, die sich aus dem Arrangement aus Abbildung 12.5 ergibt, wird somit sofort ersichtlich. Man betrachte die Ausführungsphase der Befehlsinterpretation. Diese Phase kann ein Fünftel eines Befehlszyklus dauern, wodurch die eigentliche Ausführungseinheit (die ALU) in den verbleibenden 80 % der Zeit im Leerlauf verbleibt. Dasselbe trifft auf alle anderen funktionalen Einheiten des Prozessors zu, welche also ebenfalls 80 % der Zeit im Leerlauf sind. Eine Technik, die Pipelining genannt wird 1 , kann angewandt werden, um die effektive Geschwindigkeit des Prozessors zu erhöhen, indem man die verschiedenen Phasen (auch Stadien) der Ausführung zeitlich überlappt. Der Befehlsdurchsatz (Anzahl der Befehle pro Sekunde) wird somit gesteigert. Den Übergang zwischen zwei Stadien nennt man Fließbandschalter oder Pipeline-Switch. Die Pipeline ist mehrstufig konstruiert. Die Durchlaufzeit (Latenz) der Pipeline beträgt n Taktzyklen, d.h. die Anzahl der Stadien ist gleich n . Im einfachsten aller Fälle kann ein solcher Prozessor Anweisung i ausführen, während er bereits Anweisung i + 1 aus dem Speicher holt. i

OF

IF

i+1

E

OS

IF

OF

E

OS

Abbildung 12.6.: Zweistufiges Pipelining. IF: instruction fetch, OF: operand fetch, E: execute, OS: operand store Die Art, in der ein RISC-Prozessor Pipelining implementiert, wird in Abbildung 12.6 gezeigt. Der RISC-Prozessor überlappt das Holen der Anweisung aus dem externen Speicher mit drei internen Phasen: Holen der Operanden, Ausführen und Sichern der Operanden. Man beachte, dass die drei internen Phasen ungefähr genauso lange dauern wie das Holen des Befehls, da diese Operationen in der CPU selbst stattfinden und die Operanden aus CPU-internen Registern geholt und darin auch wieder zurückgespeichert werden. Weiterhin existiert mit RISC ein sehr einfaches und regelmäßiges Befehlsformat, sodass die Dekodierphase nicht nötig ist. Die Pipeline-Technik ist nicht frei von Problemen. Wir werden uns hier einen Überblick über die beiden grundlegenden Probleme verschaffen, welche sich den Entwicklern von pipelinebasierten Systemen in den Weg stellen: Pipeline-Blasen, welche durch Sprunganweisungen hervorgerufen werden, und Datenabhängigkeiten. Eine Pipeline ist eine geordnete Struktur, die sich durch eine Regelmäßigkeit auszeichnet. Zu jedem Zeitpunkt der Ausführung eines Programms enthält die Pipeline Teile von zwei oder mehr Befehlen in den verschiedenen Phasen ihrer Ausführung. In Abbildung 12.7 wird eine Folge von Befehlen durch einen Pipeline-Prozessor ausgeführt. Wenn der Prozessor eine Sprunganweisung ausführen soll, ist er gezwungen, den Programmzähler 1 in Ermangelung eines entsprechenden deutschen Wortes findet man in deutschsprachiger Literatur oft

Begriffe wie Befehlsverknüpfung oder Parallelverarbeitung

Technische Informatik I

227

12. RISC-Prozessoren mit einen neuem Wert zu laden. Das bedeutet, dass all die Arbeit, die durch die Pipeline bisher geleistet wurde, verworfen werden muss: Die Anweisungen, die direkt dem Verzweigungsbefehl folgen, werden doch nicht ausgeführt. i-1

IF

OF

E

OS

i

IF Sprung

OF

E

OS

i+1

IF

OF

E

OS

i+2

IF

OF

E

OS

N

IF

OF

E

OS

N+1

IF

OF

E

OS

N

N+1

N+2

N+3

N+4

Zeitpunkt, zu dem ein neuer Befehl startet

Blase in der Pipeline

Pipeline

i-1

i

Blase Blase

Abbildung 12.7.: Blase in einem Pipeline-Prozessor (vierstufig) Wenn Daten in einer Pipeline zurückgewiesen werden oder die Pipeline durch die Ausführung von Leerläufen aufgehalten wird, spricht man vom Auftreten einer Blase (bubble). Natürlich müssen bei einem Sprung umso mehr Befehle verworfen werden, je länger die Pipeline war. Wie bereits gezeigt, machen Programmsteuerungsbefehle wie Sprünge einen großen Teil eines Programms aus. Folglich muss jeder reale Prozessor, der Pipelining nutzt, das Problem der durch diese Art von Befehlen erzeugten Blasen überwinden. Die Berkeley-RISCArchitektur reduziert diesen Effekt von Blasen, indem die Befehle, die auf einen Sprungbefehl folgen, gar nicht erst verworfen werden. Das heißt, die Anweisung, die auf einen Sprung folgt, wird immer ausgeführt. Man überlege sich den Effekt bei der dargestellten Folge von Befehlen: ADD R1,R2,R3 [R 3 ] ← [R 1 ] + [R 2 ] JMPX N [PC] ← [N ] ADD R3,R4,R5 [R 5 ] ← [R 3 ] + [R 4 ] ADD R7,R8,R9 Der zweite Befehel (JMPX N) führt zu einem Sprung zu N, der ADD-Befehl danach wird aber auch noch ausgeführt. Erst der vierte Befehl (ADD R7,R8,R9) wird aufgrund des Sprungs nicht ausgeführt. Der Prozessor berechnet [R 5 ] ← [R 3 ]+[R 4 ], bevor er den Sprung ausführt. Diese Folge von Befehlen muss einem „normalen“ Assemblerprogrammierer äußerst merkwürdig vorkommen, da er es nicht gewohnt ist, solche Anweisungen nach einem Sprung ausgeführt zu sehen. Unglücklicherweise ist es nicht immer möglich, ein Programm so zu strukturieren, dass ein „nützlicher“ Befehl sofort einem Sprung folgt. Wann immer diese Situation eintritt, muss der Compiler einen Leerbefehl (NOP – No operation) nach dem Sprung einführen

228

Technische Informatik I

12.3. Wünschenswerte Eigenschaften einer RISC-Architektur und die Unvermeidbarkeit einer Blase akzeptieren. Die RISC-Literatur bezeichnet diesen Mechanismus als den verzögerten Sprung oder die Spring-und-Ausführ-Technik. Abbildung 12.8 zeigt, wie ein RISC-Prozessor einen verzögerten Sprung umsetzt. Man beachte, dass der Sprung in Abbildung 12.8 ein berechneter Sprung ist, bei dem die Zieladresse während der Ausführungsphase des Befehlszyklus berechnet wird. Hole i-1

OF/E

OS

Hole i

OF/E

OS

(berechne N)

Hole i+1

OF/Ei+1

OSi+1

Hole N

OF/E

OS

Abbildung 12.8.: Verzögerter Sprung in einem RISC-Prozessor Die Ausführungszeiten von Programmen in einer Pipeline können vorhergesagt werden. Im Idealfall: (Tiefe der Pipeline (Anzahl der Stadien) + Anzahl der Befehle - 1) · Taktzyklusdauer

Im allgemeinen Fall: (Tiefe der Pipeline (Anzahl der Stadien) + Gesamtzyklenanzahl - 1) · Taktzyklusdauer

Dabei gilt im Idealfall, dass die Anzahl der Zyklen zum erteilen aller Anweisungen gleich der Gesamtanzahl der Anweisungen ist. Ein anderes Problem, welches durch Pipelining hervorgerufen wird, ist Datenabhängigkeit, die bei bestimmten Folgen von Befehlen Ärger verursacht, da die aktuelle Operation ein Ergebnis von der vorherigen Operation benötigt und diese vorherige Operation die Pipeline noch nicht verlassen hat. Abbildung 12.9 zeigt, wie Datenabhängigkeit auftritt. i

IF

OF

E

OS

ADD A,B, Tmp1

Hole A,B

Addiere A,B

Tmp1=A+B

i+1

IF SUB Tmp1,C,Tmp2

Blase

i+2 Pipeline-Blase, weil Operand Tmp1 aus vorherigem Befehl nicht verfügbar ist

OF

E

OS

Hole Tmp1,C

Subtrahiere Tmp1,C

Tmp2= Tmp1-C

IF ADD Tmp1,C,Tmp2

Blase

OF

E

Hole Tmp1,Tmp2

Addiere Tmp1,Tmp2

Abbildung 12.9.: Datenabhängigkeiten Man stelle sich vor, dass ein Programmierer die scheinbar harmlose Berechnung X := (A + B ) AND (A + B − C ) durchführen will. In der Annahme, dass A, B, C und X und zwei temporäre Variable Temp1 und Temp2 sich in Registern im aktuellen Fenster befinden, kann man folgende Befehlsfolge schreiben.

Technische Informatik I

229

12. RISC-Prozessoren ADD SUB AND

A,B,Temp1 Temp1,C,Temp2 Temp1,Temp2,X

[Temp1] ← [A] + [B ] [Temp2] ← [Temp1] − [C ] [X ] ← [Temp1] AND [Temp2]

Der Befehl i +1 in Abbildung 12.9 beginnt seine Ausführung mit dem Holen der Operanden der vorherigen Anweisung. Der Befehl i + 1 kann jedoch nicht zur nächsten Phase (dem Holen der Operanden) voranschreiten, weil der benötigte Operand für zwei weitere Taktzyklen nicht zurückgeschrieben wird. Folglich tritt eine Blase in der Pipeline auf, während der Befehl i + 1 auf ihre Daten wartet. In ähnlicher Weise muss der logische AND-Befehl eine Blase erzeugen, weil sie ebenfalls die Ergebnisse einer vorherigen Operation erwartet, die sich in der Pipeline befindet. i

IF ADD R1,R2,R3

OF

i+1

E

OS

IF ADD R4,R5,R6

OF

E

OS

IF ADD R3,R4,R7

i+2

OF

i+3

E

IF ADD R7,R1,R8

OS

OF

E

OS

R7 wird intern übergeben

Abbildung 12.10.: Interne Übergabe Abbildung 12.10 zeigt eine Technik, die Interne Übergabe oder auch Bypassing genannt wird und entwickelt wurde, um den Effekt der Datenabhängigkeit zu überwinden. Das vorliegende Beispiel nutzt eine dreistufige Pipeline. Die folgende Sequenz von Befehlen wird ausgeführt: 1. 2. 3. 4.

ADD ADD ADD ADD

R1,R2,R3 R4,R5,R6 R3,R4,R7 R7,R1,R8

[R 3 ] ← [R 1 ] + [R 2 ] [R 6 ] ← [R 4 ] + [R 5 ] [R 7 ] ← [R 3 ] + [R 4 ] [R 8 ] ← [R 1 ] + [R 7 ]

In diesem Beispiel benutzt der dritte Befehl einen Operanden, welcher aus dem ersten Befehl erzeugt wird (der Inhalt von R 3 ). Durch den dazwischen liegenden zweiten Befehl besteht genügend Zeit, sodass der durch den Befehl in Zeile 1 erzeugte Operand in das Register gespeichert werden kann, bevor er als Quelloperand vom Befehl in Zeile 3 gelesen wird. Befehl 3 erzeugt einen Zieloperanden R 7 , der als Quelloperand vom darauffolgenden Befehl benötigt wird. Wenn der Prozessor den Quelloperanden, der von Befehl 4 angefordert wurde, nicht durch interne Weiterleitung erhielte, würde er den alten Wert von R 7 bekommen. Allerdings können wir feststellen: Durch die interne Übergabe überträgt der Prozessor R 7 von der Ausführung des Befehls gleich zur Ausführung von Befehl 4. Das bedeutet, das

230

Technische Informatik I

12.3. Wünschenswerte Eigenschaften einer RISC-Architektur Resultat der ALU wird gleich als Operand wieder hineingeleitet (siehe Abbildung 12.10), z. B. durch direkte Übergabe vom ALU-Outputregister in das ALU-Inputregister.

12.3.5. Zugriff auf den externen Speicher In ihrer Jagd nach Effizienz haben die Entwickler der Berkeley-RISC-Architektur die Möglichkeiten für den Zugriff auf externen Speicher streng beschränkt. Gewöhnliche CISC-Prozessoren haben eine Fülle von Adressierungsarten. Zum Beispiel implementiert ein CISC wie der 68020 einen Befehl wie ADD D0(A5)+ , der den Inhalt von D 0 zum Inhalt der Speicheradresse addiert, auf die A 5 zeigt, danach wird der Wert von A 5 inkrementiert. Die Berkeley-RISC-Architektur erlaubt lediglich zwei Formen der Referenzierung auf externen Speicher: Laden und Speichern. Folglich beziehen sich alle arithmetischen und logischen Operationen, die vom RISC ausgeführt werden, nur auf Quell- und Zieloperanden in Registern. Ähnlich verhält es sich mit der begrenzten Anzahl von Adressierungsarten, mit denen auf einen Operanden im Hauptspeicher zugegriffen werden kann. Der Grund für diese strengen Restriktionen ist nicht schwer zu finden. Bevor wir uns den Berkeley-RISC im Detail ansehen, besprechen wir kurz die allgemeinen Prinzipien für die Lade- und Speicherungsbefehle in einer RISC-Umgebung. Die Lade-Operation der Form LOAD(Rx)S2,Rd hat den Effekt [Rd ] ← [M ([R x ] + S2)]. Abbildung 12.11 zeigt die Folge der Aktionen, die durch diesen Befehl benötigt werden. Während der Holphase wird das Register Rx aus dem Registerfeld gelesen und benutzt, um die effektive Adresse des Operanden in der Ausführungsphase zu berechnen. Nun jedoch, nachdem wir die Ausführphase durchgeführt haben, können wir nicht zur Speicherung des Operanden voranschreiten, da der Operand noch nicht aus den Hauptspeicher geholt wurde. Deswegen muss auf den Hauptspeicher zugegriffen werden, um den Operanden zu lesen, und der Operand muss im Zielregister Rd abgelegt werden. i

IF

i+1

OF

E

OS

IF

OF

E

IF

OF

i+2

Mem

OS

E

OS

OF

E

OS

IF

OF

E

Blase i+3

IF

i+4

OS

Abbildung 12.11.: LOAD-Operation eines RISC-Prozessors Der Berkeley-RISC-Prozessor implementiert zwei grundlegende Adressierungsarten: Entweder über einen Index oder relativ zum Programmzähler. Alle anderen Adressierungsarten werden auf diese zwei Primitiven zurückgeführt. Die effektive Adresse EA im Index-Modus ergibt sich aus einem Indexregister R x ist (eins der 32 Allzweckregister, auf die durch die aktuelle Prozedur zugegriffen werden kann) und dem Adressabstand (Offset) S2 (Source2). E A = [R x ] + S2

Technische Informatik I

231

12. RISC-Prozessoren Für den Adressabstand kann auch ein Allzweckregister oder eine 13-Bit-Konstante verwendet werden (siehe Abbildung 12.12). Die effektive Adresse im Modus relativ zum Programmzähler PC ergibt sich analog. E A = [PC ] + S2

Diese Adressierungsarten erlauben sehr variable Arbeitsweisen: Kein Zeiger, ein Zeiger oder zwei Zeiger und ein konstantes Offset. Wem sich die Frage stellt, wie man den Adressierungsmodus ohne einen Index (d. h. einen Adresszeiger) nutzt, erinnere sich, dass die globale Registermenge immer die Konstante Null enthält. Man beachte, dass es einen Unterschied zwischen den Adressierungsarten gibt, die für die Lade- und Speicherbefehle erlaubt sind. Die Ladeanweisung erlaubt, dass die zweite Quelle S2 sowohl ein fixer Wert oder ein zweites Register sein kann, während bei der Speicherung S2 lediglich eine 13-Bit-Konstante sein darf. 13 Source2 ist ein Register Kurzes Immediate-Format

Op-Code

SCC

Op-Code

7 Bits

SCC

12

18 RS1

13 IMM

4

nicht verwendet

0 RS2

0

12

1

Ziel

13-Bit-Konstante

12

0 Kurze Quelle

5 Bits 18

24

31

13

Source2 ist ein 13-BitLiteral 24

31

0

0

Ziel

Immediate-Wert

5 Bits

19 Bits

Langes Immediate-Format

Abbildung 12.12.: Adressformate eines RISC-Prozessors Der RISC-Prozessor hat zwei grundlegende Befehlsformate, wie sie in Abbildung 12.12 definiert sind, obwohl es leichte Variationen dieser Formate gibt. Das Format für kurze Konstanten bietet eine 5-Bit-Zieladresse, einen 5-Bit- und einen 13-Bit-Operanden. Das Format hat zwei Varianten: Bei der ersten wird ein 13-Bit-Literal für den zweiten Operanden angegeben und bei der anderen wird ein 5-Bit-Quell-Register spezifiziert. Bit 13 wird benutzt, um anzugeben, ob der zweite Operand ein 13-Bit-Literal oder ein 5-BitRegisterzeiger ist. Das Format für lange Konstanten bietet einen 19-Bit-Quelloperanden, indem die zwei Quelloperandenfelder zusammengefasst werden. Konstantenfelder mit 13 und 19 Bits mögen auf den ersten Blick vielleicht etwas seltsam aussehen. Da jedoch 13 + 19 = 32, erlaubt RISC, mit nur zwei Befehlen einen vollständigen 32-Bit-Wert in ein Fensterregister zu laden. Ein typischer Mikroprozessor würde die gleiche Anzahl von Befehlsbits benötigen, um die gleiche Aktion auszuführen (d. h. ein 32-Bit-Befehlscode gefolgt von einem 32-Bit-Literal). Der RISC kann also 32-Bit-Konstanten genauso effizient laden wie viele Mikroprozessoren. Bedingte Befehle benötigen keine Zieladresse, daher werden die fünf Bits 19–23, mit denen normalerweise das Zielregister bestimmt wird, zur Angabe der Bedingung (eine von 16 verschiedenen, da Bit 23 nicht von bedingten Befehlen genutzt wird) verwendet.

232

Technische Informatik I

13. Realisierung einer leistungsfähigen Speicherarchitektur 13.1. Einleitung Seit den frühesten Tagen des Rechnerzeitalters melden Programmierer Bedarf an unendlichen Mengen von schnellem Speicher an. Die Themen, mit denen wir uns in diesem Kapitel beschäftigen werden, befassen sich alle damit, wie man zur Hilfe des Programmierers die Illusion eines solchen unendlichen Speichers schafft. Bevor wir uns genau ansehen, wie dieser Speicher vorgetäuscht wird, betrachten wir eine Analogie, welche die von uns genutzten wichtigen Prinzipen und Mechanismen veranschaulicht. Stellen Sie sich vor, Sie müssten als Student eine Seminararbeit über die wichtigen historischen Entwicklungen der Rechnerhardware anfertigen. Sie sitzen mit einer Sammlung Bücher, die sie aus den verschiedenen Regalen entnommen haben und nun untersuchen, an einem Schreibtisch in der Universitätsbibliothek. Sie finden heraus, dass viele der historischen Maschinen, über die Sie schreiben müssen, in den Büchern vor Ihnen beschrieben werden, aber es gibt keinerlei Informationen über die legendäre Zuse II. Sie müssen zu den Regalen gehen und sich ein zusätzliches Buch in den Regalen suchen. Sie finden eine Abhandlung über die frühen deutschen Rechner, die auch die Rechenanlage Zuse II umfasst. Nachdem Sie einmal eine gute Auswahl von Büchern getroffen haben, werden mit großer Wahrscheinlichkeit alle Themen, die sie brauchen, in ihnen abgehandelt, sodass Sie nicht mehr zu den Regalen gehen müssen. Dadurch, dass Sie mehrere Bücher vor sich haben, sparen Sie Zeit, im Vergleich dazu, lediglich ein Buch vor sich zu haben und jedes Mal zu den Regalen gehen zu müssen, um es gegen ein anderes Buch zu tauschen. Die gleichen Prinzipien erlauben es uns, die Illusion eines großen Speichers zu schaffen, auf den wir genauso schnell wie auf einen sehr kleinen Speicher zugreifen können (Abbildung 13.1). Genau wie Sie nicht alle Bücher in der Bibliothek mit gleicher Geschwindigkeit auf einmal benötigen, braucht ein Programm nicht alle Befehle und Daten mit der gleichen Wahrscheinlichkeit gleichzeitig. Andernfalls wäre es nicht möglich, die meisten Speicherzugriffe schnell zu machen und dennoch eine große Menge Speicher im System zu haben – so wie es Ihnen nicht möglich wäre, alle Bücher der Bibliothek auf dem Schreibtisch zu haben und trotzdem eine Information schnell zu finden. Dieses Lokalitätsprinzip liegt sowohl der Arbeit in der Bibliothek als auch der Arbeitsweise von Programmen zu Grunde. Das Prinzip der Lokalität sagt aus, dass Programme zu jedem Zeitpunkt nur auf einen relativ geringen Teil ihrer Adressräume zugreifen, genauso wie man in der Bibliothek nur einen kleinen Teil der verfügbaren Büchersammlung braucht. Es gibt zwei Arten von Lokalität: • Zeitliche Lokalität: Wenn Sie kürzlich ein Buch zum Schreibtisch gebracht haben, werden Sie sicherlich bald wieder hinein schauen müssen. Wenn eine Adresse referiert wird, ist die Wahrscheinlichkeit hoch, bald wieder darauf zu verweisen.

233

13. Realisierung einer leistungsfähigen Speicherarchitektur CPU

Ebene 1 Ebene 2

...

Ebene n

Abbildung 13.1.: Struktur einer Speicherhierarchie: Je weiter eine Speicherebene von der CPU entfernt ist, desto größer ist die Zugriffszeit • Räumliche Lokalität: Wenn Sie beispielsweise über frühe deutscher Rechner recherchieren, werden Sie neben einem sehr speziellen Buch auch weitere relevante Literatur entleihen und auf Ihrem Desktop liegen haben. Somit ist es wahrscheinlich, dass Sie neben dem bevorzugten Buch auch häufig auf daneben liegende, relevante Exemplare zugreifen werden. Wir werden später noch sehen, wie räumliche Lokalität in Speicherhierarchien genutzt wird. Wenn auf eine Adresse verwiesen wird, ist die Wahrscheinlichkeit groß, dass dies bald mit einer benachbarten Adresse geschieht. So wie der Zugriff auf Bücher auf dem Schreibtisch von Natur aus Lokalität an den Tag legt, ergibt sich Lokalität in Programmen durch einfache und natürliche Programmstrukturen. Zum Beispiel enthalten die meisten Programme Schleifen, sodass wahrscheinlich wiederholt auf Befehle und Daten zugegriffen wird, was eine große Menge zeitlicher Lokalität aufweist. Da Befehle normalerweise nacheinander abgearbeitet werden, besitzen Programmen auch eine hohe räumliche Lokalität. Zugriffe auf Daten legen auch eine natürliche räumliche Lokalität an den Tag. Zum Beispiel haben Zugriffe auf Elemente eines Feldes oder eine Struktur von Natur aus einen hohen Grad räumlicher Lokalität. Wir ziehen einen Vorteil aus dem Lokalitätsprinzip, indem wir den Speicher eines Rechners als Speicherhierarchie implementieren. Eine Speicherhierarchie besteht aus mehreren Speicherebenen mit verschiedenen Geschwindigkeiten und Größen. Die schnellsten Speicher liegen in direkter Nähe zur CPU, sind auch die teuersten und folglich auch die mit der geringeren Kapazität. Heutzutage kommen primär drei Technologien beim Bau von Speicherhierarchien zum Einsatz. Hauptspeicher wird aus DRAM gefertigt, während die der CPU näherstehenden Ebenen (Caches) SRAM benutzen. DRAM kostet pro Bit weniger als SRAM, ist er wesentlich langsamer. Die Preisdifferenz ergibt sich dadurch, dass DRAM erheblich weniger Platz pro Speicherbit verbraucht und somit mehr Kapazität für die gleiche Menge Silizium bietet. Der Geschwindigkeitsunterschied ergibt sich aus den Faktoren, die im vorherigen Kapitel beschrieben wurden. Die letzte Technologie, die zur Fertigung

234

Technische Informatik I

13.1. Einleitung der größten und langsamsten Ebene der Speicherhierarchie verwendet wird, sind Magnetplatten. Die Zugriffszeiten und Preise pro Bit unterscheiden sich bei den genannten Technologien stark, wie die Tabelle 13.1 zeigt. Technologie SRAM DRAM Festplatten

Typische Zugriffszeiten 5–25 ns 50–120 ns 10–20 Millionen ns

$/Megabyte 100–250 5–10 0,10–0,20

Tabelle 13.1.: Zugriffszeiten und Kosten verschiedener Speicherarten (Typische Werte für 1997) Aufgrund dieser Preis- und Geschwindigkeitsunterschiede ist es vorteilhaft, Speicher als eine Hierarchie von Ebenen zu bauen, mit dem schnelleren Speicher dicht beim Prozessor und dem langsameren, kostengünstigeren Speicher darunter, wie in Abbildung 13.2 dargestellt. Das Ziel ist, dem Nutzer so viel Speicher wie verfügbar in der kostengünstigsten Technologie zu bieten, während der Zugriff mit der Geschwindigkeit des schnellsten Speichers erfolgt. CPU Geschwindigkeit

Größe

Kosten ($/Bit)

am schnellsten

am kleinsten

am höchsten

am größten

am niedrigsten

Cache

Hauptspeicher

Festplatten

Wechselmedien am langsamsten

Abbildung 13.2.: konkrete Speicherhierarchie Das Speichersystem ist hierarchisch angeordnet: Eine Ebene, die dichter am Prozessor ist, ist eine Untermenge jeder weiteren, darunter liegenden Ebene und sämtliche Daten werden auf der untersten Ebene gespeichert. Zum Vergleich dazu sind die Bücher auf dem Schreibtisch eine Untermenge der Bibliothek, in der man arbeitet, welcher wiederum eine Untermenge aller Bibliotheken der Hochschule ist. Weiterhin braucht man für den Zugriff auf die verschiedenen Ebenen fortwährend mehr Zeit, genauso wie es beim Aufsuchen der verschiedenen Bibliotheken der Fall ist. Eine Speicherhierarchie kann aus mehreren Ebenen bestehen, jedoch werden Daten immer nur zwischen zwei benachbarten Ebenen gleichzeitig bewegt, sodass wir unsere Aufmerksamkeit auf nur zwei Ebenen zu richten brauchen. Die obere Ebene, die näher am Prozessor ist, ist kleiner, schneller und teurer. Die kleinste Einheit der Information, die in der Zwei-Ebenen-Hierarchie entweder vorhanden oder nicht vorhanden sein kann,

Technische Informatik I

235

13. Realisierung einer leistungsfähigen Speicherarchitektur nennt man Block, wie in Abbildung 13.3 gezeigt. Bei der Bibliothek-Metapher ist der Informationsblock ein einzelnes Buch.

CPU

Datentransfer

Abbildung 13.3.: Zwei-Ebenen-Hierarchie Wenn Daten vom Prozessor angefordert werden, die in einem Block der oberen Ebene auftreten, nennt man dies Treffer (hit) (analog zum Finden einer Information in einem der Bücher auf dem Schreibtisch). Wenn die Daten nicht auf der oberen Ebene gefunden werden, nennt man dies einen Zugriffsfehler (miss). In diesem Fall wird auf die untere Ebene der Hierarchie zugegriffen, um den Block zu lesen, der die Daten enthält. Setzt man die Analogie fort, entspricht dies einem Verlassen des Schreibtischs, um aus dem Regal ein Buch mit der entsprechenden Information zu suchen. Die Trefferrate (hit rate) ist der Anteil von Speicherzugriffen, welcher in der oberen Ebene stattfindet, im Verhältnis zu allen Speicherzugriffen. Dieser Wert wird häufig als Maß für die Leistung der Speicherhierarchie verwendet. Die Fehlerrate (miss rate) ist der Teil der Speicherzugriffe, der nicht in der oberen Ebene zum Ziel führt (1 − Trefferrate). Da Leistungsgewinn der Hauptgrund für eine Speicherhierarchie darstellt, ist die Zeit für Treffer und Fehler von großer Wichtigkeit. Die Trefferzeit (hit time) ist die Zeit, um auf die obere Ebene der Speicherhierarchie zuzugreifen. Sie schließt die Zeit zur Bestimmung von Treffer oder Fehler mit ein. Das entspricht der Zeit, die zum Durchsuchen der Bücher auf dem Schreibtisch nötig ist. Die Fehlerstrafe (miss penalty) ist die Zeit für das Ersetzen eines Blockes in der oberen Ebene mit dem entsprechenden Block der unteren Ebene plus der Zeit, um diesen Block an den Prozessor auszuliefern (die Zeit, um ein anderes Buch aus dem Regal zu holen und es auf den Schreibtisch zu legen). Da die obere Ebene kleiner und aus schnelleren Speicherkomponenten gebaut ist, wird die Trefferzeit viel kleiner sein als die Zeit, die für den Zugriff auf die nächste Ebene der Hierarchie benötigt wird, was ja den Hauptanteil der Fehlerstrafe ausmacht. Die Zeit, um die Bücher auf dem Schreibtisch zu durchsuchen, ist viel kleiner als die Zeit, um aufzustehen und ein Buch in den Regalen zu suchen. Wie wir in diesem Kapitel sehen werden, betreffen die Konzepte für den Bau von Speicherhierarchien viele weitere Aspekte eines Rechners, inklusive die Art und Weise, wie das Betriebssystem Speicher und E/A verwaltet, wie Compiler Code erzeugen und wie Anwendungen die Maschine benutzen. Da alle Programme viel Zeit mit Speicherzugriffen verbringen, ist das Speichersystem natürlich und notwendigerweise ein Hauptfaktor

236

Technische Informatik I

13.2. Cache-Struktur und Arbeitsweise bei der Bestimmung von Leistung. Dass man sich beim Erreichen von Leistung auf Speicherhierarchien verlässt, bedeutet, dass Programmierer, die den Speicher lange Zeit als ebenes Gerät mit wahlfreiem Zugriff betrachteten, nun verstehen, wie Speicherhierarchien arbeiten, um Leistung herauszuholen.

13.2. Cache-Struktur und Arbeitsweise Da Speichersysteme für die Leistung so wichtig sind, haben Rechnerentwickler diesen Systemen viel Aufmerksamkeit gewidmet und raffinierte Mechanismen geschaffen, die die Leistung eines Speichersystems verbessern. In diesem Kapitel werden wir uns mit den wichtigen konzeptionellen Ideen beschäftigen. In unserem Bibliotheksbeispiel fungierte der Schreibtisch als Cache (Zwischenspeicher), einem sicheren Platz, um Daten (Bücher) zu sichern, die wir untersuchen sollten. Cache wurde im ersten kommerziellen Rechner, der so eine zusätzliche Ebene besaß, als offizielle Bezeichnung für die Speicherebene zwischen dem Prozessor und dem Hauptspeicher gewählt. Obwohl der Begriff Cache auch weiterhin hauptsächlich dafür verwendet wird, bezeichnet er heutzutage auch jede Art von Datenspeicherung, die Vorteil aus der Lokalität von Zugriffen bezieht. Die ersten Cachespeicher erschienen in Rechnern für die Forschung in den frühen Sechzigern des vergangenen Jahrhunderts, in industriellen Rechnern erst später im gleichen Jahrzehnt. Heute enthält nahezu jede Allzweckmaschine (von der schnellsten bis zur langsamsten) einen Cache.

13.2.1. Direct-mapped-Cache Zunächst werden wir uns einen sehr einfachen Cache ansehen, in dem die Prozessoranforderungen jeweils nur ein Wort umfassen und die Blöcke ebenso nur aus einzelnen Wörtern bestehen. Abbildung 13.4 zeigt einen solchen einfachen Cache, bevor und nachdem ein Datum angefordert wurde, welches sich anfangs nicht im Cache befand. Vor der Anforderung enthält der Cache eine Reihe von kürzlich benötigten Referenzen X 1 , X 2 , . . . , X n−1 und der Prozessor fordert ein Wort X n an, welches sich nicht im Cache befindet. Die Anforderung endet in einem Fehler und das Wort X n wird vom Hauptspeicher in den Cache gebracht. X4 X1 X n−2

X4 X1 X n−2

X n−1 X2 X3

X n−1 X2 Xn X3

a) vor der Referenz auf X n

b) nach der Referenz auf X n

Abbildung 13.4.: Einfacher Cache Wenn wir das Szenario auf betrachten, stellen sich zwei Fragen zur Beantwortung: Woher wissen wir, ob ein Datum sich im Cache befindet? Und, wenn es das tut, wie finden

Technische Informatik I

237

13. Realisierung einer leistungsfähigen Speicherarchitektur wir es dann? Die Antworten auf diese beiden Fragen hängen zusammen. Wenn jedes Wort nur an eine bestimmte Stelle in den Cache kommen kann, dann werden wir wissen, wo wir dieses Wort im Cache finden. Der einfachste Weg, eine Stelle im Cache für jedes Wort im Speicher zuzuweisen, ist die Zuweisung einer Cache-Stelle basierend auf der Adresse des Wortes im Speicher. Diese Cache-Struktur nennt man direkte Zuweisung (direct mapped), da jede Speicherstelle direkt auf genau eine Stelle im Cache abgebildet wird. Die Zuweisungsvorschrift zwischen Speicheradressen und Cache-Blöcken ist bei der direkten Zuweisung recht einfach. Fast alle Caches mit direkter Zuweisung benutzen die folgende Abbildung: AC = A S mod nC

• AC – Adresse des Blocks im Cache • A S – Adresse des Blocks im Speicher • nC – Anzahl der Blöcke im Cache Wenn die Cache-Blockanzahl eine ganzzahlige Zweierpotenz ist, entspricht die ModuloBerechnung einer Beschneidung der Speicheradresse auf die unteren log2 nC Bit. Anders betrachtet: Ist nC eine ganzzahlige Zweierpotenz, so ist die Abbildungsberechnung trivial.

000 001 010 011 100 101 110 111

Ein stark vereinfachtes Beispiel dafür veranschaulicht Abbildung 13.5. In diesem Fall habe der Cache acht Blöcke, d. h. er nimmt maximal acht Speicherworte auf. Für die Adressierung des Caches reichen – wie gerade gezeigt – log2 8 = 3 Bit. Der (Haupt-)Speicher sei 32 Worte groß. Diese werden mit einer log2 32 = 5 Bit langen Adresse angesprochen. Da die Blockanzahl des Caches eine Zweierpotenz ist, lässt sich die oben beschriebene vereinfachte Umsetzung der Speicheradressen auf Cache-Adressen anwenden. Der einer Speicheradresse zugeordnete Cache-Block ergibt sich somit aus den unteren (niederwertigsten) drei Bit der Fünf-Bit-Adresse. Folglich werden die Adressen 00001, 01001, 10001 und 11001 auf dem Cacheeintrag 001 und die Adressen 00101, 01101, 10101 und 11101 auf den Eintrag 101 abgebildet.

00001 00101

01001

01101 10001 10101

11001

11101

Abbildung 13.5.: Beispiel für einen Cache mit acht Einträgen An diesem Beispiel erkennt man das Dilemma: Jeder Cache-Block ist mehreren Speicheradressen zugeordnet, d. h. für jeden Cache-Eintrag gibt es mehrere Adress-Kandidaten.

238

Technische Informatik I

13.2. Cache-Struktur und Arbeitsweise Es ist also allein aus dem Cache-Block-Index nicht erkennbar, welches Speicherwort dort im Moment zwischengespeichert wird. Das Problem wird gelöst, indem man sich zu jedem Cache-Block die “Restadresse” merkt. Aus diesem Rest und dem Index des CacheBlocks lässt sich im Nachhinein die eigentliche Speicheradresse rekonstruieren. Diese “Restadressen” werden Tags (Etiketten) genannt. Konkret bedeutet das: Der Cache besteht (bis hierhin) aus nC Zweier-Tupeln aus Tag und Cache-Block, die so angeordnet sind, dass die Cache-Block-Adresse den im Tag fehlenden Adressteil darstellt, um die ursprüngliche Speicheradresse zu bestimmen. Das Tag enthält also lediglich den vorderen Teil der Speicheradresse, also die Bits, die nicht als Index für den Cache verwendet werden. Zum Beispiel benötigt man in Abbildung 13.5 nur zwei von fünf Adressbits im Tag, da die untersten drei Bits der Adresse den Block auswählen. Wenn der Computer gestartet wird, enthalten die Cache-Blöcke Zufallsdaten oder bestimmte Startwerte (je nach Implementierung). Selbst nach diversen Arbeitsschritten kann es sein, dass Cache-Blöcke immer noch nicht verwendet wurden, wie in Abbildung 13.5 zu sehen. Wünschenswert wäre an dieser Stelle irgendeine Belegung für Cache-BlockInhalt oder Tag, die signalisiert, dass der Cache-Block nur ungültige Daten enthält. Dies ist aber nicht möglich, da sämtliche Belegungen für Tag und Cache-Block durch die Speicherabbildung “verbraucht” sind. Es muss daher ein anderer Weg gesucht werden, um anzuzeigen, dass ein Eintrag ignoriert werden muss. Die gebräuchlichste Methode ist ein zusätzliches Gültigkeitsbits (valid bit). Wenn das Bit nicht gesetzt ist, kann sofort auf einen Fehler geschlossen und das Nachladen vom Hauptspeicher eingeleitet werden. Für den Rest dieses Abschnitts werden wir uns auf die Erklärung konzentrieren, wie Lesen in einem Cache funktioniert und wie die Cachesteuerung beim Lesen arbeitet. Im Allgemeinen ist die Behandlung von Lesezugriffen einfacher als die von Schreibzugriffen, da Lesezugriffe den Inhalt des Caches nicht verändern. Nachdem wir uns die Grundlagen der Lesezugriffe und die Behandlung von Cachefehlern angesehen haben, untersuchen wir jetzt Cachekonzepte hinsichtlich Schreibzugriffe in einer realen Maschine.

13.2.2. Beispiel für Ladezugriffe auf einen Cache Der Cache ist anfangs leer, alle Gültigkeitsbits (Spalte V) sind ausgeschaltet (N). Der Prozessor fordert die folgenden Adressen an, wie in Tabelle 13.2 gezeigt: 10110 (Fehler), 11010 (Fehler), 10110 (Treffer), 10000 (Fehler), 00011 (Fehler), 10000 (Treffer), und 10010 (Fehler). Die Abbildungen zeigen die Cacheinhalte nach jedem Fehler in der Folge, wie sie behandelt werden. Wenn Adresse 10010 (18) referenziert wird, muss der Eintrag für Adresse 11010 (26) ersetzt werden, so dass eine Referenz auf 11010 einen weiteren Fehler auslösen wird. Das Tag-Feld enthält nur den oberen Teil der Adresse. Die ganze Adresse eines Wortes im Cache-Block i mit dem Tag j ist also j ·8+i , oder die Verkettung des Tag-Feldes j und des Index i . Zum Beispiel hat der Index 010 im Teil f) der Tabelle 13.3 das Tag 10 und entspricht der Adresse 10010.

Technische Informatik I

239

13. Realisierung einer leistungsfähigen Speicherarchitektur Referenzadresse dezimal binär 22 10110 26 11010 22 10110 26 11010 16 10000 3 00011 16 10000 3 10010

Treffer oder Fehler Fehler (b) Fehler (c) Treffer Treffer Fehler (d) Fehler (e) Treffer Fehler (f)

zugewiesener Cache-Block (101102 (110102 (101102 (110102 (100002 (000112 (100002 (100102

mod 8) = 1102 mod 8) = 0102 mod 8) = 1102 mod 8) = 0102 mod 8) = 0002 mod 8) = 0112 mod 8) = 0002 mod 8) = 0102

Tabelle 13.2.: Zugriffe auf einen Cache mit direkter Zuweisung

Index

V

Index

V

000

N

000

N

001

N

001

N

010

N

010

N

011

N

011

N

100

N

100

N

101

N

101

N

110

N

110

J

111

N

111

Tag

Daten

N a) Initialzustand

Index

V

000

N

001

N

Tag

Daten

Index

Daten

000

J

102

Speicher 100002

001

N 112

Speicher 110102

102

Speicher 101102

J

010

J

011

N

100

N

100

N

101

N

101

N

110

J

111

N

c) nach Zugriff auf Adresse 110102

Speicher 101102

Tag

N

Speicher 101102

102

V

011

102

Speicher 110102

Daten

b) nach Zugriff auf Adresse 101102

010

112

Tag

110

J

111

N

d) nach Zugriff auf Adresse 100002

Index

V

Tag

Daten

Index

V

Tag

Daten

000

J

102

Speicher 100002

000

J

102

Speicher 100002

001

N

001

N

010

J

112

Speicher 110102

010

J

102

Speicher 100102

011

J

002

Speicher 000112

011

J

002

Speicher 000112

100

N

100

N

101

N

101

N 102

Speicher 101102

110

J

111

N

102

Speicher 101102

e) nach Zugriff auf Adresse 000112

110

J

111

N

f) nach Zugriff auf Adresse 100102

Tabelle 13.3.: Cacheeinträge für das Beispiel in Tabelle 13.2

240

Technische Informatik I

13.2. Cache-Struktur und Arbeitsweise Durch das Ersetzen von Daten wird es dem Cache ermöglicht, Vorteile aus zeitlicher Lokalität zu ziehen: Daten, auf die zugegriffen wird, ersetzen Daten, auf die schon länger nicht zugegriffen wurde. Diese Situation ist direkt analog dazu, ein Buch aus dem Regal zu benötigen und keinen Platz mehr auf dem Schreibtisch zu haben – einige der Bücher auf dem Tisch müssen ins Regal zurückgebracht werden. In einem Cache mit direkter Zuweisung gibt es nur einen Platz, wo die neu angeforderten Daten abgelegt werden können und folglich nur eine Wahl, was ersetzt werden muss. Wir wissen, wo wir im Cache für jede mögliche Adresse nachschauen müssen: Die hinteren Bits einer Adresse können benutzt werden, um den speziellen Cacheeintrag zu finden, auf den die Adresse abgebildet wird. Abbildung 13.6 zeigt, wie eine Adresse aufgeteilt wird. • Ein Index, welcher zur Auswahl des Cache-Blocks verwendet wird • Ein Tag-Feld, welches für den Vergleich mit dem Wert des Tag-Feldes im CacheEintrag herangezogen wird Adresse (Bit-Positionen) 31 30

13 12 11

2 1 0 ByteVersatz

Tag

20 Index Index

Valid

Tag

10 Daten

0 1 2

1021 1022 1023

20

32

=

Daten &

Treffer

Abbildung 13.6.: Direct-Mapped-Cache mit 1K Wörter Kapazität, der als Zwischenspeicher für 32-Bit-Daten verwendet wird, die mit 32-Bit-Adressen referenziert werden Der Tag im Cache wird mit dem vorderen Teil der Adresse verglichen, um zu bestimmen, ob ein Eintrag im Cache der angeforderten Adresse entspricht. Da der Cache 210 = 1024 Worte und eine Blockgröße von einem Wort hat, werden 10 Bits verwendet, um den Cache zu indizieren, so dass 32 − 10 − 2 = 20 Bits verbleiben, um mit dem Tag verglichen zu werden. Wenn der Tag und die oberen 20 Bits der Adresse gleich sind, ist der Zugriff ein Treffer und das Wort wird an den Prozessor ausgeliefert, sofern dieser Cache-Eintrag gültig ist. Da eine gegebene Adresse nur an genau einer Stelle auftreten kann, muss das Tag nur dem oberen Teil der Adresse entsprechen, welcher nicht zu Indizierung des Caches ver-

Technische Informatik I

241

13. Realisierung einer leistungsfähigen Speicherarchitektur wendet wird. Folglich gibt der Index eines Cacheblicks zusammen mit dem Tag-Inhalt dieses Blocks eindeutig die Speicheradresse des Wortes im Cache-Block an. Weil das Indexfeld als Adresse für den Cachezugriff benutzt wird und ein n -Bit-Feld 2n Werte hat, hat die gesamte Anzahl der Einträge im Cache eine Zweierpotenz zu sein. Bei der MIPSArchitektur geben die letzten zwei Bit jeder Adresse ein Byte innerhalb eines Wortes an und werden nicht zur Auswahl eines Wortes im Cache verwendet. Die gesamte Anzahl Bits, die für einen Cache gebraucht werden, ist eine Funktion der Cachegröße und der Adressgröße, da der Cache sowohl die Speicherung der Daten als auch der Tags umfasst. Geht man von einer 32-Bit-Adresse aus, erfordert ein Cache mit direkter Zuweisung der Größe 2n Worte mit Ein-Wort-Blöcken (4 Byte) ein Tag-Feld, dessen Größe 32 − (n + 2) ist, da zwei Bits für den Byteversatz und n Bits für den Index benutzt werden. Die Gesamtanzahl von Bits in einem Cache mit direkter Zuweisung ist 2n · (Blockgröße + Taggröße + Gültigkeitsfeldgröße). Da die Blockgröße ein Wort (32 Bits) und die Adressengröße ebenfalls 32 Bit ist, ist die Anzahl der Bits eines solchen Cache gleich 2n · (32 + (32 − n − 2) + 1 = 2n · (63 − n). Ein Beispiel: Wie viele Bits werden für einen Cache mit direkter Zuweisung benötigt, der 64 KB Daten und Ein-Wort-Blöcke aufnehmen kann, wenn wir von 32-Bit-Adressen ausgehen? 64 KB entsprechen insgesamt 16K = 214 Worte und dies sind, bei einer Blockgröße von einem Wort, 214 Blöcke. Jeder Block umfasst 32 Bits an Daten plus einem Tag, welches 32 − 14 − 2 Bits groß ist, plus einem Gültigkeitsbit. Folglich ist Gesamtgröße 214 · (32 + (32 − 14 − 2) + 1) = 214 · 49 = 784 · 210

also 784 Kilobit oder 98 KB für einen 64KB-Cache. Für diesen Cache ist die Gesamtanzahl der Bits mehr als anderthalb Mal so groß wie die Anzahl der für die reine Datenspeicherung benötigten Bits.

13.2.3. Fehlerbehandlung bei Direct-Mapped-Cache Bevor wir den Cache eines realen Systems betrachten, schauen wir uns an, wie die Kontrolleinheit mit Cachefehlern umgeht. Die Kontrolleinheit muss einen Fehler erkennen und den Fehler behandeln, indem Daten aus dem Hauptspeicher (oder aus einem tieferen Cache) geladen werden. Wenn der Cache einen Treffer meldet, benutzt die Maschine diese Daten. Folglich können wir die gleichen Grundsteuerungen anwenden, die wir bei der Betrachtung der CPU-Struktur entwickelt haben. Die Veränderung der Steuerung eines Prozessors zur Behandlung von Treffern ist trivial. Cachefehler erfordern jedoch weitere Arbeit. Der grundlegende Ansatz ist, die Arbeit des Prozessors auszusetzen, indem man die Inhalte aller Register einfriert. Ein separater Controller behandelt den Cachefehler und lädt die Daten vom Hauptspeicher in den Cache. Sobald die Daten vorhanden sind, wird die Ausführung bei dem Zyklus wieder aufgenommen, der den Cachefehler hervorrief. Die Cachefehlerbehandlung wird durch die Kontrolleinheit des Prozessors und einen zusätzlichen Controller vollzogen. Der letztere initiiert den Speicherzugriff und füllt den Cache wieder auf. Die Verarbeitung eines Cachefehlers verursacht eine Blase ähnlich wie beim Pipelining ein Sprung. Um den Overhead von Cachefehlern zu verringern, kann man dem Prozessor erlauben, den nächsten Befehl zu holen (was allerdings nur bei Datenzugriffen Sinn macht, also nur, wenn ein

242

Technische Informatik I

13.2. Cache-Struktur und Arbeitsweise Operand geladen wird anstelle eines Befehls). Mitunter ist der Nutzen nicht erheblich, weil entweder der Operanden sehr schnell benötigt wird oder der nächste Befehl ebenfalls auf den Cache zugreifen möchte. Wenn der Zugriff auf einen Befehl zu einem Fehler führt, sind die Inhalte des Anweisungsregisters ungültig. Um den korrekten Befehl in den Cache zu bekommen, müssen wir in der Lage sein, die untere Ebene der Speicherhierarchie zum Lesen anzuweisen. Wir können nun die Schritte angeben, um einen solchen Fehler zu behandeln: 1. Sende den ursprünglichen Programmzähler (momentaner Wert minus 4) zum Speicher. 2. Weise den Hauptspeicher an zu lesen und warte, dass der Speicher den Zugriff beendet. Der Zugriff wird mehrere Prozessorzyklen dauern. 3. Schreibe den Cacheeintrag: setze die Daten aus dem Speicher in den Datenteil des Eintrags, die vorderen Bits der Adresse in das Tag-Feld und aktiviere das Gültigkeitsbit. 4. Starte die Befehlausführung neu, wodurch der Befehl neu geladen wird – diesmal wird sie im Cache gefunden. Bei einem Cachefehler können wir die ganze Maschine zögern lassen, insbesondere durch Einfrieren der Inhalte der temporären und für den Programmierer sichtbaren Register, während wir auf den Speicher warten. Im Gegensatz dazu sind Pipelineblasen komplexer, da wir die Ausführung einiger Anweisungen fortsetzen müssen, während wir andere verzögern. Der Rest dieses Abschnitts beschreibt eine Caches aus einer realen Maschine und untersucht, wie er Lesen und Schreiben behandelt. Anschließend werden wir die Behandlung von Schreibzugriffen detaillierter beschreiben.

13.2.4. Beispiel: Cache in der DECstation 3100 Die DECstation 3100 war eine Workstation, die einen MIPS R2000 als Prozessor nutzte und eine sehr einfache Cacheimplementierung besaß. Als Beispiel verdeutlicht es in anschaulicher Weise die zu beschreibenden Prinzipien. Dieser Prozessor hat eine Pipeline ähnlich der aus dem obigen Kapitel über RISC-Prozessoren. Wenn der Prozessor arbeitet, kann er jeden Takt sowohl ein Befehlswort als auch ein Datenwort anfordern. Um das Verlangen der Pipeline ohne Verzögerungen erfüllen zu können, werden getrennte Anweisungs- und Datencaches verwendet. Jeder Cache ist 64 KB groß (oder 16K Worte), mit einer Blockgröße von einem Wort. Abbildung 13.6 zeigt den Aufbau des Datencache der DECstation 3100. Cache-Leseanforderungen erfordern folgende Grundfunktionen: Da es getrennte Datenund Befehlscaches gibt, werden getrennte Steuersignale benötigt, um jeden Cache auslesen und beschreiben zu können (Man erinnere sich daran, dass wir in den Befehlscache schreiben müssen, wenn ein Cachefehler auftritt). Es sind zwei Schritte für eine Leseanforderung für beide Caches notwendig. 1. Sende die Adresse zum entsprechenden Cache. Die Adresse kommt entweder aus dem Programmzähler (für das Lesen einer Befehlsanweisung) oder aus der ALU (für einen Datenzugriff).

Technische Informatik I

243

13. Realisierung einer leistungsfähigen Speicherarchitektur 2. Wenn der Cache einen Treffer meldet, ist das angeforderte Wort auf den Datenleitungen verfügbar. Wenn der Cache jedoch einen Fehler meldet, muss die Adresse zum Hauptspeicher gesendet werden. Wenn dieser die Daten zurückliefert, schreiben wir sie in den Cache. Cache-Schreiben funktioniert aufgrund von Daten-Konsistenzproblemen ganz anders. Man stelle sich einen Speicherbefehl vor, bei die Daten nur in den Datencache geschrieben werden (ohne, dass der Hauptspeicher verändert wird). In solch einem Fall sind Cache und Hauptspeicher inkonsistent. Der einfachste Weg, Hauptspeicher und Cache konsistent zu halten, ist, die Daten sowohl in den Hauptspeicher als auch in den Cache zu schreiben. Dieses Verfahren, welches der DECstation 3100 benutzt, nennt man Durchschreiben (write-through). Neben dem Problem der Datenkonsistenz existiert das des Cachefehlers beim Schreiben in den Cache, wenn dieser voll ist. Da das Datenwort im Cache durch den Prozessor geschrieben wird, gibt es keinen Grund, warum ein Wort aus dem Speicher gelesen werden muss, es würde durch den Prozessor lediglich überschrieben. Tatsächlich können wir immer das Wort in den Cache schreiben und somit Tag und Daten auf den neuesten Stand bringen. Wir müssen nicht überlegen, ob ein Schreibzugriff in den Cache trifft oder fehlerhaft verläuft. Diese Beobachtung führt zu dem folgenden einfachen Schema zur Verarbeitung von Schreibzugriffen, welcher auch bei der DECstation 3100 eingesetzt wird: 1. Indiziere den Cache unter Verwendung der Bits 15–2 der Adresse. 2. Schreibe die Bits 31–16 der Adresse in den Tag, schreibe das Datenwort in den Datenteil und setze das Gültigkeitsbit. 3. Schreibe das Wort außerdem in den Hauptspeicher, wobei die ganze Adresse verwendet wird. Obwohl dieser Entwurf die Schreibzugriffe sehr einfach abhandelt, erbringt er keine guten Leistungen. Beim Durchschreibe-Schema verursacht jeder Schreibzugriff ein Schreiben der Daten in den Hauptspeicher. Diese Schreibzugriffe würden folglich eine lange Zeit benötigen und könnten die Maschine erheblich verlangsamen. Beim GNU C Compiler gcc sind 13 % der Anweisungen Schreibzugriffe. Bei der DECstation 3100 beträgt die Anzahl der Zyklen pro Anweisung (für ein Programm wie den gcc) um die 1,2. Damit entsteht ein Verlust von zehn Zyklen bei jedem Schreiben. Dies führt zu einer Anzahl von 1,2+10·13 % = 2,5 Zyklen pro Anweisung und die Leistung wird um mehr als Faktor zwei verringert. Eine Lösung für dieses Problem ist die Verwendung eines Schreibpuffers. Dieser speichert die Daten zwischen, die in den Speicher geschrieben werden sollen. Nach dem Speichern der Daten in den Cache und in den Schreibpuffer kann der Prozessor die Ausführung fortsetzen. Sobald das Schreiben in den Speicher beendet ist, wird der Eintrag im Schreibpuffer frei gegeben. Sollte der Schreibpuffer bereits voll sein, wenn der Prozessor einen Schreibzugriff ausführen will, muss der Prozessor solange blockieren, bis es eine freie Position im Schreibpuffer gibt. Wenn die Rate, mit der der Hauptspeicher Schreibzugriffe vollziehen kann, geringer als die Rate ist, mit der der Prozessor diese Zugriffe erzeugt, hilft natürlich kein noch so großer Puffer, da Schreibzugriffe schneller erzeugt werden, als das Speichersystem sie annehmen kann. Selbst wenn der Prozessor nicht mit der maximal möglichen Rate versucht zu schreiben, sind Verzögerungen möglich. Dies passiert, wenn das Schreiben stoßweise auftritt. Um

244

Technische Informatik I

13.2. Cache-Struktur und Arbeitsweise das Auftreten solcher Verzögerungen zu reduzieren, wird die Tiefe des Schreibpuffers des Prozessors kleiner als der Speicher größer gewählt hat. Zum Beispiel ist der Schreibpuffer des DECstation 3100 vier Worte tief. Da der Unterschied zwischen der Rate der Erzeugung von Schreibzugriffen eines Programms und der Rate, mit der das Speichersystem diese annehmen kann, wächst, sind noch “tiefere” Schreibpuffer angebracht. Größere Schreibpuffer z. B. mit 10 Einträgen verbessern den Wirkungsgrad. Die Alternative zum Durchschreiben ist das Zurückschreiben (write-back). Beim Zurückschreiben wird beim Auftreten eines Schreibzugriffs der neue Wert nur in den CacheBlock geschrieben. Der veränderte Block wird dann in die untere Ebene der Hierarchie geschrieben, wenn er dort ersetzt werden soll. Man muss nun bei einem Lesefehler explizit das Datum, das aus dem Cache geworfen wird, in den Speicher schreiben und dort nicht einfach überschreiben, wie dies oben ausgeführt der Fall war. Zurückschreiben kann die Leistung verbessern, besonders dann, wenn der Prozessor Schreibzugriffe genauso schnell oder schneller erzeugen kann, wie bzw. als sie vom Hauptspeicher behandelt werden können. Zurückschreiben ist jedoch viel komplexer zu implementieren als Durchschreiben. Welche Art von Cachefehlerraten werden durch eine Cachestruktur wie die der DECstation 3100 erzeugt? Tabelle 13.4 zeigt die Fehlerraten für die Befehls- und Datencaches für zwei Programme, zum einen den bereits erwähnten gcc und zum anderen spice, ein Simulationsprogramm für Elektronikschaltungen. Man beachte, dass die Lokalität beim gcc von Natur aus geringer ist. Programm gcc spice

Anweisungsfehlerrate

Datenfehlerrate

Gesamtfehlerrate

6,1 % 1,2 %

2,1 % 1,3 %

5,4 % 1,2 %

Tabelle 13.4.: Fehlerraten für Befehls- und Datencaches Die Gesamtfehlerrate ist die effektive Fehlerrate für die Verbindung der beiden 64-KBCaches für Befehle und Daten. Man erhält sie, in dem man die individuellen Fehlerraten für Befehle und Daten gegen die Häufigkeit von Befehls- und Datenbezügen aufwiegt. Man beachte, dass bei der DECstation 3100 die Datenfehler nur Lesefehler beinhalten, weil das Schreiben keinen Fehler verursacht. Ein gemeinsamer Cache mit der Gesamtgröße der beiden getrennten Caches wird eine bessere Trefferrate haben. Diese höhere Rate entsteht, weil der kombinierte Cache die Anzahl der Einträge, die für Befehle genutzt werden, nicht gleichmäßig von der, die für Daten gebraucht werden, abgrenzt. Dennoch benutzen viele Maschinen getrennte Befehlsund Datencaches, um die Bandbreite des Caches zu erhöhen. Hier nun einige Messungen für die DECstation 3100 für den gcc, und für einen gemeinsamen Cache, dessen Größe gleich der Summe der beiden Caches der DECstation 3100 ist. • Gesamtgröße des Caches: 128 KB • Fehlerrate des geteilten Caches: 5,4 % • Fehlerrate des gemeinsamen Caches: 4,8 % Die Fehlerrate beim geteilten Cache ist nur geringfügig schlechter. Bei vielen Systemen überwiegt der Vorteil bei der Verdoppelung der Cache-Bandbreite durch die Möglichkeit des gleichzeitigen Zugriffs auf Daten und Befehle den Nachteil

Technische Informatik I

245

13. Realisierung einer leistungsfähigen Speicherarchitektur einer leicht erhöhten Fehlerrate. Diese Beobachtung ist ein weiterer Hinweis darauf, dass man die Fehlerrate nicht als einzige Maß für die Leistung eines Caches nutzen kann.

13.2.5. Vorteile durch räumliche Lokalität Der Cache, den wir bisher beschrieben haben, war zwar einfach, zog jedoch keinen Nutzen aus der räumlichen Lokalität von Anforderungen, da jedes Wort in seinem eigenen Block war. Wie wir jedoch eingangs festgestellt haben, existierte in Programmen eine räumliche Lokalität. Um daraus einen Vorteil zu ziehen, brauchen wir einen Cache-Block, der größer als ein einzelnes Wort ist. Wenn ein Cachefehler auftritt, werden wir dann mehrere benachbarte Worte laden, die sich durch eine hohe Wahrscheinlichkeit auszeichnen, dass sie in Kürze benötigt werden. Adresse (Bit-Positionen) 31 .

. . 16 15 . . . 4

3 2 1 0 ByteVersatz

16

12

2

Block-Versatz

Tag Index 16 Bits

128 Bits

Tag

Daten

4k Einträge

V

32

32

32

32

16

=

Mux &

Treffer Daten

Abbildung 13.7.: 64-KB-Cache mit Vier-Wort-Blöcken Abbildung 13.7 zeigt einen Cache, der 64 KB Daten enthält. Diesmal werden allerdings Blöcke mit jeweils vier Worten (16 Bytes) verwendet. Verglichen mit Abbildung 13.6, welche einen gleich großen Cache zeigt, wurde ein zusätzliches Indexfeld benutzt, um den Multiplexer zu steuern (am unteren Rand der Abbildung), welcher das angeforderte im indizierten Block auswählt. Die totale Anzahl an Tags und Gültigkeitsbits ist einem Cache mit Multiwortblöcken kleiner, da jedes Tag und Gültigkeitsbit für vier Worte benutzt wird. Die gemeinsame Nutzung von Tags verbessert die Effizienz der Speichernutzung im Cache. Das Tag-Feld ist 16 Bits und das Indexfeld 12 Bits breit, während ein 2-Bit-Feld zur Auswahl des Wortes aus dem Block (unter Verwendung eines 4-zu-1-Multiplexers) benutzt wird. In der Praxis werden die hinteren Bits der Adresse (Bits 2 und 3 in diesem Fall) dazu benutzt, nur die RAM-Module zu aktivieren, die das gewünschte Wort enthalten, wodurch man den Multiplexer einsparen kann.

246

Technische Informatik I

13.2. Cache-Struktur und Arbeitsweise Ein anderer Weg zur Eliminierung des Multiplexers ist, ein großes RAM für die Daten zu haben (in dem die Tags getrennt gespeichert werden) und den Blockversatz zu benutzen, um zwei Adressbits für das RAM zu liefern. Das RAM muss 32 Bits breit sein und vier mal so viele Worte enthalten, wie es Blöcke im Cache gibt. Wie finden wir nun den Cache-Block für eine bestimmte Adresse? Wir können die gleiche Abbildung nutzen, die wir für den Cache mit nur einem Wort pro Block verwendet haben: AC = A S mod nC (siehe Abschnitt 13.2.1). Die Blockadresse ist einfach die Wortadresse geteilt durch die Anzahl der Wörter im Block (oder äquivalent dazu: Die Byteadresse geteilt durch Anzahl der Bytes im Block). Dazu ein Beispiel: Man stelle sich einen Cache mit 64 Blöcken und einer Blockgröße von 16 Bytes vor. Auf welche Blocknummer wird die Byteadresse 1200 abgebildet? Der Block ist durch die obige Formel gegeben. Wobei die Adresse des Blockes wie folgt ist: Byteadresse Bytes pro Block Man beachte, dass diese Blockadresse der Block ist, der alle Adressen zwischen Byteadresse · Bytes pro Block Bytes pro Block und Byteadresse · Bytes pro Block + (Bytes pro Block − 1) Bytes pro Block enthält. Folglich ergibt sich (bei 16 Bytes pro Block) für die Byteadresse 1200 die Blockadresse 1200 = 75 16

die auf die Cache-Blockadresse 75 mod 64 = 11 abgebildet wird. Lesefehler werden für einen Multiwortblock genauso wie für Einzelwortblöcke behandelt. Ein solcher Fehler bewirkt eine Blockholephase. Schreibtreffer und -fehler müssen allerdings anders behandelt werden, als dies beim Cache der DECstation 3100 getan wird. Da der Block mehr als ein einzelnes Wort enthält, können wir nicht einfach Tag und Daten schreiben. Um dies nachzuvollziehen, stelle man sich zwei Speicheradressen X und Y vor, die beide auf den Cache-Block C abgebildet werden. C ist ein Vier-WortBlock, der momentan Y enthält. Nun stelle man sich vor, an Adresse X zu schreiben, indem man einfach die Daten und das Tag im Cache-Block C überschreibt. Nach dem Schreiben wird Block C das Tag für X haben, aber der Datenteil des Cacheeintrags im Block C enthält ein Wort von X und drei Wörter von Y . Bei durchschreibendem Cache ist das Problem wie folgt lösbar: Das Wort wird in den Hauptspeicher geschrieben und gleichzeitig ein Tag-Vergleich durchgeführt. Ist der Vergleich positiv, kann das betreffende Wort auch in den Cache geschrieben werden. Wenn nicht, muss vorher der betreffende Block aus dem Hauptspeicher geladen werden.

Technische Informatik I

247

13. Realisierung einer leistungsfähigen Speicherarchitektur Bei zurückschreibendem Cache entfällt das Schreiben des Wortes in den Hauptspeicher. Bei einem negativen Tag-Vergleich muss auch hier zuerst ein neuer Block aus dem Hauptspeicher geladen werden. Im Gegensatz zu einem Ein-Wort-Block erfordern Schreibfehler bei einem Multiwort-Block ein Lesen aus dem Hauptspeicher. Der Grund für die Vergrößerung der Blöcke war, Vorteile aus der räumlichen Lokalität zu beziehen, um die Leistung zu verbessern. Wie betrifft eine Veränderung der Blockgröße die Leistung? Im allgemeinen verringert sich die Fehlerrate, wenn wir die Blockgröße erhöhen. Dieser Trend wird am besten an einem Beispiel deutlich. Nehmen wir an, die folgenden Adressen würden durch ein Programm angefordert: 16, . . . , 24, . . . , 20 und keine dieser Adresse befinde sich im Cache. Räumliche Lokalität sagt uns, dass ein Muster dieser Form sehr wahrscheinlich ist, obwohl die Reihenfolge der Referenzen variieren kann. Wenn der Cache einen Vier-Wort-Block hat, würde ein Fehler bei Adresse 16 den Block in den Cache laden, der die Adressen 16, 20, 24 und 28 enthält. Nur ein Fehler tritt bei den drei Referenzen auf, vorausgesetzt, dass eine dazwischen liegende Referenz den Block nicht wieder aus dem Speicher wirft. Mit einem Ein-Wort-Block wären zwei zusätzliche Fehler nötig, da jeder Fehler nur ein einzelnes Wort in den Speicher bringt. Tabelle 13.5 zeigt die Fehlerraten der Programme gcc und spice, mit Ein- und Vier-WortBlocken. Die Befehlsfehlerraten fallen fast genauso schnell, wie die Blockgrößen steigen, dieser steilere Fall der Befehlsfehlerrate gegenüber der Datenfehlerrate resultiert daraus, dass Befehle eine höhere räumliche Lokalität haben. Programm gcc gcc spice spice

Blockgröße 1 4 1 4

Anweisungsfehlerrate 6,1 % 2,0 % 1,2 % 0,3 %

Datenfehlerrate 2,1 % 1,7 % 1,3 % 0,6 %

Gesamtfehlerrate 5,4 % 1,9 % 1,2 % 0,4 %

Tabelle 13.5.: Fehlerraten in Abhängigkeit von der Blockgröße Mit den Vier-Wort-Blöcken führen wir Schreibfehler ein, die bei einem Ein-Wort-Block keine Verzögerung verursachten und somit in diesem Fall nicht berücksichtigt wurden. Würde man Schreibfehler in beiden Fällen berücksichtigen, wäre die Fehlerrate ein wenig höher als angegeben. Die Fehlerrate kann auch dann steigen, wenn die Blockgröße ein erheblicher Teil der Cachegröße wird, da die Anzahl der Blöcke, die im Cache gehalten werden können, klein wird und sich die Blöcke immer öfter gegenseitig verdrängen. Ein Block wird aus dem Speicher verdrängt, bevor auf viele seine Worte zugegriffen werden konnte. Wie Abbildung 13.8 zeigt, verringert eine Erhöhung der Blockgröße normalerweise die Fehlerrate. Allerdings steigt sie wieder an, wenn die Blöcke im Vergleich zum Cache groß werden. Ein ernsteres Problem, welches mit der Erhöhung der Blockgröße verbunden ist, sind die steigenden Kosten eines Fehlers. Die Fehlerstrafe wird durch die Zeit bestimmt, welche zum Laden eines Blocks aus der nächsttieferen Ebene der Hierarchie in den Cache benötigt wird. Diese Zeit setzt sich aus zwei Teilen zusammen: Die Wartezeit auf das erste Wort und die Übertragungszeit für den Rest des Blocks. Klar ist, ohne Änderung der Kapazität des Speichersystems ändert sich die Übertragungszeit und die Fehlerstrafe erhöht sich, wenn nur die Blockgröße wächst. Weiterhin beginnen die Verbesserungen in der Fehlerrate zu schrumpfen, wenn die Blöcke größer werden. Das Ergebnis ist, dass

248

Technische Informatik I

13.2. Cache-Struktur und Arbeitsweise

Abbildung 13.8.: Fehlerraten in Abhängigkeit von Blockgrößen und Cachegröße

die Erhöhung in der Fehlerstrafe die Verkleinerung der Fehlerrate für große Blöcke überwiegt und die Cacheleistung somit abnimmt. Wird der Speicher so aufgebaut, dass er größere Blöcke schneller überträgt, kann die Blockgröße erhöht und weitere Verbesserungen der Cacheleistung erreicht werden. Wir besprechen dieses Thema im nächsten Abschnitt. Eine Vergrößerung der Cache-Blöcke führt zu einer Erhöhung der Fehlerstrafe (Wartezeit). Diese Wartezeit ist eng verknüpft mit der Zugriffszeit des Hauptspeichers und kann nicht ohne weiteres verändert werden. Eine geschickte Methode, die Übertragungszeit zwischen den beiden Speicherebenen zu kaschieren ist, einen frühen Neustart (early restart) durchzuführen. Darunter versteht man die Wiederaufnahme der Ausführung, sobald das angeforderte Wort des Blockes geliefert wurde, anstatt auf den ganzen Block zu warten. Diese Strategie ist dort besonders erfolgreich, wenn Daten bzw. Befehle in hohem Maße sequentiell verarbeitet werden: Angenommen, das Speichersystem kann pro Taktzyklus ein Wort ausliefern. Beim frühen Neustart wird bei einem Cache-Fehler der Prozessor starten, sobald der Cache-Block soweit im Cache angekommen ist, dass das angeforderte Wort verfügbar ist. Fordert der Prozessor nun sequentiell die im Speicher nachstehenden Blöcke an, so sind diese im Cache genau rechtzeitig da. Diese Technik ist wegen der grundsätzlichen Sequentialität effektiv bei Befehls-Caches. Sie ist jedoch weniger effektiv für Daten-Caches, da es wahrscheinlich ist, dass die Worte des Blocks auf eine wenig vorhersagbare Weise angefordert werden. Die Wahrscheinlichkeit, dass der Prozessor ein weiteres Wort aus einem anderen Cache-Block benötigt, bevor die Übertragung beendet ist, ist groß. Wenn der Prozessor nicht auf den Datencache zugreifen kann, weil bereits eine Übertragung vollzogen wird, muss er warten. Ein noch vorteilhafterer Ansatz besteht darin, den Speicher so aufzubauen, damit das angeforderte Wort als erstes aus dem Speicher in den Cache geladen wird. Der Rest des Blocks wird anschließend übertragen, beginnend mit der Adresse nach dem angeforder-

Technische Informatik I

249

13. Realisierung einer leistungsfähigen Speicherarchitektur ten Wort und einem nachfolgenden Sprung vom letzten Wort des Blocks zum ersten. Diese Technik, die angefordertes Wort zuerst oder kritisches Wort zuerst (critical word first) genannt wird, kann ein wenig schneller als der frühe Neustart sein, wird aber durch die gleichen Eigenschaften begrenzt, die auch dem frühen Neustart die Grenzen aufzeigen.

13.2.6. Entwurf eines Speichersystems mit Cacheunterstützung Cachefehler werden durch Zugriff auf den Hauptspeicher gelöst, der aus DRAM aufgebaut ist. In letzten Kapitel haben wir gesehen, dass der DRAM mit dem Schwerpunkt auf Speicherdichte entworfen wurde, statt nur kurzer Zugriffszeit. Obwohl es schwierig ist, die Wartezeit beim Laden des ersten Wortes aus dem Speicher zu reduzieren, können wir wie zur Fehlerstrafenreduzierung die Bandbreite vom Speicher zum Cache erhöhen. Diese Reduzierung erlaubt die Verwendung von größeren Blöcken, während man trotzdem eine geringe Fehlerstrafe erhält, ähnlich der eines kleineren Blocks. Um die Auswirkungen von verschiedenen Konfigurationen des Speichers zu verstehen, definieren wir eine Menge von hypothetischen Speicherzugriffszeiten: • 1 Taktzyklus, um die Adresse zu senden • 15 Taktzyklen für jeden begonnenen DRAM-Zugriff • 1 Taktzyklus, um ein Datenwort zu senden Wenn wir einen Cache-Block aus vier Worten und eine ein Wort breite Bank aus DRAM haben, wäre die Fehlerstrafe 1 + 4 · 15 + 4 · 1 = 65 Taktzyklen. Folglich wäre die Anzahl der Bytes, die pro Taktzyklus übertragen werden: 4·4 ≈ 0, 25 65

Diese Größe wird als Bandbreite bezeichnet und beschreibt die Anzahl der pro Taktzyklus übertragbaren Bytes. Abbildung 13.9 zeigt drei Möglichkeiten für den Entwurf des Speichersystems. Die erste Option folgt dem, was wir bisher vorausgesetzt haben: Der Speicher ist ein Wort breit und alle Zugriffe finden sequentiell statt. Die zweite Möglichkeit erhöht die Bandbreite zum Speicher, indem sie den Speicher und die Busse zwischen Speicher und Prozessor verbreitert. Dies erlaubt parallelen Zugriff auf alle Worte des Blocks. Die dritte Option erhöht die Bandbreite, in dem sie den Speicher verbreitert, nicht aber den Verbindungsbus. Folglich bezahlen wir immer noch für das Versenden jedes Wortes, können aber die Kosten der Zugriffswartezeiten mehr als einmal vermeiden. Schauen wir uns an, wie die zwei letzten Möglichkeiten die 65-Zyklen-Fehlerwartezeit verringern, die beim ersten Entwurf auftreten würde. Das Verbreitern des Speichers und des Busses erhöht die Bandbreite des Speichers proportional, wodurch sich sowohl Zugriffszeit- als auch Übertragungszeitanteil der Fehlerstrafe verringern. Bei einem Hauptspeicher mit zwei Worten sinkt die Fehlerstrafe von 65 Taktzyklen auf 1+2·15+2·1 = 33 Taktzyklen. Bei einem vier Worte breiten Speicher ist die Fehlerrate nur 17 Taktzyklen. Die Bandbreite bei einen einzelnen Fehler ist dann 0,48 (fast doppelt so hoch) Bytes pro Taktzyklus für einen Speicher, der zwei Worte breit ist, und 0,94 Byte pro Takt bei vier Worten Breite (fast viermal so viel). Die Hauptkosten dieser Verbesserung

250

Technische Informatik I

13.2. Cache-Struktur und Arbeitsweise

CPU

CPU CPU

Cache Multiplexer Bus Cache

Cache Hauptspeicher

Hauptspeicher

a)

Bus

Bus Speicherbank 0

b)

Speicherbank 1

Speicherbank 2

Speicherbank 3

c)

Abbildung 13.9.: Erhöhung der Bandbreite gegenüber ein Wort breiten Speicher (a)) durch b) Busverbreiterung oder c) Interleaving sind der breitere Bus, der mögliche Anstieg in Cachezugriffszeiten durch den Multiplexer und die Steuerelektronik zwischen Prozessor und Cache. Anstatt den gesamten Pfad zwischen Hauptspeicher und Cache breiter zu machen, können die Speicherchips in Bänken angelegt werden, um mehrere Worte bei einem Zugriff zu lesen oder zu schreiben statt jedes Mal nur ein einzelnes Wort. Jede Bank könnte ein Wort breit sein, damit sich die Breite des Busses und des Caches nicht ändern muss. Das Versenden einer Adresse zu mehreren Bänken erlaubt dennoch ein gleichzeitiges Lesen aller Bänke. Dieses Schema nennt man interleaving (Überlappen, Verschachteln). Es hat den Vorteil, dass nur einmal die volle Speicherwartezeit zum Tragen kommt. Zum Beispiel würde die Zeit zum Holen eines Vier-Wort-Blocks aus einem Zyklus für das Übertragen der Adresse und Leseanforderungen zu den Bänken, 15 Zyklen für den Zugriff der vier Bänke auf den Speicher und 4 Zyklen für das Zurücksenden der vier Worte in den Cache bestehen. Das ergibt eine Fehlerstrafe von 1+1·15+4·1 = 20 Taktzyklen. Die daraus resultierende effektive Bandbreite von 0,80 Bytes pro Takt ergibt ungefähr die dreifache Bandbreite des Ein-Wort-Speichers und Ein-Wort-Bus. Diese Technologie der Bänke ist auch beim Schreiben vorteilhaft. Jede der Bänke kann unabhängig voneinander schreiben, wodurch sich die Schreibbandbreite vervierfacht, was zu geringeren Verzögerungen bei einem durchschreibenden Cache führt. Wie wir sehen werden, gibt es eine alternative Strategie, die Interleave noch attraktiver macht. 13.2.6.1. Effektivitätssteigerung Wenn die Kapazität pro Speicherchip erhöht wird, gibt es weniger Chips in einem gleichgroßen Speichersystem. Speicherchips werden so aufgebaut, dass sie eine kleine Anzahl

Technische Informatik I

251

13. Realisierung einer leistungsfähigen Speicherarchitektur von Ausgangsbits besitzen (normalerweise zwischen 1 und 16). Wir beschreiben den Aufbau des RAM als d × w , wobei d die Anzahl der adressierbaren Stellen (die Tiefe) und w die Anzahl der Bits (oder Breite jeder Stelle) ist. Die beliebten 16-Mbit-DRAM waren vom Aufbau 4M × 4. Da die Speicherdichte wächst, bleibt die Breite eines Speicherchips konstant (oder wächst nur sehr langsam), während sich die Tiefe sehr schnell erhöht. DRAM-Module sind in schmaleren Konfigurationen erhältlich (z. B. 4M bi t × 4). Breitere Anordnungen (z. B. 1M × 16) sind weniger verfügbar als schmalere und kosten mehr. Die Gründe dafür sind, dass sowohl die Kosten für die Qualitätskontrolle als auch für die Verpackung hinsichtlich die schmaleren Strukturen geringer sind und die schmalere Struktur normalerweise Massenware mit geringerer Lebensdauer ist.

13.2.7. Zusammenfassung Wir haben das Kapitel mit der Untersuchung des einfachsten Caches begonnen: Einem Cache mit direkter Zuweisung und Ein-Wort-Blöcken. In solch einem Cache sind sowohl Treffer als auch Fehler einfach handhabbar, da ein Wort nur an genau eine Stelle gehen kann, und es ein getrenntes Tag für jedes Wort gibt. Um Cache und Speicher konsistent zu halten, kann ein Durchschreiben-Schema verwendet werden, so dass jeder Schreibzugriff in den Cache auch den Speicher auf den neuesten Stand bringt. Die Alternative zum Durchschreiben ist das Zurückschreiben, das einen Block zurück in den Speicher kopiert, wenn er ersetzt wird. Um einen Nutzen aus der räumlichen Lokalität zu ziehen, muss ein Cache Blöcke haben, die größer als ein einzelnes Wort sind. Die Verwendung von größeren Blöcken verringert die Fehlerrate und verbessert die Effizienz des Caches, indem sie die Menge der aufbewahrten Tags in Relation zu den aufbewahrten Daten verringert. Obwohl größere Blöcke die Fehlerrate verringern, kann sich auch die Fehlerstrafe erhöhen. Wenn die Fehlerstrafe sich linear mit der Blockgröße erhöht, können größere Blöcke schneller zu geringerer Leistung führen. Das soll vermieden werden. Die Bandbreite des Hauptspeichers wird deshalb erhöht, um so Cache-Blöcke noch effizienter zu transportieren. Zwei gebräuchlichen Methoden, dies zu tun, sind die Verbreiterung des Speichers und Interleaving. In beiden Fällen reduzieren wir die Zeit zum Laden eines Blocks, indem wir die Anzahl der neuen Speicherzugriffe für das Laden eines Blockes minimieren. Bei einem breiteren Bus können wir ebenso die Zeit verringern, die zum Senden eines Blockes vom Speicher in den Cache benötigt wird.

13.3. Messen und Verbessern der Cacheleistung In diesem Abschnitt beginnen wir mit einem Blick auf die Messung und Analyse der Cacheleistung, um anschließend zwei verschiedene Methoden zu Verbesserung dieser Leistung zu finden. Die eine konzentriert sich auf die Verkleinerung der Fehlerrate durch eine Verringerung der Wahrscheinlichkeit, dass zwei verschiedene Speicherblöcke um die gleiche Cacheposition konkurrieren. Die zweite Methode verkleinert die Fehlerstrafe durch Hinzufügen einer weiteren Ebene zur Speicherhierarchie. Diese Technik, Multilevel Caching genannt, erschien zuerst in Hochleistungsmaschinen, die im Jahr 1990 mehr als $100.000 kosteten. Heutzutage ist diese Technik Standard in jedem Desktoprechner.

252

Technische Informatik I

13.3. Messen und Verbessern der Cacheleistung Die Prozessorzeit, d. h. die insgesamt zur Programmausführung benötigte Zeit, kann in die Taktzyklen, die der Prozessor mit der Ausführung des Programms verbringt und die Taktzyklen, die er auf das Speichersystem wartet, unterteilt werden. Normalerweise setzen wir voraus, dass die Kosten eines Cachezugriffs mit einem Treffer Teil der normalen Prozessorausführungszyklen sind. Folglich gilt ¡

¢

Prozessorzeit = Ausführungszyklen + Aussetzertaktzyklen · Taktzyklenzeit Die Aussetzertaktzyklen entstehen hauptsächlich durch Cachefehler und wir werden genau dies auch für die weitere Betrachtung annehmen. Wir werden die Betrachtung auch auf ein vereinfachtes Model des Speichersystems begrenzen. In realen Prozessoren können die Verzögerungen durch Lese- und Schreibzugriffe recht komplex sein und eine fehlerfreie Leistungsvoraussage erfordert üblicherweise sehr genaue Simulationen des Prozessors und des Speichersystems. Aussetzerzyklen können als die Summe der Aussetzerzyklen beim Lesen plus der beim Schreiben definiert werden: Aussetzerzyklen = Leseaussetzerzyklen + Schreibaussetzerzyklen Die Leseaussetzerzyklen können jeweils in Form von Lesezugriffen des Programms, der Lesefehlerrate und der Fehlerstrafe pro Lesezugriff für einen Taktzyklus definiert werden: Leseaussetzerzyklen =

Lesezugriffe · Lesefehlerrate · Lesefehlerstrafe Programmzeit

Schreibzugriffe sind viel komplizierter. Für ein Durchschreib-Schema haben wir zwei Quellen für Aussetzer: Schreibfehler, welche normalerweise ein Laden des Blocks vor der Fortsetzung des Schreibens erfordern und Schreibpufferaussetzer, welche auftreten, wenn der Schreibpuffer bei einem Schreibzugriff bereits voll ist. Folglich entsprechen die auszusetzenden Zyklen der Summe dieser beiden:

Schreibaussetzerzyklen =

Schreibzugriffe · Schreibfehlerrate · Fehlerstrafe Programmzeit +Schreibpufferaussetzer

Da Schreibpufferaussetzer auch vom Zeitpunkt der Schreibzugriffe abhängen statt nur von der Häufigkeit, ist es nicht möglich, eine einfache Gleichung zur Berechung solcher Aussetzer anzugeben. Glücklicherweise wird die Anzahl der Schreibpufferaussetzer sehr klein sein, wenn wir ein System mit sinnvoller Puffertiefe (z. B. vier oder mehr Worte) und einen Speicher haben, der Schreibzugriffe mit einer wesentlich höheren Rate annehmen kann, als sie in Programmen auftreten (z. B. doppelt so viel). In diesem Fall können wir die Schreibpufferaussetzer einfach ignorieren. Wenn ein System diese Kriterien nicht erfüllt, ist es nicht sauber entworfen. Stattdessen hätten die Entwickler entweder einen tieferen Schreibpuffer oder Zurückschreiben verwenden sollen. Zurückschreiben beinhaltet mögliche zusätzliche Aussetzer, die sich aus der Notwendigkeit des Zurückschreibens eines Cache-Blocks in den Speicher ergeben, wenn der Block ersetzt werden soll. Damit werden wir uns später noch genauer auseinandersetzen.

Technische Informatik I

253

13. Realisierung einer leistungsfähigen Speicherarchitektur Bei den meisten durchschreibenden Caches sind die Lese- und Schreibfehlerstrafen gleich groß (die Zeit für das Laden eines Blocks in den Speicher). Wenn wir annehmen, dass Schreibpufferaussetzer vernachlässigbar sind, können wir die Lese- und Schreibzugriffe verbinden, indem wir eine einzelne Fehlerrate und Fehlerstrafe verwenden: Aussetzertaktzyklen =

Speicherzugriffe · Fehlerrate · Fehlerstrafe Programmzeit

Wir können die Schreibzugriffe dann so ausdrücken: Aussetzerzyklen =

Fehler Befehle · · Fehlerstrafe Programmzeit Befehl

Denken wir uns ein einfaches Beispiel zum Verständnis der Auswirkungen der Cacheleistung auf die Maschinenleistung aus.

13.3.1. Beispiel für die Berechnung der Cacheleistung Nehmen wir an, wir hätten eine Cachefehlerrate für Befehle für den gcc von 2 % und eine Cachefehlerrate für Daten von 4 %. Wenn eine Maschine zwei Zyklen pro Befehl ohne Speicherverzögerung braucht und die Fehlerstrafe für alle Fehler 40 Zyklen beträgt, bestimme man, wie viel schneller eine Maschine mit einem perfekten Cache wäre, der keine Cachefehler erzeugt. Die Anzahl der Speicherfehlerzyklen für Befehle als Ausdruck einer Befehlszahl I ist (mit I = Befehle/Programmzeit): Befehlsfehlerzyklen = I · 2 % · 40 = 0,8 · I Die Häufigkeit aller Lade- und Speicheranweisungen im gcc ist 36%. Deswegen ergibt sich die Anzahl der Speicherfehlerzyklen für Datenverweise wie folgt: Datenfehlerzyklen = I · 36 % · 4 % · 40 = 0,56 · I Die Gesamtanzahl für Aussetzerzyklen ist 0,80 · I + 0,56 · I = 1,36 · I . Dies ist mehr als ein Zyklus eines Aussetzers pro Befehl. Dementsprechend braucht man bei einem Aussetzer 2 + 1,36 = 3,36 Zyklen pro Befehl. Da es keine Änderung bei der Anzahl der Befehle oder bei der Taktrate gibt, ist das Verhältnis der Prozessorausführungszeiten:

Prozessorzeit mit Aussetzern Prozessorzeit mit perfektem Cache

Zyklen · Taktzyklen verzögerte Anweisung Zyklen I· · Taktzyklen perfekte Anweisung

I· =

=

3,36 2

Die Leistung des perfekten Caches ist also um 3,36/2 = 1,68 besser.

254

Technische Informatik I

13.3. Messen und Verbessern der Cacheleistung Was passiert, wenn man den Prozessor schneller macht, das Speichersystem aber das Gleiche bleibt? Die Zeit, die man mit Speicheraussetzern verbringt, nimmt einen größeren Teil der Ausführungszeit ein. Einige einfache Beispiele zeigen, wie ernst dieses Problem sein kann. Nehmen wir an, dass wir die Maschine aus dem vorherigen Beispiel beschleunigen, indem wir die Anzahl der Taktzyklen pro Anweisung von 2 auf 1 verringern, ohne die Taktrate zu verändern (dies könnte zum Beispiel durch eine verbesserte Pipeline geschehen). Das System mit Cachefehlern hätte dann eine Zyklenanzahl pro Befehl von 1+1,36 = 2,36 und das System mit dem perfekten Cache wäre 2,36/1 = 2,36 Mal schneller. Der Anteil der Speicheraussetzer an der Ausführungszeit würde von 1,36/3,36 = 41 % auf 1,36/2,36 = 58 % steigen. Ähnlich ist es, wenn man die Taktrate erhöht, ohne das Speichersystem zu wechseln. Des erhöht ebenfalls den Leistungsverlust in Folge von Cachefehlern, wie das folgende Beispiel zeigt. Nehmen wir an, dass wir die Leistung der Maschine aus dem vorherigen Beispiel erhöhen, indem wir ihre Taktrate verdoppeln. Da sich der Hauptspeicher wahrscheinlich nicht ändern wird, können wir annehmen, dass sich die absolute Zeit zur Behandlung eines Cachefehlers ebenfalls nicht ändert. Wie viel schneller wird die Maschine mit dem schnelleren Takt sein, wenn wir die gleichen Fehlerraten wie im vorherigen Beispiel voraussetzen? In Maßen der schnelleren Taktzyklen ist die neue Fehlerstrafe doppelt so lang: 80 Zyklen. Folglich gilt: Gesamtfehlerzyklen = (2 % · 80) + 36 % · (4 % · 80) = 2,75 Anweisung Deswegen wird die schnellere Maschine bei Cachefehlern eine Zyklenanzahl von 2 + 2,75 = 4,75 pro Anweisung haben, verglichen mit dem Wert 3,36 für die langsamere Maschine. Benutzen wir die Formel für die Prozessorzeit, die wir beim vorherigen Beispiel erhielten, errechnet sich die relative Leistung wie folgt: 3,36 = 1,41 4,75 · 0,5

Deswegen ist die Maschine mit dem schnelleren Takt nur ungefähr 1,4 mal schneller statt exakt 2 mal, wie es der Fall gewesen wäre, würde sich der Effekt der Cachefehler nicht verschlimmern. Wie diese Beispiele veranschaulichen, erhöhen sich die relativen Cachefehlerstrafen, wenn die CPU schneller wird. Weiterhin verliert die Maschine gleich doppelt Leistung, wenn sie sowohl Taktrate als auch Zyklenzahl pro Anweisung verbessert: 1. Je geringer die Zyklenzahl pro Anweisung ist, desto stärker wirken sich Aussetzerzyklen aus 2. Die Leistung eines Hauptspeichersystems wird sich kaum so gut wie die eines Prozessors verbessern lassen. Wenn man die Zyklenzahl pro Anweisung errechnet, wird der Cachefehler in Prozessorzyklen gemessen, die für den Fehler gebraucht werden. Deswegen führt eine höhere Prozessortaktrate zu größeren Fehlerstrafen, wenn die Hauptspeicher zweier Maschinen die gleiche absolute Zugriffszeit haben

Technische Informatik I

255

13. Realisierung einer leistungsfähigen Speicherarchitektur Es ist ein Fehler, wenn man bei der Leistungsabschätzung das Cacheverhalten vernachlässigt. Wie wir noch sehen werden, hat die Verwendung von schnellen Pipelineprozessoren selbst in (relativ kostengünstigen) Desktoprechnern und Workstations zu ausgeklügelten Cachesystemen geführt. Die vorigen Beispiele und Gleichungen setzten voraus, dass die Trefferzeit kein Faktor für die Bestimmung der Cacheperformance ist. Klar ist, bei der Erhöhung der Cachezeiten erhöht sich ebenfalls die Gesamtzeit für den Zugriff auf ein Wort aus dem Speichersystem: Das verursacht unter Umständen einen Anstieg der Prozessorzykluszeit. Wir werden gleich weitere Beispiele dafür sehen, was die Trefferzeit erhöhen kann. Eine Möglichkeit ist auf alle Fälle die Vergrößerung des Caches. Ein größerer Cache könnte natürlich eine größere Zugriffszeit haben: Wenn der Schreibtisch in der Bibliothek (man erinnere sich bitte an das Beispiel vom Beginn des Kapitels) sehr groß wäre (vielleicht drei Quadratmeter...), würde es länger dauern, ein Buch auf dem Schreibtisch zu finden. Der Anstieg der Trefferzeiten (Dauer) bei einem wachsenden Cache beeinträchtigt irgendwann die Verbesserungen der Trefferrate. Dies führt dann zu einer Abnahme der Prozessorleistung. Der nächste Unterabschnitt bespricht andere Varianten für den Aufbau des Caches, welche die Fehlerrate verringern, aber manchmal die Trefferzeit erhöhen.

13.3.2. Reduzierung der Fehlerraten durch flexiblere Blockplatzierung Fall 1: Wenn bislang ein Block im Cache abgelegt wurde, haben wir ein einfaches Platzierungsschema verwendet: Ein Block kann nur an genau einer Stelle im Cache stehen. Dieses Schema heißt direkte Zuweisung, da es eine eindeutige Abbildung für jede Blockadresse im Speicher auf eine einzelne Stelle in der oberen Ebene der Hierarchie gibt. Es gibt in Wirklichkeit eine Vielzahl von Platzierungsschemen. Das eine Extrem ist die direkte Zuweisung, bei der ein Block an genau eine Stelle gelegt werden kann. Fall 2: Beim anderen Extrem kann ein Block an jede beliebige Position im Cache abgelegt werden. Solch ein Schema nennt man vollassoziativ. Hier wird ein Block im Speicher mit jedem Eintrag im Cache assoziiert (also verknüpft). Um einen bestimmten Block in einem vollassoziativen Cache zu finden, müssen alle Einträge im Cache durchsucht werden, da ein Eintrag an jeder Stelle des Caches liegen kann. Um die Suche praktikabel zu machen, wird sie parallel durchgeführt. Dazu ist jeder Eintrag mit einem Vergleicher ausgestattet. Diese Vergleicher (Komparatoren) erhöhen die Hardwarekosten erheblich, was die vollassoziative Platzierung nur für Caches mit einer kleinen Anzahl von Blöcken ökonomisch vertretbar macht. Eine Alternative von Entwürfen zwischen direkter Zuweisung und Vollassoziativität heißt mengenassoziativ. In einem mengenassoziativen Cache gibt es eine feste Anzahl von Stellen (mindestens zwei), in die jeder Block abgelegt werden kann. Ein mengenassoziativer Cache mit n Stellen für jeden Block heißt n-fach mengenassoziativer Cache. Ein n-fach mengenassoziativer Cache besteht aus einer Anzahl von Mengen, wobei jede Menge n Blöcke beinhaltet. Jeder Block im Speicher wird auf genau eine Menge abgebildet, die durch das Indexfeld der Adresse gegeben wird. Innerhalb dieser Menge kann der Speicherblock in jedem Cache-Block abgelegt werden Die Suche eines Cacheeintrags erfolgt nach folgendem Schema:

256

Technische Informatik I

13.3. Messen und Verbessern der Cacheleistung 1. Direkte Zuweisung einer Menge entsprechend der Indexbits der Adresse 2. Vergleich aller Tag-Einträge dieser Menge, da die Blockplatzierung innerhalb der Menge beliebig ist Man erinnere sich, dass bei der direkten Zuweisung die Stelle eines Speicherblocks wie folgt bestimmt wird: ¡ ¢ (Blocknummer) mod Anzahl der Cacheblöcke

In einem mengenassoziativen Cache wird die Menge, die den Speicherblock enthält, wie folgt bestimmt: ¡ ¢ (Blocknummer) mod Anzahl der Mengen im Cache

Da der Block in jedem Element der Menge abgelegt werden kann, müssen alle Elemente der Menge durchsucht werden. Bei einem vollassoziativen Cache hingegen kann ein Block überall hin gehen und alle Blöcke im Cache(!) müssen durchsucht werden. Abbildung 13.10 zeigt, wo Block 12 in einem Cache mit insgesamt acht Blöcken gelegt werden kann, wenn die Platzierungsstrategie direkte Zuweisung, zweifach mengenassoziativ und vollassoziativ ist. Direkte Zuweisung Block #

0 1 2 3 4 5 6 7

Mengenassoziativ Menge #

1

2

3

Daten

Daten

1 Tag

Daten

1 Tag

2 Suche

0

Vollassoziativ

1 Tag

2 Suche

2 Suche

Abbildung 13.10.: Vergleich der Cache-Strategien: Speicherblock 12 wird in einen Cache mit direkter Zuweisung, einen mengenassoziativen und einen vollassoziativen Cache gelegt. Bei der direkten Zuweisung gibt es nur einen Block im Cache, an dem Speicherblock 12 gefunden werden kann: 12 mod 8 = 4. Im zweifach mengenassoziativen Speicher würde es vier Mengen geben und Speicherblock 12 gehört in die Menge 12 mod 4 = 0. Dort kann er aber jeden Eintrag belegen. Im vollassoziativen Cache kann der Speicherblock mit der Adresse 12 in jedem der acht Cache-Blöcke auftauchen.

Technische Informatik I

257

13. Realisierung einer leistungsfähigen Speicherarchitektur 13.3.2.1. Blockplatzierungsstrategie Wir können uns jede Blockplatzierungsstrategie als eine Variation der Mengenassoziativität vorstellen. Ein Cache mit direkter Zuweisung ist nichts weiter als ein einfach mengenassoziativer Speicher: Jeder Cacheeintrag enthält einen Block und bildet eine Menge mit einem Element. Ein vollassoziativer Cache mit m Einträgen ist einfach ein n -facher mengenassoziativer Cache. Er hat eine einzige Menge mit m Blöcken und ein Eintrag kann sich in jedem Eintrag dieser Menge niederlassen. Abbildung 13.11 zeigt die möglichen Assoziativitätsstrukturen für einen Acht-Block-Cache. Einfach Mengenassoziativ (Direkte Zuweisung) Block Tag Daten

Zweifach mengenassoziativ Menge

Tag

Daten Tag

Daten

0 1 2 3

0 1 2 3 4 5 6 7

Vierfach mengenassoziativ Menge

Tag

Daten

Tag

Daten

Tag

Daten

Tag

Daten

0 1 Achtfach mengenassoziativ (vollassoziativ) Tag

Daten

Tag

Daten

Tag Daten

Tag

Daten Tag

Daten

Tag

Daten

Tag Daten

Tag

Data

Abbildung 13.11.: Acht-Block-Cache, der als direkte Zuweisung, zweifach mengenassoziativ oder vollassoziativ aufgebaut wurde. Die Gesamtgröße des Caches in Blöcken ist gleich die Anzahl der Mengen multipliziert mit dem Grad der Assoziativität. Folglich verringert eine Erhöhung der Assoziativität bei fester Cachegröße die Anzahl der Mengen, während sich gleichzeitig die Anzahl der Elemente pro Menge vergrößert. Bei acht Blöcken ist ein achtfach mengenassoziativer Cache das Gleiche wie ein vollassoziativer Cache. Der Vorteil der Erhöhung des Assoziativitätsgrades ist, dass dieser für gewöhnlich die Fehlerrate verringert, wie das nächste Beispiel zeigt. Der größte Nachteil ist ein Anstieg der Trefferzeiten. Dies schauen wir und in Kürze genauer an.

13.3.3. Beispiel: Assoziativität in Caches Es gibt drei kleine Caches, die jeweils aus vier Ein-Wort-Blöcken bestehen. Ein Cache ist vollassoziativ, der zweite zweifach mengenassoziativ und der dritte arbeitet mit einer direkten Zuweisung. Man bestimme die Anzahl der Cachefehler für jeden Cacheaufbau, die durch diese Folge von Blockadressen erzeugt werden: 0, 8, 0, 6, 8. Die direkte Zuweisung ist die einfachste. Bestimmen wir zuerst, welche Blockadressen auf welche Cache-Blöcke abgebildet werden: Blockadresse

Cache-Block

0

0 mod 4 = 0

6

6 mod 4 = 2

8

8 mod 4 = 0

258

Technische Informatik I

13.3. Messen und Verbessern der Cacheleistung Nun können wir die Cacheinhalte nach jedem Verweis füllen. Ein leerer Eintrag bedeutet, dass der Block ungültig ist und ein fettgedruckter Eintrag zeigt einen Neuzugang zum Cache. Adresse des Speicherblocks

Treffer oder

Inhalte der Cache-Blöcke nach jedem Zugriff

Fehler 0

1

2

0

Fehler

Speicher[0]

8

Fehler

Speicher[8]

0

Fehler

Speicher[0]

6

Fehler

Speicher[0]

Speicher[6]

8

Fehler

Speicher[8]

Speicher[6]

3

Die direkte Zuweisung erzeugt fünf Cachefehler. Der mengenassoziative Cache hat zwei Mengen (mit den Indizes 0 und 1) mit zwei Elementen pro Menge. Bestimmen wir zuerst, welcher Menge eine Blockadresse zugeordnet ist. Blockadresse

Cachemenge

0

0 mod 2 = 0

6

6 mod 2 = 0

8

8 mod 2 = 0

Da wir eine Wahl haben, welchem Eintrag einer Menge wir bei einem Fehler ersetzen, brauchen wir eine Ersetzungsregel. Mengenassoziative Caches ersetzen üblicherweise den am längsten ungebrauchten Block in einer Menge, d. h. den Block, der am wenigsten kürzlich benutzt wurde (least recently used, LRU). Wir werden diese Ersetzungsregel gleich genauer betrachten. Bei ihrer Verwendung würde der mengenassoziative Cache nach jeder Speichereferenz so aussehen: Adresse des Speicherblocks

Treffer oder

Inhalte der Cache-Blöcke nach jedem Zugriff

Fehler Menge 0

Menge 0

0

Fehler

Speicher[0]

8

Fehler

Speicher[0]

0

Treffer

Speicher[0]

Speicher[8]

6

Fehler

Speicher[0]

Speicher[6]

8

Fehler

Speicher[8]

Speicher[6]

Menge 1

Menge 1

Speicher[8]

Man beachte, dass bei der Referenz auf Block 6 der Block 8 ersetzt wird, da dieser länger nicht im Gebrauch als Block 0 war. Der zweifachassoziativer Cache hat insgesamt vier Fehler, einen weniger als der Cache mit der direkten Zuweisung. Der vollassoziative Cache hat vier Cache-Blöcke (in einer einzelnen Menge): Jeder Speicherblock kann in jedem Cache-Block abgelegt werden. Der vollassoziative Cache erbringt die beste Leistung: Nur drei Fehler.

Technische Informatik I

259

13. Realisierung einer leistungsfähigen Speicherarchitektur Adresse des Speicherblocks

Treffer oder

Inhalte der Cache-Blöcke nach jedem Zugriff

Fehler Menge 0

Menge 0

Menge 0

0

Fehler

Speicher[0]

8

Fehler

Speicher[0]

Speicher[8]

0

Treffer

Speicher[0]

Speicher[8]

6

Fehler

Speicher[0]

Speicher[8]

Speicher[6]

8

Treffer

Speicher[0]

Speicher[8]

Speicher[6]

Menge 0

Für diese Folge von Verweisen sind drei Fehler das bestmögliche Ergebnis, wenn auf drei einzelne Adressen zugegriffen wurde. Man beachte, dass wir bei acht Blöcken im Cache keine Ersetzungen im zweifach assoziativen Cache machen müssen (der Leser möge sich dies anhand einer entsprechende Tabelle selbst klar machen) und somit die gleiche Anzahl von Fehlern wie bei einem vollassoziativen Cache haben würden. Bei 16 Blöcken würden alle drei Caches die gleiche Anzahl von Fehlern haben. Dieser Veränderung der Fehlerrate zeigt uns, wie die Bestimmung der Cacheleistung von Cachegröße und Assoziativität abhängt. Wie sehr wird die Verkleinerung der Fehlerrate durch die Assoziativität bestimmt? Tabelle 13.6 zeigt die Verbesserungen für die Programme gcc und spice bei einem Paar von 64-KB-Caches (für Befehle und Daten jeweils einer) mit einem Vier-Wort-Block und einer Assoziativität, die von direkter Zuweisung bis hin zu vierfach reicht. Beim gcc verbessert der Schritt von der einfachen zur zweifachen Assoziativität die Gesamtfehlerrate um 20 %, aber es gibt keine weiteren Verbesserungen beim Umstieg auf vierfache Assoziativität. Die geringen Fehlerraten für spice lassen wenig Spielraum für Verbesserungen bei der Erhöhung der Assoziativität. Programm

Assoziativität

Anweisungsfehlerrate

Datenfehlerrate

Gesamtfehlerrate

gcc

1

2,0 %

1,7 %

1,9 %

gcc

2

1,6 %

1,4 %

1,5 %

gcc

4

1,6 %

1,4 %

1,5 %

spice

1

0,3 %

0,6 %

0,4 %

spice

2

0,3 %

0,6 %

0,4 %

spice

4

0,3 %

0,6 %

0,4 %

Tabelle 13.6.: Einfluss der Assoziativität auf die Cacheleistung bei gcc und spice

13.3.4. Finden eines Blocks im Cache Schauen wir uns nun die Aufgabe an, einen Block in einem mengenassoziativen Cache zu finden. Wie bei einem Cache mit direkter Zuweisung beinhaltet jeder Block im mengenassoziativen Cache ein Adress-Tag, dass die Blockadresse angibt. Das Tag jedes CacheBlocks in der entsprechenden Menge wird überprüft, um herauszufinden ob es mit der Blockadresse, die der Prozessor verlangt hat, übereinstimmt. Abbildung 13.12 zeigt, wie sich die Adresse zusammensetzt. Der Indexwert wird benutzt, um die Menge zu bestimmen, welche die gesuchte Adresse enthält. Dann müssen die Tags aller Blöcke dieser Menge untersucht werden. Da Geschwindigkeit von essentieller Wichtigkeit ist, werden alle Tags in der ausgewählten Menge parallel untersucht. Wie in einem vollassoziativen

260

Technische Informatik I

13.3. Messen und Verbessern der Cacheleistung Cache würde eine sequentielle Suche die Trefferzeit in einem mengenassoziativen Cache zu groß machen. Tag

Index

Block-Versatz

Abbildung 13.12.: Drei Teile einer Adresse in einem mengenassoziativen Cache Wenn die Gesamtgröße konstant gehalten wird, erhöht eine Vergrößerung der Assoziativität die Anzahl der Blöcke pro Menge, welche die Anzahl der gleichzeitig durchgeführten Vergleiche ist und die zur parallelen Suche durchgeführt werden: Jede Vergrößerung der Assoziativität um den Faktor zwei verdoppelt die Anzahl der Blöcke pro Menge und halbiert die Anzahl der Mengen. Entsprechend verringert jede Faktor-ZweiVergrößerung der Assoziativität des Index um ein Bit und erhöht die Tag-Größe um ebenfalls ein Bit. In einem vollassoziativen Cache gibt es nur eine Menge und alle Blöcke müssen gleichzeitig untersucht werden. Es gibt keinen Index und die gesamte Adresse außer dem Blockversatz wird mit dem Tag jedes Blockes verglichen. In anderen Worten: Wir durchsuchen den gesamten Cache ohne Indizierung. Bei einem Cache mit direkter Zuweisung wie in Abbildung 13.6 wird nur ein einziger Vergleicher benötigt, da der Eintrag in nur einem Block gefunden werden kann. Wir können also aufgrund der Indizierung einfach auf den Cache zugreifen. Bei einem vierfach mengenassoziativen Cache wie in Abbildung 13.13 werden vier Vergleicher benötigt, zusammen mit einem 4-zu-1-Multiplexer für die Auswahl zwischen den vier möglichen Elementen der ausgewählten Menge. Der Cachezugriff besteht aus der Indizierung der entsprechenden Menge und der anschließenden Durchsuchung der Mengenelemente. Die Kosten für einen assoziativen Cache sind die zusätzlichen Vergleicher und jede Verzögerung für die Vergleiche und Auswahl der Elemente der Menge. Die Vergleicher bestimmen, welches Element der ausgewählten Menge (falls vorhanden) mit dem Tag übereinstimmt. Die Ausgabe der Vergleicher werden benutzt, um einen der vier Blöcke aus der indizierten Menge auszuwählen, indem man einen Multiplexer mit einem dekodierten Auswahlsignal verwendet. Bei einigen Implementierungen werden die Aktivierungssignale der Datenteile der Cache-RAMs verwendet, um den Eintrag in der Menge zu bestimmen, der den Ausgang ansteuert. Die Aktivierungssignale kommen von den Vergleichern und verursachen die Ausgabe der übereinstimmenden Daten an den Ausgängen. Dieser Aufbau schafft die Notwendigkeit für einen Multiplexer ab.

13.3.5. Tag-Größe und Mengenassoziativität Die Vergrößerung der Assoziativität erfordert mehr Vergleicher sowie ein weiteres TagBit pro Cache-Block. Wenn wir von einem Cache mit 4K-Blöcken und einer 32-Bit-Adresse ausgehen, bestimme man die Gesamtanzahl der Mengen und Tag-Bits für Caches mit direkter Zuweisung, zwei-, vierfacher sowie voller Assoziativität. Der Cache mit der direkten Zuweisung hat die gleiche Anzahl Blöcke wie Mengen und folglich einen 12-Bit-Index, da log2 (4K) = 12. Folglich ist die Gesamtanzahl der Tag-Bits (32 − 12) · 4K = 80K. Jeder Assoziativitätsgrad verringert die Anzahl der Mengen um den Faktor zwei und verringert deswegen die Anzahl der Bits für die Indizierung des Caches um eins. Die Anzahl der notwendigen Bits für ein Tag wird also um eins erhöht. Folglich haben wir bei

Technische Informatik I

261

13. Realisierung einer leistungsfähigen Speicherarchitektur 31 . . . 16

15 . . . 4 3 2 1 0

22 Index

V Tag

Daten

8 V Tag

Daten

V Tag

Daten

V Tag

Daten

0 1 2 253 254 255 22 32 =

=

=

&

&

=

&

&

4-zu-1-Multiplexer

>=1

Treffer

Daten

Abbildung 13.13.: Implementierung eines vierfach assoziativen Caches erfordert vier Vergleicher und einen 4-zu-1-Multiplexer einem zweifach assoziativen Cache 2K Mengen, so dass die Gesamtanzahl der Tag-Bits (32 − 11) · 2 · 2K = 84K ist. Für einen vierfach mengenassoziativen Speicher haben wir eine Gesamtanzahl von 1K Mengen, was zu einer Gesamtanzahl der Tag-Bits von (32 − 10) · 4 · 1K = 88K führt. Für einen vollassoziativen Cache gibt es nur eine Menge mit 4K Blöcken und das Tag ist 32 Bits groß, was zu eine Gesamtanzahl von 32 · 1 · 4K = 128K Tag-Bits ergibt. 13.3.5.1. Entscheidung welcher Assoziativitätstyp Die Wahl zwischen direkter Zuweisung, Mengen- oder Vollassoziativität in einer Speicherhierarchie hängt von den Kosten eines Cachefehlers gegenüber den Kosten der Implementierung der Assoziativität ab, sowohl für Zeit als auch für Hardware.

13.3.6. Auswahl des zu ersetzenden Blocks Wenn ein Fehler in einem Cache mit direkter Zuweisung auftritt, kann der angeforderte Block nur an eine Stelle gehen, und der Block, der sich an dieser Stelle aufhält, muss ersetzt werden. In einem assoziativen Cache haben wir die Wahl, wohin der angeforderte Block platziert werden kann und folglich die Wahl, welcher Block zu ersetzen ist. In ei-

262

Technische Informatik I

13.3. Messen und Verbessern der Cacheleistung nem vollassoziativen Cache sind alle Blöcke potentielle Ersetzungskandidaten. In einem mengenassoziativen Cache hingegen müssen wir zwischen den Blöcken der ausgewählten Menge wählen. Das gebräuchlichste Ersetzungsverfahren nennt man Least Recently Used (LRU, „In letzter Zeit am wenigsten verwendet“), welches wir bereits im vorigen Beispiel verwendet haben. Beim LRU-Schema wird der Block ersetzt, der am längsten unbenutzt war. Die LRUErsetzung wird implementiert, indem man sich merkt, wann jedes Element einer Menge relativ zu den anderen Elementen verwendet wurde. Bei einem 2-fach mengenassoziativen Cache kann de Information, welcher Block zuletzt gebraucht wurde, so gewonnen werden, indem ein einzelnes Bit in Abhängigkeit davon, auf welchen Block zugegriffen wird, gesetzt oder zurückgesetzt wird. Wenn die Assoziativität steigt, wird die Implementierung schwieriger. Wir werden später noch auf eine weitere Ersetzungsstrategie zu sprechen kommen.

13.3.7. Verringerung der Fehlerstrafe durch mehrere Cache-Ebenen Alle modernen Rechner machen Gebrauch von Caches. Heutzutage sind die prozessornahen Caches im Prozessor selber untergebracht (Level-1- und Level-2-Caches); manchmal trifft man einen Level-3-Cache an, der auf der gleichen Platine wie der Prozessor untergebracht ist. Auf L2-Cache wird zugegriffen, wann immer ein Cachefehler im ersten Cache auftritt. Wenn dieser Second-Level-Cache die gewünschten Daten beinhaltet, wird die Fehlerstrafe lediglich die Zugriffszeit des Second-Level-Cache sein, die viel kleiner als die Zugriffszeit des Hauptspeichers ist. Wenn weder der primäre noch der sekundäre Cache die Daten enthalten, ist ein Hauptspeicherzugriff erforderlich und man handelt sich eine größere Fehlerstrafe ein. Wie erheblich ist die Leistungssteigerung durch die Verwendung eines Second-LevelCache? Das nächste Beispiel zeigt es.

13.3.8. Leistung von Mehrebenencaches Nehmen wir an, wir haben einen Prozessor mit einer Taktzyklenzahl pro Anweisung 1,0 (vorausgesetzt alle Verweise treffen im Primärcache) und einer Taktrate von 500 MHz1 . Setzen wir für die Hauptspeicherzugriffszeit 200 Nanosekunden voraus (inklusive der ganzen Fehlerbehandlung). Nehmen wir weiterhin an, dass die Fehlerrate bei den Anweisungen für den Primärcache 5 % beträgt. Wie viel schneller wird die Maschine, wenn wir einen zweiten Cache hinzufügen, der eine 20-ns-Zugriffszeit sowohl für Treffer als auch für Fehler hat und groß genug ist, um die Fehlerrate auf 2 % zu drücken? Die Fehlerstrafe für den Hauptspeicher ist 200 ns 2 ns pro Takt

= 100 Taktzyklen

Die effektive Zyklenzahl pro Anweisung ist bei einer Cache-Ebene Gesamtzyklenanzahl Zyklenanzahl = + Aussetzerzyklenzahl Anweisung Anweisung 1 500 MHz entspricht einer Taktzeit von 2 Nanosekunden.

Technische Informatik I

263

13. Realisierung einer leistungsfähigen Speicherarchitektur Für eine Maschine mit einer Cache-Ebene ergibt sich also Gesamtzyklenanzahl = 1,0 + 5 % · 100 = 6,0 Anweisung Mit zwei Cache-Ebenen kann ein Fehler im primären Cache entweder durch den sekundären Cache oder durch den Hauptspeicher behoben werden. Die Fehlerstrafe für den Zugriff auf den Second-Level-Cache ist 20 ns 2 ns pro Takt

= 10 Taktzyklen

Wenn der Taktzyklus durch den sekundären Cache behoben wird, ist dies schon die gesamte Fehlerrate. Wenn aufgrund des Fehlers auf den Hauptspeicher zugegriffen werden muss, ergibt sich die Gesamtstrafe aus der Summe der Zugriffszeit auf den sekundären Cache und der Zugriffszeit auf den Hauptspeicher. Folglich ist bei einem Zweiebenencache die Gesamtzyklenanzahl pro Anweisung die Summe der Zyklenanzahl für beide Cache-Ebenen und der Grundzyklenanzahl pro Anweisung. Gesamtzyklenanzahl Anweisung

= 1+

Primäre Aussetzer Sekundäre Aussetzer + Anweisung Anweisung

= 1 + 5 % · 10 + 2 % · 100 = 3,, 5

Somit ist die Maschine mit dem sekundären Cache 6,0/3,5 = 1,7 Mal schneller. Die Entwurfsüberlegungen für einen primären und einen sekundären Cache unterscheiden sich erheblich, da die Anwesenheit der anderen Caches die optimale Wahl im Vergleich zu einem normalen Einzelebenencache verändert. Eine Zweiebenencachestruktur erlaubt dem primären Cache, sich auf die Minimierung der Trefferzeit zu konzentrieren um einen kürzeren Taktzyklus zu erhalten, während sich der sekundäre Cache auf die Fehlerrate konzentriert, um die Strafe der langen Speicherzugriffszeiten zu verringern. Die Zusammenarbeit der beiden Caches erlaubt somit folgende Ausrichtung. Die Fehlerstrafe des primären Caches wird durch die Anwesenheit des sekundären Caches erheblich reduziert, was einen kleineren Cache mit einer höheren Fehlerrate zulässt. Für den sekundären Cache ist die Zugriffszeit mit Anwesenheit des primären Caches weniger wichtig, da die Zugriffszeit des sekundären Caches die Fehlerstrafe des primären Caches beeinflusst, statt direkt dessen Trefferzeit oder Prozessorzyklenzeit. Die Auswirkung dieser Veränderungen der beiden Caches kann man sich klar machen, indem man jeden Cache mit dem besten Entwurf für eine einzelne Cache-Ebene vergleicht. Im Vergleich zu einem Einzelebenencache ist der primäre Cache einer Mehrebenenstruktur oft kleiner. Weiterhin benutzt der primäre Cache oft eine kleinere Blockgröße, um kleinere Cachegrößen und geringere Fehlerstrafen zu erhalten. Im Vergleich dazu wird der sekundäre Cache oft größer als ein Einzelebenencache sein, da die Zugriffszeit des sekundären Caches nicht so kritisch ist. Bei einer größeren Gesamtgröße wird der sekundäre Cache oft größere Blöcke als bei einem entsprechenden Einzelebenencache haben.

264

Technische Informatik I

13.3. Messen und Verbessern der Cacheleistung Es gibt eine Reihe von Komplikationen bei der Verwendung von Mehrebenencaches. Eine davon ist, dass es mehrere Typen von Cachefehlern und dazugehörigen Fehlerstrafen gibt. Im obigen Beispiel sahen wir die primäre Fehlerrate und globale Fehlerrate, d. h. der Teil aller Verweise, der in allen Ebenen zu Cachefehlern führte. Es gibt auch eine Fehlerrate für den sekundären Cache, die durch das Verhältnis aller Fehler im sekundären Cache geteilt durch die Anzahl der Zugriffe bestimmt wird. Da der primäre Cache die Zugriffe filtert, (besonders die mit guter räumlicher und zeitlicher Lokalität) ist die lokale Fehlerrate des sekundären Caches viel größer als die globale Fehlerrate. Für das obige Beispiel können wir die lokale Fehlerrate des sekundären Caches so errechnen: 2 %/5 % = 40 %. Glücklicherweise ist es die Gesamtfehlerrate, die bestimmt, wie oft wir auf den Hauptspeicher zugreifen müssen. Weitere Komplikationen ergeben sich, wenn die Caches verschiedene Blockgrößen haben, um der größeren oder kleineren Gesamtgröße zu entsprechen. Gleichermaßen kann sich die Assoziativität des Caches verändern. Primäre Caches im Prozessor werden oft mit einer zwei- oder vierfachen Assoziativität gebaut, während äußere Caches kaum eine Assoziativität größer als zwei haben. Diese Veränderungen der Blockgröße und Assoziativität führen zu Komplikationen bei der Modellierung von Caches.

13.3.9. Zusammenfassung In diesem Abschnitt konzentrierten wir uns auf drei Themen: Cacheleistung, Verwendung von Assoziativität zur Verringerung der Fehlerraten und die Nutzung von Cachehierarchien zur Verkleinerung der Fehlerstrafen. Da die Gesamtanzahl der Zyklen eines Programms die Summe der Prozessorzyklen und Aussetzerzyklen ist, kann das Speichersystem einen erheblichen Einfluss aus die Ausführungszeit des Programms haben. Tatsächlich erhöht sich bei Beschleunigung des Prozessors (durch Verkleinerung der Zyklenzahl pro Anweisung oder Erhöhung der Taktrate) der relative Effekt der Aussetzerzyklen, wodurch ein gutes Speichersystem kritisch für das Erreichen hoher Leistung ist. Die Anzahl der Aussetzerzyklen hängt sowohl von der Fehlerrate als auch von der Fehlerstrafe ab. Die Herausforderung ist die Verkleinerung einer dieser Faktoren ohne erhebliche Beeinflussung anderer kritischer Faktoren in der Speicherhierarchie. Um die Fehlerrate zu verkleinern, haben wir die Verwendung von assoziativen Platzierungsstrategien untersucht. Solche Strategien können die Fehlerrate eines Caches reduzieren, indem sie eine flexiblere Belegung des Caches mit Blöcken zulassen. Vollassoziative Strategien erlauben, einen Block überall abzulegen, erfordern aber auch, dass jeder Block im Cache durchsucht wird, um eine Anforderung zu erfüllen. Diese Suche wird normalerweise durch einen Vergleicher pro Cache-Block und paralleles Durchsuchen der Einträge implementiert. Die Kosten für die Vergleicher machen große vollassoziative Caches unpraktikabel. Mengenassoziative Caches sind eine praktikable Alternative, da wir nur zwischen den Elementen einer einzigen Menge suchen müssen, die durch Indizierung ausgewählt wird. Mengenassoziative Caches ergeben eine Verbesserung bei der Trefferrate, sind aber ein wenig langsamer beim Zugriff (aufgrund der Vergleiche und der Auswahl zwischen den Elementen einer Menge). Ob ein Cache mit direkter Zuweisung oder ein mengenassoziative Cache eine bessere Leistung erbringt, hängt sowohl von der Technologie als auch von den Einzelheiten der Implementierung ab.

Technische Informatik I

265

13. Realisierung einer leistungsfähigen Speicherarchitektur Wir haben uns Mehrebenencaches angesehen, die zur Verkleinerung der Fehlerstrafen verwendet werden. Bei ihnen wird ein größerer sekundärer Cache zur Behandlung der Zugriffs- und Schreibfehlern eingesetzt die im primären Cache stattfinden. Second-LevelCaches wurden zur Grundausstattung als die Entwickler herausfanden, dass begrenzte Chipfläche und die Ziele der hohen Taktraten das Wachsen von primären Caches verhinderten. Der sekundäre Cache, der oftmals zehn- oder mehrfach größer als der Primärcache ist, fängt viele Zugriffe ab, die der Primärcache nicht bedienen konnte. In einem solchen Fall ist die Fehlerstrafe die Zugriffszeit auf den sekundären Cache (normalerweise weniger als 10 Zyklen), verglichen mit der Zugriffszeit auf den Speicher (normalerweise mehr als 40 Zyklen). Wie bei der Assoziativität muss beim Entwurf ein Kompromiss zwischen der Größe des sekundären Caches und seiner Zugriffszeit gefunden werden, welcher von einer Reihe von Implementierungsdetails abhängt.

13.4. Virtueller Speicher Im vorigen Abschnitt haben wir gesehen, wie Caching als Methode für den schnellen Zugriff auf kürzlich benutzte Teile des Codes und der Daten eines Programms wirkt. Ähnlich kann der Hauptspeicher als Cache für den Sekundärspeicher (normalerweise Festplatten) eingesetzt werden. Diese Technik heißt virtueller Speicher. Es gibt zwei Beweggründe für virtuellen Speicher: Die Möglichkeit zur effizienten und sicheren Aufteilung von Speicher zwischen mehreren Programmen und die Abschaffung der lästigen Grenze der kleinen begrenzten Menge an Hauptspeicher. Man stelle sich eine Anzahl von Programmen vor, die gleichzeitig auf einer Maschine laufen. Der Gesamtspeicher, der von allen Programmen benötigt wird, kann viel größer sein als der in der Maschine verfügbare reale Speicher; zu jedem Zeitpunkt wird aber nur ein Bruchteil dieses Speichers benötigt. Der Hauptspeicher braucht nur die aktiven Teile vieler Programme zu enthalten, genau wie der Cache nur die aktiven Teile eines einzelnen Programms enthalten muss. Dies erlaubt uns, sowohl Prozessor als auch Hauptspeicher effizient zu nutzen. Soll ein Haupspeicher von mehreren Programmen gleichzeitig genutzt werden, dürfen sich die Speicherbereiche, die den Programmen zugewiesen werden, nicht überlappen. Es muss sichergestellt sein, dass ein Programm nur den Teil des Hauptspeichers lesen und schreiben kann, der ihm zugewiesen wurde. Wenn wir ein Programm kompilieren, wissen wir nicht, mit welchen anderen Programmen es sich den Speicher teilen wird. Tatsächlich verändern sich die Programme, die sich den Speicher teilen, dynamisch, während sie laufen. Aufgrund dieser Wechselwirkungen würden wir jedes Programm gerne so kompilieren, dass es seinen eigenen Adressraum verwendet. Das heißt, nur auf einen abgegrenzten Bereich des Speichers kann von diesem Programm aus zugegriffen werden. Dieser Ansatz erzwingt den Schutz des Adressraums eines Programms vor anderen Programmen. Ein weiterer Akzeptanzgrund für virtuellen Speicher besteht darin, einem einzelnen Benutzerprogramm zu erlauben, die Größe des primären Speichers zu überschreiten. Früher musste der Programmierer ein Programm anpassen, wenn es zu groß für den Speicher wurde. Die Programmierer zerteilten Programme in Stücke und suchten dann die Stücke heraus, die sich gegenseitig ausschlossen. Diese Overlays (Überlagerungen) wurden während der Ausführung durch das Benutzerprogramm geladen und entfernt. Der Pro-

266

Technische Informatik I

13.4. Virtueller Speicher grammierer stellte sicher, dass ein Programm niemals auf ein Overlay zugreifen wollte, das nicht geladen war, und die Overlays die Gesamtgröße des Speichers nicht überschritten. Überlagerungen wurden üblicherweise als Module angeordnet, die sowohl Code als auch Daten beinhalteten. Die Aufrufe zwischen den Prozeduren verschiedener Module führten zu einer Überlagerung eines Moduls mit einem anderen. Wie man sich leicht vorstellen kann, ist diese Verantwortung eine erhebliche Last für den Programmierer. Der virtuelle Speicher wurde zu einer Entlastung der Programmierer. Er verwaltet die zwei Ebenen der Speicherhierarchie, die durch Hauptspeicher (manchmal physischer Speicher genannt, um ihn vom virtuellen zu unterscheiden) und Sekundärspeicher gebildet werden, automatisch. Einen virtuellen Speicherblock nennt man eine Seite (page) und ein virtueller Speicherfehler heißt Seitenfehler (page fault). Bei virtuellem Speicher erzeugt der Prozessor eine virtuelle Adresse, die durch Zusammenarbeit von Hard- und Software in eine physische Adresse umgewandelt wird und für den Zugriff auf den Hauptspeicher verwendet werden kann. Abbildung 13.14 zeigt den virtuell adressierten Speicher, dessen Seiten auf den Hauptspeicher abgebildet werden. Dieser Prozess heißt Speicherabbildung (memory mapping) oder Adressumsetzung. Heutzutage sind die beiden Speicherhierarchieebenen, die durch virtuellen Speicher kontrolliert werden, DRAMs und Festplatten. Wenn wir zu unser Bibliotheksanalogie zurückkehren, können wir uns die virtuelle Adresse als Titel eines Buches vorstellen und die physische Adresse als Position dieses Buches in der Bibliothek. Virtuelle Adressen

Physische Adressen

Adressumsetzung

Adressen auf der Festplatte

Abbildung 13.14.: Abbildung von virtuellen auf physische Adressen Der Prozessor erzeugt virtueller Adressen, während der Speicher den Zugriff über physische Adressen abwickelt. Sowohl physischer als auch virtueller Speicher sind in Seiten aufgeteilt, sodass eine virtuelle Seite auf eine physische Seite abgebildet wird. Es ist möglich, dass sich eine virtuelle Seite nicht im Hauptspeicher befindet und somit nicht auf eine physische Seite abgebildet wird. Die Seite liegt in dem Fall auf der Festplatte. Physische Adressen können gemeinsam genutzt werden, wenn man zwei virtuelle Adressen auf die gleiche Adresse abbildet. Diese Fähigkeit ermöglicht zwei verschiedenen Programmen, Daten oder Code gemeinsam zu verwenden. Virtueller Speicher vereinfacht das Laden eines Programms durch Verlagern bzw. Umlagern. Die Abbildung der virtuellen Adressen eines Programms auf physische Adressen erfolgt, bevor Hauptspeicherzugriffe stattfinden. Verlagerung war ursprünglich eine

Technische Informatik I

267

13. Realisierung einer leistungsfähigen Speicherarchitektur Technik, um Speicherraum zu verwalten, ohne virtuellen Speicher zu betreiben. Stattdessen muss das Betriebssystem nur eine ausreichende Anzahl von Seiten im Hauptspeicher finden. Früher erforderten Verlagerungsprobleme besondere Hardware und Unterstützung im Betriebssystem, heute bietet virtueller Speicher diese Funktion.

13.4.1. Adressumsetzungskonzepte Bei virtuellem Speicher wird eine Adresse in virtuelle Seitennummer und Seitenversatz (page offset) untergliedert (siehe Abbildung 13.15). Eine große virtuelle Seitenanzahl vermittelt die Vorstellung, einen erheblich größeren virtuellen als physischen Speicher zu haben. Die physische Adresse unterteilt sich in einen vorderen Teil, der die physische Seitennummer bildet, und einen hinteren Teil, der den seiteninternen Versatz darstellt; der Seitenversatz verändert sich bei der Adressumsetzung nicht. Die Seitengröße ergibt sich aus der Bitanzahl des Seitenversatzes. Die Anzahl der Bits der physischen Seiten bestimmt die Größe des adressierbaren physischen Hauptspeichers. 31

30

29

28

27

......

15

14

Virtuelle Adresse 13 12 11 10

9

8

Virtuelle Seitenzahl

...

3

2

1

0

3

2

1

0

Seitenversatz

Adressumsetzung

29

28

27

......

15

14

13

12

11

10

Physische Seitenzahl

9

8

... Seitenversatz

Physische Adresse

Abbildung 13.15.: Umrechung einer virtuellen in einer physische Adresse Im Beispiel von Abbildung 13.15 ist die Seitengröße ist 212 = 4K . Die übrigen 30 − 12 = 18 Bit adressieren die physischen Seiten; folglich gibt es in diesem Beispiel 218 = 256K Seiten · 4K bzw. einen Adressraum von 1 GB. Die virtuelle Adresse ist um zwei Bit länger, sodass der virtuelle Adressraum viermal so groß ist. Ein Seitenfehler, d. h. das Nachladen einer Seite von der Festplatte in den Hauptspeicher, ist teuer: Er kostet Millionen von Taktzyklen, in denen die CPU hauptsächlich darauf wartet, die angeforderten Daten geliefert zu bekommen. Daher wird bei der Entwicklung von virtuellen Speichersystemen in wesentlichem Maße auf die Fehlerstrafe geachtet. • Die Seiten sollten groß genug sein, um die hohen Zugriffszeiten amortisieren zu können. Üblich sind 4-KB-Seiten. Neuere Systeme unterstützen zum Teil riesige Seiten (huge pages) von 4 MB Größe (Intel x86) oder gar bis 256 MB Größe (IA-64). Sie werden aber eher im professionellen Rahmen eingesetzt. • Anordnungen, welche die Seitenfehlerrate verringern, sind wünschenswert. Die primär benutzte Methode ist eine vollassoziative Platzierung der Seiten. • Seitenfehler können durch Software behandelt werden, da der Mehraufwand im Vergleich zur Zugriffszeit der Platte klein ist. Weiterhin kann man ausgefeilte Algo-

268

Technische Informatik I

13.4. Virtueller Speicher rithmen in Software realisieren, welche die Platzierung der Seiten übernehmen, da sich selbst bei kleinen Senkungen der Fehlerrate die Kosten für diese Algorithmen bezahlt machen. • Die Verwendung von Durchschreiben bei Schreibzugriffen im virtuellen Speicher wird nicht funktionieren, da es zu lange dauert. Stattdessen verwenden virtuelle Speichersysteme das Zurückschreiben. Die nächsten Abschnitte befassen sich mit diesen Faktoren des Entwurfs virtueller Speicher. Die Betrachtung konzentriert sich auf die Seitenverwaltung, die Blöcke fester Größe verwendet. Es gibt auch ein Verfahren mit Blöcken variabler Größe, dass man Segmentierung nennt. Bei Segmentierung besteht die Adresse aus zwei Teilen: Einer Segmentnummer und einem Segmentversatz. Das Segmentregister wird auf eine physische Adresse gesetzt und der Versatz dazuaddiert, um die eigentliche physische Adresse zu finden. Da ein Segment sich in seiner Größe verändern kann, ist eine Grenzüberprüfung notwendig, um sicherzustellen, dass sich die physische Adresse noch im Segment befindet. Das Haupteinsatzgebiet für Segmentierung ist die Unterstützung mächtigerer Methoden für den Schutz und die gemeinsame Nutzung in einem Adressraum. Die meisten Betriebssystemlehrbücher enthalten weitreichende Betrachtungen über den Vergleich von Segmentierung und Seiteneinteilung und die Verwendung von Segmentierung zur logischen Unterteilung des Adressraums. Der größte Nachteil der Segmentierung ist, dass es den Adressraum in logisch getrennte Bereiche zerlegt, die durch eine zweiteilige Adresse (Segmentnummer und Versatz) verändert werden müssen. Die Seiteneinteilung macht dagegen die Grenze zwischen Seitennummer und Versatz für Programmierer und Compiler unsichtbar. Segmente wurden auch als Methode eingesetzt, den Adressraum zu erweitern, ohne die Wortgröße der Maschine zu verändern. Solche Versuche haben sich aufgrund der Schwerfälligkeit und den Leistungseinbußen, die durch eine zweiteilige Adresse entstehen, nicht durchgesetzt, da sich sowohl die Programmierer als auch die Compiler damit auseinandersetzen mussten. Viele Architekturen teilen den Adressraum in große Blöcke fester Größe auf, die den Schutz zwischen Adressraum und Benutzerprogrammen vereinfachen und die Effizienz der Seitenimplementierung erhöhen. Obwohl diese Teile auch oft Segmente genannt werden, ist dieser Mechanismus viel einfacher als die Segmentierung mit variabler Blockgröße und nicht sichtbar für die Nutzerprogramme. Wir werden dies gleich noch ausführlicher betrachten.

13.4.2. Platzierung und Wiederfinden einer Seite Durch die hohe Strafe für einen Seitenfehler würden Entwickler die Anzahl der Seitenfehler gerne durch die Platzierung verringern. Wenn wir zulassen, dass eine virtuelle Seite auf jede physische Seite abgebildet werden darf, kann das Betriebssystem, wenn ein Seitenfehler auftritt, jede beliebige Seite zum Ersetzen auswählen. Beispielsweise kann das Betriebssystem einen effizienten Algorithmus und komplexe Datenstrukturen verwenden, die laufend die Seitenbenutzung verfolgen, und eine Seite auswählen, die lange nicht benötigt wurde. Die Möglichkeit zur Verwendung eines guten und flexiblen Ersetzungsschemas ist die primäre Begründung für eine vollassoziative Belegung der Seiten. Natürlich reduziert sie auch die Seitenfehlerrate.

Technische Informatik I

269

13. Realisierung einer leistungsfähigen Speicherarchitektur Wie bereits erwähnt, besteht die Schwierigkeit bei vollassoziativer Platzierung darin, einen Eintrag zu finden, da er sich überall in der oberen Ebene der Hierarchie befinden kann. Eine Vollsuche ist unpraktikabel. In virtuellem Speicher finden wir Seiten durch eine Tabelle, die den Speicher indiziert. Diese Struktur heißt Seitentabelle. Die Seitentabellen werden im Hauptspeicher aufbewahrt. Sie werden mit der Seitennummer der virtuellen Adresse indiziert und enthalten die entsprechende physische Seitennummer. Jedes Programm hat seine eigene Seitentabelle, welche den virtuellen Adressraum dieses Programms auf den Hauptspeicher abbildet. In unserer Bibliotheksanalogie entspricht die Seitentabelle der Abbildung zwischen den Buchtiteln und den Positionen in der Bibliothek. Wie der Buchkatalog Einträge für ein Buch in einer anderen Bibliothek statt für den lokalen Bibliotheksflügel enthalten kann, werden wir sehen, dass die Seitentabelle Einträge für Seiten haben kann, die sich nicht im Speicher befinden. Um die Position der Seitentabelle im Speicher anzugeben, verfügt die Hardware über ein Register, welches auf den Anfang der Seitentabelle zeigt, das sogenannte Seitentabellenregister (page table register). Für die weitere Betrachtung nehmen wir an, dass die Seitentabelle sich an einer festen Stelle im Speicher befindet. Abbildung 13.16 stellt das Seitentabellenregister dar, die virtuelle Adresse und die angezeigte Seitentabelle zur Veranschaulichung, wie die Hardware eine physische Adresse bestimmen kann. Ein Gültigkeitsbit wird für jeden Seitentabelleneintrag verwendet, wie es auch im Cache der Fall ist. Wenn das Bit aus ist, ist die Seite nicht im Hauptspeicher anzufinden und ein Seitenfehler tritt auf. Wenn das Bit an ist, ist die Seite gültig und der Eintrag enthält die physische Seitennummer. Seitentabellenregister

31

30

29

28

27

14

15

......

Virtuelle Adresse 13 12 11 10 9

8

Virtuelle Seitenzahl 20 V

3

2

1

0

3

2

1

0

... Seitenversatz 12

Physische Seitenzahl

Seitentabelle

Wenn 0, dann befindet sich die Seite nicht im Speicher

18

29

28

27

......

15

14

13

12

11

10

Physische Seitenzahl

9

8

... Seitenversatz

Physische Adresse

Abbildung 13.16.: Die Seitentabelle wird mit der virtuellen Seitennummer indiziert, um den entsprechenden Teil des physischen Adresse zu erhalten Die Anfangsadresse der Seitentabelle wird durch den Seitentabellenzeiger angegeben. In dieser Abbildung ist die Seitengröße 212 Bytes oder 4 KB. Der virtuelle Adressraum ist 232 Bytes oder 4 GB und der physische Adressraum 230 Bytes, was einen Hauptspeicher

270

Technische Informatik I

13.4. Virtueller Speicher von bis zu 1 GB erlaubt. Die Anzahl der Einträge ist dann höchstens 220 oder 1 Million Einträge. Obwohl der hier gezeigte Seitentabelleneintrag nur 19 Bits breit ist, würde er normalerweise auf 32 Bits aufgerundet, um die Indizierung zu erleichtern. Die zusätzlichen Bits würden zur Speicherung zusätzlicher Informationen benutzt, die pro Seite beibehalten werden (z. B. für den Schutz). Da die Seitentabelle eine Abbildung für jede mögliche virtuelle Seite enthält, werden keine Tags benötigt. Bei der Cacheterminologie besteht der Index, welcher zum Zugriff auf die Seitentabelle verwendet wird, aus der gesamten Blockadresse, welche die virtuelle Seitennummer ist. Die Seitentabelle bestimmt zusammen mit dem Programmzähler und den Registern den Zustand eines Programms. Wenn wir einem weiteren Programm die Nutzung des Prozessors erlauben wollen, müssen wir den aktuellen Zustand des ersten Programms (in Registern) sichern. Nach Wiederherstellung dieses Zustandes kann das Programm die Ausführung fortsetzen. Wir bezeichnen diesen Zustand oft als einen Prozess. Ein Prozess wird als aktiv angesehen, wenn er in Besitz des Prozessors ist, ansonsten ist er inaktiv. Das Betriebssystem kann einen Prozess aktivieren, in dem es den Zustand des Prozesses inklusive des Programmzählers lädt. Dann wird die Ausführung, beginnend bei der Adresse (Wert) des zuvor gespeicherten Programmzählers, fortgesetzt wird. Der Adressraum des Prozesses (und somit auch alle Daten, auf die er im Hauptspeicher zugreifen kann) ist durch seine Seitentabelle festgelegt. Satt die gesamte Seitentabelle zu speichern, lädt das Betriebssystem das Seitentabellenregister so, dass es auf die Seitentabelle des Prozesses zeigt, der aktiviert werden soll. Jeder Prozess hat seine eigene Seitentabelle, da verschiedene Prozesse die gleichen virtuellen Adressen benutzen können. Das Betriebssystem ist für die Zuweisung des virtuellen Speichers und die Aktualisierung der Seitentabelle verantwortlich, so dass virtuelle Adressen verschiedener Prozesse nicht miteinander kollidieren. Wie wir noch sehen werden, bietet die Verwendung von getrennten Seitentabellen auch Schutz der Prozesse voreinander.

13.4.3. Seitenfehler Wenn das Gültigkeitsbit einer virtuellen Seite aus, d. h. 0 ist, tritt einen Seitenfehler auf. Die Übertragung der Seite wird durch einen Ausnahmebehandlung durchgeführt, dessen Details später noch besprochen werden. Sobald das Betriebssystem die Kontrolle übernimmt, muss es die Seite in der nächsten Ebene der Hierarchie (die Plattenposition) finden und entscheiden, wohin die angeforderte Seite im Hauptspeicher geht. Die virtuelle Adresse allein verrät uns nicht sofort, wo die Seite auf der Platte abgelegt ist. In unserer Analogie der Bibliothek entspricht dies folgender Situation: Wir können die Position eines Buches in den Regalen nicht allein durch Kenntnis seines Titels bestimmen. Stattdessen suchen wir in einem Katalog nach der Regal-Position, an der das Buch zu finden ist, anhand eines Labels bzw. einer Adresse. Gleichermaßen müssen wir in einem virtuellen Speichersystem verfolgen, mit welcher Plattenposition (Festplatten-Adresse) jede Seite des virtuellen Adressraums assoziiert ist. Da wir den Zeitpunkt nicht im Voraus kennen, zu dem eine Seite im Speicher für die Ersetzung ausgewählt wird, beschafft das Betriebssystem normalerweise Plattenplatz für alle Seiten eines Prozesses, wenn es diesen Prozess anlegt. Zu dieser Zeit erschafft es auch eine Datenstruktur für die Aufzeichnung, wohin jede virtuelle Seite auf der Platte gespeichert wird. Diese Datenstruktur kann Teil der Seitentabelle oder eine Hilfsdatenstruktur

Technische Informatik I

271

13. Realisierung einer leistungsfähigen Speicherarchitektur sein, die in der gleichen Weise wie die Seitentabelle adressiert wird. Abbildung 13.17 zeigt die Beziehungen einer Seitentabelle zum Hauptspeicher (dem physischen Speicher) und der Plattenadresse. Virtuelle Seitenzahl

Seitentabelle V

Physische Seite oder Festplatten-Adresse

Physischer Speicher

1 1 1 1 0 1 1 0 1 1 0 1

FestplattenSpeicher

Abbildung 13.17.: Die Speichertabelle bildet jede virtuelle Seite auf eine Seite im Hauptspeicher oder auf der Festplatte ab Ist das Gültigkeitsbit eines Eintrags gesetzt, dann enthält die Seitentabelle die physische Seitennummer. Anderenfalls befindet sich die zu suchende Seite auf der Platte und die Plattenadresse wird angegeben. Das Betriebssystem verfolgt durch eine spezielle Datenstruktur (Statistik), wie die physischen Seiten des Hauptspeichers durch Prozesse referenziert werden. Wenn ein Seitenfehler auftritt, weil alle Seiten im Hauptspeicher in Gebrauch sind, muss das Betriebssystem eine Seite für eine Ersetzung auswählen. Da wir die Anzahl der Seitenfehler minimieren wollen, versuchen die meisten Betriebssysteme eine Seite auszuwählen, die wahrscheinlich nicht in der näheren Zukunft benötigt wird. Indem sie die Vergangenheit zur Voraussage der Zukunft nutzen, folgen Betriebssysteme dem bereits vorgestellten LRUSchema. Das System gibt den Speicherplatz der in der jüngsten Vergangenheit am selten referenziertesten Seiten frei. Die Implementierung eines vollständig fehlerfreien LRU-Schemas ist zu teuer, da es eine Aktualisierung der Datenstruktur bei jedem Speicherverweis erfordert. Stattdessen nähern sich die meisten Betriebssysteme dem LRU an, indem sie verfolgen, welche Seiten kürzlich benutzt wurden und welche nicht. Damit das Betriebssystem die LRU-Seiten abschätzen kann, bieten einige Maschinen ein Referenzbit, welches bei jedem Zugriff auf die Seite gesetzt wird. Das Betriebssystem löscht diese Referenzbits periodisch und zeichnet dies auf, so dass es bestimmen kann, welche Seiten während einer bestimmten Periode verwendet wurden. Mit dieser Information kann das Betriebssystem eine Seite zwischen den am wenigsten verwendeten Seiten auswählen. Wenn dieses Bit nicht durch

272

Technische Informatik I

13.4. Virtueller Speicher die Hardware angeboten wird, muss das Betriebssystem einen anderen Weg für die Abschätzung finden, auf welche Seiten zugegriffen wurde. Die Anzahl der Seitentabelleneinträge n S ergibt sich als Quotient von Adressraumgröße A und Seitengröße S : n S = SA . Die Größe der Seitentabelle T lässt sich mithilfe der Größe pro Eintrag E berechnen: T = n S · E . Ein Beispiel: Angenommen, wir haben ein virtuelles 32-Bit-System, d. h. A = 232 , 4-KB-Seiten, d. h. S = 4K = 212 , und vier Bytes pro Seitentabelleneintrag, d. h. E = 22 , so wird die Größe der gesamten Seitentabelle wie folgt bestimmt: nS =

A 232 = = 220 S 212

Die Größe einer Seitentabelle ergibt sich entsprechend: T = n S · E = 220 · 22 = 4 M

Das heißt, dass wir zu jedem Zeitpunkt 4 MB Speicher für jedes Programm in Ausführung benötigen würden. Auf einer Maschine mit einer hohen Anzahl von aktiven Programmen und einer Seitentabelle fester Größe würde fast der gesamte Speicher für Seitentabellen beansprucht werden. Eine Reihe von Techniken wird verwendet, um die Menge des Speichers für die Seitentabellen zu reduzieren. Die einfachste Technik ist, ein Grenzregister zu benutzen, das die Größe der Seitentabelle für einen bestimmten Prozess begrenzt. Wenn die virtuelle Seitennummer größer als das Grenzregister wird, müssen Einträge zur Seitentabelle hinzugefügt werden. Diese Technik erlaubt, dass die Seitentabelle wächst, wenn ein Prozess mehr Speicher verbraucht. Folglich wird die Seitentabelle nur dann sehr groß, wenn der Prozess viele Seiten des virtuellen Speichers verwendet. Diese Technik erfordert es, dass der Adressraum sich nur in eine Richtung vergrößert.

13.4.4. Schreibzugriffe In einem virtuellen Speichersystem verbrauchen Schreibzugriffe auf die nächste Hierarchieebene (in diesem Fall die Festplatte) Millionen von Prozessorzyklen. Deswegen ist der Bau eines Schreibpuffers für das Durchschreiben absolut unpraktikabel. Stattdessen verwenden virtuelle Speichersysteme die Technik des Zurückschreibens. Hier werden die einzelnen Schreibzugriffe auf Speicherseiten durchgeführt und die Seite zurück auf die Platte kopiert, wenn sie im Speicher ersetzt werden muss. Dieses Zurückkopieren zur unteren Ebene der Hierarchie ist auch ein anderer Name für diese Schreibstrategie. Das Zurückschreiben hat einen weiteren großen Vorteil für das virtuelle Speichersystem. Da die Plattenübertragungszeit im Vergleich zur Zugriffszeit recht klein ist, ist das Zurückkopieren einer ganzen Seite viel effizienter als das Zurückschreiben einzelner Worte auf die Platte. Eine Zurückschreibenoperation ist trotz der erhöhten Effizienz gegenüber der Übertragung einzelner Worte teuer. Folglich möchten wir wissen, ob eine Seite wirklich zurückgeschrieben werden muss, wenn sie ersetzt wird. Um zu verfolgen, ob eine Seite beschrieben wurde, seit sie in den Speicher geladen wurde, wird ein Dirty-Bit zur Seitentabelle hinzugefügt. Diese Dirty-Bit wird gesetzt, wenn die Seite zum ersten Mal beschrieben wird. Will das Betriebssystem die Seite ersetzen, dann gibt das Dirty-Bit an, ob die Seite auf die Platte geschrieben werden muss, bevor die Speicherstelle an eine andere Seite vergeben wird.

Technische Informatik I

273

13. Realisierung einer leistungsfähigen Speicherarchitektur

13.4.5. Beschleunigung eines Speicherzugriffs mit dem TLB Da die Seitentabellen im Hauptspeicher hinterlegt werden, kann jeder Speicherzugriff eines Programms mindestens doppelt so lange dauern: Ein Speicherzugriff, um die physische Adresse herauszufinden und ein zweiter Zugriff, um die Daten zu holen. Der Schlüssel zur Verbesserung der Zugriffsleistung liegt in der Lokalität der Verweise zur Seitentabelle. Wenn eine Umsetzung für eine virtuelle Seitennummer verwendet wird, dann wird sie mit großer Wahrscheinlichkeit wieder benötigt, da die Verweise auf die Worte der Seite sowohl eine zeitliche wie räumliche Lokalität besitzen. Dementsprechend verfügen moderne Maschinen über einen speziellen Cache, der sich die jüngst verwendeten Umsetzungen merkt. Dieser spezielle Adressumsetzungscache wird üblicherweise als Übersetzungspuffer oder translation lookaside buffer (TLB) bezeichnet. Der TLB entspricht dem kleinen Zettel, auf dem man sich normalerweise die Regalposition einer Menge Bücher notiert, die man im Katalog gesucht hat. Statt jedes Mal erneut den Katalog zu durchsuchen, schreiben wir die Stellen mehrerer Bücher auf und benutzen den Zettel als Cache. Ein TLB ist ein Cache, der nur Seitentabellenumsetzungen enthält. Folglich enthält jeder Tag-Eintrag im TLB einen Teil der virtuellen Seitennummer und jeder Dateneintrag eine physische Seitennummer. Da wir nicht bei jedem Verweis auf die Seitentabelle zugreifen müssen und stattdessen auf den TLB zugreifen, muss der TLB weitere Bits (wie z. B. Referenz- oder Dirty-Bit) enthalten. Abbildung 13.18 zeigt, wie der TLB als Cache für Seitentabellenverweise fungiert. Der TLB enthält eine Untermenge der Adressabbildung aus der Seitentabelle. Da der TLB ein Cache ist, braucht er ein Tag-Feld. Wenn es im TLB keinen passenden Eintrag für eine Seite gibt, muss die Seitentabelle untersucht werden. Die Seitentabelle liefert entweder eine physische Seitennummer für den Cache (welche dann für den TLB-Eintrag verwendet werden kann) oder gibt an, dass die Seite sich auf der Platte befindet, wodurch dann ein Seitenfehler auftritt. Da Seitentabellen einen Eintrag für jede virtuelle Seite haben (also ist die Tabelle kein Cache), braucht sie kein Tag-Feld. Bei jeder Referenz schlagen wir die virtuelle Seitennummer im TLB nach. Wenn wir sie dort finden, wird die physische Seitennummer zur Bildung der Adresse verwendet und das Referenzbit der Seite wird gesetzt. Wenn der Prozessor schreibend zugreift, müssen wir auch noch das Dirty-Bit setzen. Tritt ein Cachefehler im TLB auf, müssen wir herausfinden, ob es sich um einen Seitenfehler oder lediglich um das Nichtvorhandensein des Eintrags im TLB handelt. Falls die Seite sich im Speicher befindet, deutet der TLB-Cachefehler darauf hin, dass nur der Eintrag für die Umsetzung fehlt. In diesem Fall kann der Prozessor den Fehler behandeln, indem er die Umsetzung von der Seitentabelle in den Prozessor lädt und dann die Speicherreferenz erneut ausführt. Wenn die Seite nicht im Speicher vorhanden ist, handelt es sich um einen echten Seitenfehler. In diesem Fall muss der Prozessor das Betriebssystem über einen Interrupt wecken. Da der TLB sehr wenige Einträge hat im Vergleich zur Anzahl Seiten im Speichersystem, werden TLB-Cachefehler viel häufiger auftreten als echte Seitenfehler. Nachdem ein Cachefehler im TLB aufgetreten ist und die fehlende Umsetzung aus der Speichertabelle empfangen wurde, müssen wir einen Eintrag im TLB zum Ersetzen eintragen. Da die Referenz- und Dirty-Bits im TLB-Eintrag enthalten sind, müssen auch diese in den Seitentabelleneintrag zurückkopiert werden, wenn wir den TLB-Eintrag ersetzen. Diese Bits sind die einzigen, die sich im Eintrag verändern können. Der Verwendung

274

Technische Informatik I

13.4. Virtueller Speicher TLB Virtuelle Seitenzahl

V

Tag

Physische Seitenadresse

Physischer Speicher

1 1 1 1 0 1

Seitentabelle

V

Physische Seite oder Festplatten-Adresse

Festplatten-Speicher

1 1 1 1 0 1 1 0 1 1 0 1

Abbildung 13.18.: TLB als Cache für Seitenumsetzungen einer zurückschreibenden Strategie (d. h. das Kopieren der Einträge vor dem Ersetzen statt bei jedem Schreiben darin) ist sehr effizient, da die Fehlerrate des TLB erwartungsgemäß sehr klein ist. Einige Systeme benutzen andere Techniken für die Anpassung der Referenz- und Dirty-Bits, welche die Notwendigkeit des Schreibens in den TLB außer beim Laden eines neuen Tabelleneintrags abschafft. Einige typische Werte für TLBs stehen in Tabelle 13.7. Größe Blockgröße Trefferzeit Fehlerstrafe Fehlerrate

32–4096 Einträge 1–2 Seitentabelleneinträge (normalerweise jeweils 4–8 Byte) 0,5–1 Taktzyklen 10–30 Taktzyklen 0,01 %–1 % Tabelle 13.7.: Typische Werte für TLBs

Entwickler haben eine große Vielfalt der Assoziativität für TLBs verwendet. Einige Systeme benutzen kleine vollassoziative TLBs, da eine vollassoziative Abbildung eine kleinere Fehlerrate hat. Weiterhin sind bei einem kleinen TLB die Kosten für die Vollassoziativität nicht so hoch. Andere Systeme benutzen große TLBs mit kleiner oder gar nicht vorhandener Assoziativität. Bei einer vollassoziativen Abbildung wird die Auswahl des zu ersetzenden Eintrags trickreich, da die Implementierung eines LRU-Schemas in Hardware zu teuer ist. Da

Technische Informatik I

275

13. Realisierung einer leistungsfähigen Speicherarchitektur weiterhin TILB-Cachefehler viel häufiger als echte Seitenfehler sind und kostengünstiger behandelt werden müssen können wir keinen teueren Softwarealgorithmus nutzen, wie das bei Seitenfehlern möglich ist. Das Ergebnis ist, dass einige Systeme Unterstützung für die zufällige Auswahl des zu ersetzenden Blocks bieten. 13.4.5.1. Der MIPS R2000 TLB Um zu sehen, wie diese Konzepte in der Praxis arbeiten, werfen wir einen genaueren Blick auf den TLB des MIPS R200, welcher in der DECstation 3100 verwendet wurde. Dieser TLB ist zwar sehr einfach, weist aber schon die meisten Eigenschaften von moderneren TLBs auf. Das Speichersystem verwendet 4-KB-Pages und einen 32-Bit-Adressraum. Folglich ist die virtuelle Seitennummer 20 Bits lang (siehe Abbildung 13.16). Die physische Adresse hat die gleiche Größe wie die virtuelle Adresse. Der TLB enthält 64 Einträge, ist vollassoziativ und wird gemeinsam für Befehle und Daten verwendet. Jeder Eintrag ist 64 Bits breit und einhält ein 20-Bit-Tag (welches die virtuelle Seitennummer für diesen TLB-Eintrag ist), die entsprechende physische Seitennummer, ein Gültigkeitsbit, ein Dirty-Bit und mehrere andere Verwaltungsbits. Abbildung 13.19 zeigt den TLB und einen der Caches, während Abbildung 13.20 die Schritte für die Bearbeitung von Lese- oder Schreibanforderungen zeigt. Wenn ein TLBCachefehler auftritt, speichert die MIPS-Hardware die Seitennummer der Referenz in einem speziellen Register und löst eine Ausnahmebehandlung aus. Diese ruft das Betriebssystem auf, das den Cachefehler behandelt. Um die physische Adresse für die fehlende Seite zu finden, indizieren die TLB-Fehlerroutinen die Seitentabelle mit der Seitennummer der virtuellen Adresse und des Seitentabellenregisters, das die Anfangsadresse der Seitentabelle des aktiven Prozesses enthält. Durch Verwendung einer speziellen Menge von Systemanweisungen, die den TLB aktualisieren können, setzt das Betriebssystem die physische Adresse aus der Seitentabelle in den TLB. Ein echter Seitenfehler tritt auf, wenn der Seitentabelleneintrag keine gültige physische Adresse besitzt. Ein TLB-Fehler kann in weniger als zehn Zyklen behoben sein, aber der Durchschnitt beträgt um die 16 Zyklen. Die Hardware verwahrt einen Index, der einen empfohlenen Eintrag für die Ersetzung angibt. Dieser Eintrag wird zufällig ausgewählt. Es gibt eine weitere Komplikation bei Schreibanforderungen: Das Schreibzugriffsbit des TLB muss überprüft werden. Dieses Bit hindert das Programm am Schreiben von Seiten, für die nur Lesezugriff gewährt wurde. Wenn das Programm schreiben will, und das Schreibzugriffsbit aus ist, wird eine Ausnahmebehandlung ausgelöst. Das Schreibzugriffsbit ist Teil des Schutzmechanismus, den wir gleich besprechen werden. Abbildung 13.19 zeigt den Aufbau des TLB und eines Caches des DECstation 3100. Das Schema konzentriert sich auf Lesezugriffe. Während der Cache mit direkter Zuweisung arbeitet, ist der TLB vollassoziativ. Das Implementieren eines vollassoziativen TLB erfordert, dass jedes Tag im TIB mit den indizierten Wert verglichen wird, da der gesuchte Eintrag überall im TLB sein kann. Wenn das Gültigkeitsbit des gesuchten Eintrags an, d. h. 1 ist, ist der Zugriff ein Treffer und die Seitenummer bildet zusammen mit dem Seitenversatz den Index, der für den Zugriff auf den Cache gebraucht wird. Wenn der TLB einen Treffer erzeugt, kann auf den Cache mit der resultieren physischen Adresse zugegriffen werden. Ist es ein Schreibzugriff, wird der Cacheeintrag überschrieben und die Daten in den Schreibpuffer übertragen. Man erinnere sich, dass Schreibfehler beim Cache der DECstation 3100 nicht auftreten können, da Ein-Wort-Blöcke und ein

276

Technische Informatik I

13.4. Virtueller Speicher Virtuelle Adresse 31 30 29 ........ 15 14 13 12

11 10 9 8 . . . . 3 2 1 0

Virtuelle Seitenzahl

Seitenversatz 12

V Dirty

Tag

Physische Seitenzahl

= = = = = =

20 Physische Seitenzahl Seitenversatz Physische Adresse BytePhysischer Adress-Tag Cache-Index Versatz 16 14

V

Tag

2

Daten

Cache

CacheTreffer

&

14

=

Daten

Abbildung 13.19.: TLB und Cache implementieren die Umsetzung einer virtuellen Adresse in ein Datum

durchschreibender Cache verwendet werden. Beim Lesen erzeugt der Cache einen Treffer oder einen Fehler und liefert die Daten oder setzt aus, während die Daten aus dem Speicher geholt werden. In Wirklichkeit enthält der TLB kein wirkliches Dirty-Bit. Stattdessen wird das Schreibzugriffsbit verwendet, um das erste Schreiben zu erkennen und dann das Dirty-Bit in der Seitentabelle zu setzen. Man beachte, dass ein TLB-Treffer und ein Cachetreffer unabhängige Ereignisse sind und der Cachezugriff gar nicht erst versucht wird, wenn ein TLB-Fehler auftritt. Weiterhin kann ein Cachetreffer nur auftreten, wenn ein TLB-Treffer aufgetreten ist, was bedeutet, dass die Daten im Speicher vorhanden sein müssen.

13.4.6. Zusammenwirken von virtuellem Speicher, TLB und Caches Unter den besten Umständen wird eine virtuelle Adresse durch TLB umgesetzt und in den Cache gesendet, wo die entsprechenden Daten gefunden, ausgelesen und zum Prozessor zurückgesandt werden. Im schlechtesten Fall kann ein Verweis in allen drei Ebe-

Technische Informatik I

277

13. Realisierung einer leistungsfähigen Speicherarchitektur Virtuelle Adresse

TLB-Zugriff

TLBZugriffsfehlerAusnahme

Nein

TLBTreffer?

Ja

Nein

Ja Schreiben?

Versuch, Daten vom Cache zu lesen

CacheZugriffsfehlerAusnahmeBehandlung

Nein

SchreibZugriffs-Bit an?

SchreibschutzAusnahme Nein

CacheTreffer?

Ja

Ja

Daten in Cache schreiben, Tag aktualisieren, Daten und Adress-Info in Schreibpuffer ablegen

Daten an CPU übertragen

Abbildung 13.20.: Verarbeitung eines Lese- oder Schreibzugriffs durch TLB und Cache nen der Speicherhierarchie fehlschlagen: Dem TLB, der Seitentabelle und dem Cache. Das Beispiel in Tabelle 13.8 veranschaulicht diese Wechselwirkungen genauer. Unser virtueller Speicher und die Cachesysteme arbeiten als Hierarchie zusammen, so dass die Daten nicht im Cache sein können, wenn sie sich nicht auch im Hauptspeicher befinden. Das Betriebssystem spielt eine wichtige Rolle bei der Verwaltung dieser Hierarchie, indem es die Inhalte jeder Seite aus dem Cache löscht, wenn es sich entscheidet, diese Seite auf die Platte auszulagern. Zur gleichen Zeit verändert das Betriebssystem die Seitentabellen und den TLB, und ein nachfolgender Zugriff auf die Daten dieser Seite wird einen Seitenfehler auslösen.

13.4.7. Implementierung von Schutz durch virtuellen Speicher Eine der wichtigsten Funktionen für virtuellen Speicher ist die Möglichkeit, einen einzelnen Hauptspeicher von mehreren Prozessen gemeinsam nutzen zu lassen, während Schutz zwischen diesen Prozessen und dem Betriebssystem besteht. Die Schutzmechanismen müssen sicherstellen, dass trotz der gemeinsamen Nutzung des Hauptspeichers durch mehrere Prozesse unerwünschte Prozesse nicht in den Adressraum eines anderen Benutzerprozesses oder des Betriebssystems schreiben kann, sei es nun willkürlich oder unwillkürlich. Wenn zum Beispiel ein Programm, das Zensuren von Studenten ver-

278

Technische Informatik I

13.4. Virtueller Speicher Cache

TLB

Virtueller Speicher

Möglich? Wenn ja, unter welchen Umständen?

× × × × p p p

× × p p

× p

× × p

× p

TLB versagt, kein Eintrag in Seitentabelle, Cache versagt TLB versagt, Eintrag aus Seitentabelle holen, Cache versagt Unmöglich, keine Umsetzung im TLB, wenn Seite nicht im Speicher Ja, obwohl die Seitentabelle bei TLB-Treffer niemals überprüft wird Unmöglich, Daten niemals im Cache, wenn Seite nicht im Speicher TLB versagt, Eintrag in Seitentabelle gefunden, danach im Cache Unmöglich, keine Umsetzung im TLB, wenn Seite nicht im Speicher

× p

×

Tabelle 13.8.: Beispiel zu Fehlern im Cache, TLB und virtuellen Speicher (× Fehler, p Treffer) waltet, gleichzeitig mit den Programmen der Studenten im Programmiergrundkurs auf derselben Maschine läuft, soll kein fremdes Programm Zensuren verändern dürfen. Das Schreibzugriffsbit im TLB kann das Schreiben einer Seite verhindern. Wir wollen weiterhin einen Prozess daran hindern, Daten eines anderen Prozesses zu lesen. Zum Beispiel würden wir es nicht wollen, dass das Programm eines Studenten die Zensuren liest, während sich diese im Hauptspeicher befinden. Sobald wir mit der gemeinsamen Nutzung des Hauptspeichers beginnen, müssen wir einem Prozess die Möglichkeit bieten, seine Daten sowohl vor dem Lesen als auch vor dem Schreiben eines anderen Programms zu schützen. Sonst wäre ein gemeinsamer genutzter Hauptspeicher ein schlechtes Konzept. Man erinnere sich, das jeder Prozess seinen eigenen virtuellen Adressraum besitzt. Folglich wäre bei einem Betriebssystem, welches die Seitentabellen so aufbaut, dass unabhängige virtuelle Seiten auf unterschiedliche physische Seiten abgebildet werden, kein Prozess in der Lage darf, auf die Daten eines anderen zuzugreifen. Natürlich erfordert dies auch, dass der Benutzerprozess keine Seitentabellenabbildung verändern darf. Das Betriebssystem kann dann Sicherheit garantieren, wenn es die Benutzerprozesse davon abhält, ihre eigenen Seitentabellen zu verändern. Dennoch muss das Betriebssystem selbst in der Lage sein, die Seitentabellen zu modifizieren. Das Ablegen der Seitentabellen in den Adressraum des Betriebssystems erfüllt beide Anforderungen. 2

13.4.8. Behandlung von Seitenfehlern und TLB-Cachefehlern Obwohl die Umsetzung von virtuellen in physische Adressen mit einem TLB bei TLBTreffern sehr geradlinig ist, ist die Behandlung von TLB- und Seitenfehlern komplexer. Ein TLB-Fehler tritt auf, wenn kein Eintrag im TLB mit der virtuellen Adresse zusammenpasst. Ein TLB-Fehler kann eine von zwei Möglichkeiten anzeigen: 1. Die Seite ist im Speicher und wir müssen nur den fehlenden TLB-Eintrag erzeugen. 2. Die Seite ist nicht im Speicher vorhanden und wir müssen die Kontrolle an das Betriebssystem übertragen. Wie wissen wir nun, welcher dieser beiden Fälle eingetreten ist? Wenn wir den TLBFehler behandeln, werden wir einen Seitentabelleneintrag suchen, der in den TLB gebracht werden kann. Wenn der passende Seitentabelleneintrag ein ausgeschaltetes Gül2 Dieses Thema wird in Betriebssystem-Lehrveranstaltungen tiefergehend besprochen.

Technische Informatik I

279

13. Realisierung einer leistungsfähigen Speicherarchitektur tigkeitsbit hat, ist die entsprechende Seite nicht im Speicher und wir haben einen Seitenfehler, statt nur einen einfachen TLB-Cachefehler. Wenn das Gültigkeitsbit an ist, können wir einfach die physische Seitennummer aus der Seitentabelle lesen und sie zur Erzeugung des TLB-Eintrags verwenden. Ein TLB-Cachefehler kann nun durch Hard- oder Software behandelt werden, da es nur eine kurze Folge von Aktionen erfordert, einen gültigen Seitentabelleneintrag vom Speicher in den TLB zu kopieren. Die Behandlung eines Seitenfehlers hingegen erfordert einen Ausnahmemechanismus zur Unterbrechung des aktiven Prozesses, Übertragung der Kontrolle an das Betriebssystem und späteren Wiederaufnahme des unterbrochenen Prozesses. Ein Seitenfehler wird manchmal in dem Taktzyklus erkannt, der zum Speicherzugriff ausgeführt wurde. Um den Befehle neu zu starten, nach dem der Seitenfehler behandelt wurde, muss der Programmzähler des Befehls, der den Seitenfehler ausgelöst hat, gespeichert werden. Dafür wird ein spezielles Register, der Ausnahmeprogrammzähler, verwendet. Sobald der Prozess, der den Seitenfehler ausgelöst hat, unterbrochen wurde und das Betriebssystem die Kontrolle übernommen hat, wird ein Ausnahmegrundregister zur Analyse der Ausnahme benutzt. Da es sich um einen Seitenfehler handelt, weiß das Betriebssystem, welche weitere Verarbeitung notwendig ist. Folglich speichert sie den gesamten Zustand des aktiven Prozesses. Dieser Zustand beinhaltet die Mehrzweck- und Fließkommaregister, das Seitentabellenregister, den Ausnahmeprogrammzähler und das Ausnahmegrundregister. Die virtuelle Adresse, die den Fehler hervorrief, ist assoziiert mit dem Fehlertyp: Befehls- oder Datenfehler. Die Adresse des Befehls, die den Fehler hervorrief, befindet sich im Ausnahmeprogrammzähler. Sobald das Betriebssystem die virtuelle Adresse kennt, die den Seitenfehler hervorrief, muss es drei Schritte ausführen: 1. Suche über die virtuelle Adresse den Seitentabelleneintrag und finde die Position der Seite auf der Platte 2. Wähle eine physische Seite für die Ersetzung aus. Wenn die ausgewählte Seite beschrieben wurde, ist sie auf die Platte zu schreiben, bevor wir eine neue virtuelle Seite hinein legen können. 3. Beginne mit dem Lesen der Seite von der Platte in die ausgewählte physische Seite des Hauptspeichers. Natürlich wird der letzte Schritt Millionen von Prozessortaktzyklen dauern (genau wie der zweite, wenn die Seite beschrieben wurde). Dementsprechend wird das Betriebssystem üblicherweise einen anderen Prozess für die Ausführung auswählen, bis der Plattenzugriff abgeschlossen ist. Da das Betriebssystem den Zustand des Prozesses gespeichert hat, kann des den Prozessor bereitwillig an einen anderen Prozess geben. Wenn das Lesen der Seite von der Platte abgeschlossen ist, kann das Betriebssystem den Zustand des Prozesses wiederherstellen, der ursprünglich den Seitenfehler ausgelöst hat, und die Befehl zum Verlassen der Ausnahmebehandlung ausführen. Dieser Befehl wird den Prozessor vom System- in den Benutzermodus zurücksetzen und den Programmzähler wiederherstellen. Der Benutzerprozess führt dann den fehlgeschlagenen Befehl erneut aus, wobei er diesmal erfolgreich auf die Seite zugreift, und setzt die Ausführung fort. Seitenfehlerausnahmen für Datenzugriffe sind aufgrund der Verbindung dreier Eigenschaften schwer zum implementieren: Sie treten inmitten von Befehlen auf. Der Befehl

280

Technische Informatik I

13.4. Virtueller Speicher kann nicht beendet werden, bevor die Ausnahme behandelt wurde. Nach der Behandlung muss der Befehl neu gestartet werden. Befehle neu starten zu können, sodass die Ausnahme behandelt wird und die Ausführung später fortgesetzt werden kann, ist bei einer RISC-ähnlichen Struktur recht einfach. Da jeder Befehl nur ein Datum schreibt und dieser Schreibzugriff am Ende des Anweisungszyklus auftritt, kann der Befehl am Beenden gehindert werden. Dies geschieht, in dem das Schreiben unterlassen und die Anweisung an ihrem Beginn neu gestartet wird. Für Maschinen mit viel komplexeren Befehlen, die viele Speicherstellen berühren und viele Datenworte schreiben wollen, ist das Neustarten von Befehlen viel schwieriger. Die Verarbeitung eines Befehls kann eine Reihe von Seitenfehler mitten in der Befehlsabarbeitung erzeugen. Zum Beispiel haben manche Maschinen Blockkopierbefehle, die Tausende von Datenworten berühren. Bei solchen Maschinen können Befehle oft nicht vom Beginn an neu gestartet werden, wie es beim RISC der Fall ist. Stattdessen wird der Befehl unterbrochen und später fortgesetzt. Die Fortführung eines Befehls in der Mitte seiner Ausführung erfordert das Speichern eines speziellen Zustands, die Verarbeitung der Ausnahme und die Wiederherstellung des speziellen Zustands. Dies alles sauber zu entwerfen erfordert vorsichtige und genaue Abstimmung zwischen Ausnahmebehandlungscode im Betriebssystem und der Hardware. Zwischen dem Zeitpunkt, an dem wir mit der Ausnahmebehandlung im Betriebssystem beginnen, und dem Zeitpunkt, zu dem das Betriebssystem den gesamten Zustand des Prozesses gesichert hat, ist das Betriebssystem besonders verwundbar. Wenn zum Beispiel eine zweite Ausnahme auftritt, während wir die erste Ausnahme im Betriebssystem behandeln, würde die Kontrolleinheit den Ausnahmeprogrammzähler überschreiben, wodurch eine Rückkehr zum Befehl, welche die erste Ausnahme auslöste, unmöglich wird. Wir können diese Katastrophe verhindern, indem wir eine Möglichkeit zum Ein- und Ausschalten der Ausnahmen geben. Wenn die erste Ausnahme eintrifft, setzen wir ein Bit, welches alle anderen Ausnahmen ausschaltet. Diese könnte zur gleichen Zeit geschehen wie der Einstieg in den Systemmodus des Prozessors. Das Betriebssystem wird dann gerade soviel vom Zustand sichern, um ihn wiederherzustellen, wenn ein anderer Befehl auftritt (sprich: Ausnahmeprogrammzähler und Ausnahmegrundregister). Das Betriebssystem kann dann die Ausnahmen wieder anschalten. Diese Schritte stellen sicher, dass die Befehle den Prozessor nicht zum Verlust von Zuständen veranlassen und somit die Wiederaufnahme einer unterbrochenen Ausführung unmöglich machen.

13.4.9. Blockplatzierung Wir haben gesehen, kann man für die Platzierung der Blöcke in der oberen Ebene der Hierarchie eine Reihe von Strategien anwenden; von direkter Zuweisung über Mengenassoziativität bis hin zur Vollassoziativität. Wie oben erwähnt, können alle Strategien als Variationen der mengenassoziativen Abbildung betrachtet werden, wobei sich die Anzahl der Mengen und die Anzahl der Blöcke pro Menge verändert: Strategie Direkte Zuweisung Mengenassoziativ Vollassoziativ

Anzahl der Mengen Anzahl der Cache-Blöcke Anzahl der CacheBlöcke / Assoziativität 1

Blöcke pro Menge 1 Grad der Assoziativität (normalerweise 2–8) Anzahl der Cache-Blöcke

Technische Informatik I

281

13. Realisierung einer leistungsfähigen Speicherarchitektur Der Vorteil bei der Erhöhung der Assoziativität ist üblicherweise eine Verkleinerung der Fehlerraten. Diese Verbesserung entsteht durch die Reduzierung von Fehlern. Schauen wir uns an, wie sehr sich diese Änderung auswirkt. Abbildung 13.21 zeigt die Daten für die Auslastung (bestehend aus den SPEC92 Benchmark-Tests) mit Caches von 1 KB bis hin zu 128 KB, die mit direkter Zuweisung bis hin zu achtfacher Mengenassoziativität betrieben werden. Den größten Gewinn erhält man aus dem Umstieg von direkter Zuweisung auf zweifache Mengenassoziativität, welche eine Verringerung der Fehlerrate zwischen 20 % und 30 % ergibt. Wenn die Cachegröße wächst, ist die relative Verbesserung durch Assoziativität nur unwesentlich oder gar nicht vorhanden. Da die Gesamtfehlerrate eines größeren Caches jedoch geringer ist, sinkt die Möglichkeit zur Verbesserung der Fehlerrate und die absolut gemessene Verbesserung durch Erhöhung der Assoziativität sinkt erheblich. Die möglichen Nachteile der Assoziativität sind erhöhte Kosten und langsamere Zugriffe.

Abbildung 13.21.: Fehlerraten für verschiedene Cachegrößen bei Änderung der Assoziativität

13.4.10. Blockidentifizierung Die Möglichkeiten, wie wir einen Block finden, hängen von der Platzierungsstrategie ab, da sie die Anzahl der möglichen Positionen bestimmen. Wir können dies so zusammenfassen: Strategie Finden der Blöcke Benötigte Vergleiche Direkte Zuweisung Über einen Index 1 Mengenassoziativ Index zur Menge, SuGrad der Assoziativität che in der Menge Suche alle Cache-Einträge Größe des Caches Vollassoziativ Eigene Tabelle 0 Die Wahl zwischen direkter Zuweisung, mengenassoziativer oder vollassoziativer Abbildung in jeder Speicherhierarchie hängt von den Kosten eines Fehlers gegenüber den Kosten zur Implementierung der Assoziativität ab, womit sowohl die Zeit als auch die

282

Technische Informatik I

13.4. Virtueller Speicher zusätzliche Hardware gemeint ist. Im Allgemeinen lohnt sich die Implementierung eines hohen Grades der Assoziativität in Caches nicht, da die Kosten für die Vergleiche weiter steigen, während die Verbesserungen der Fehlerrate nur gering sind. Vollassoziative Caches sind unerschwinglich teuer, außer bei sehr kleinen Caches, bei denen die Kosten für die Vergleiche nicht dominieren und die absoluten Verbesserungen der Fehlerrate die größten sind. In virtuellen Speichersystemen wird eine getrennte Abbildungstabelle (die Seitentabelle) verwendet, um den Speicher zu indizieren. Zusätzlich zum Speicherplatz für die Tabelle benötigt man bei Verwendung einer Tabelle einen weiteren Speicherzugriff. Die Wahl für Vollassoziativität und der zusätzlichen Tabelle wird durch vier Fakten begründet. 1. Vollassoziativität macht sich sehr bezahlt, da Fehler sehr teuer sind. 2. Vollassoziativität erlaubt der Software, ausgeklügelte Ersetzungsmechanismen zu benutzen, um die Fehlerrate zu senken. 3. Die volle Abbildung kann ohne zusätzliche Hardware leicht indiziert werden, ohne dass eine Suche notwendig ist. 4. Die hohe Seitengröße bedeutet, dass der Mehraufwand für die Seitentabellengröße relativ klein ist. (Die Verwendung einer separaten Suchtabelle, wie es die Seitentabelle für den Cache ist; wäre für einen Cache nicht zu gebrauchen, da die Tabelle viel größer als die Seitentabelle und dabei nicht einmal schneller verfügbar wäre.) Deswegen verwenden virtuelle Speichersysteme immer eine vollassoziative Platzierung. Mengenassoziative Mechanismen werden oft für Caches und TLBs verwendet, wo der Zugriff sich aus Indizierung und der Suche in einer kleinen Menge zusammensetzt. Viele heutige Systeme haben direkte Zuweisung verwendet, da sie die Vorteile von kleinen Zugriffszeiten und einfacher Struktur haben. Der Vorteil bei der Zugriffszeit resultiert aus der Tatsache, dass das Finden eines Blockes nicht von einem Vergleich abhängt. Solche Entwurfsentscheidungen hängen von vielen Details der Implementierung ab, z. B. ob sich der Cache mit auf dem Prozessorcache befindet oder nicht, von der verwendeten Technologie und von die kritischen Rolle der Cachezugriffszeit zur Bestimmung der Prozessorzyklenzeit.

13.4.11. Blockersetzung Wenn ein Fehler in einem assoziativen Cache auftritt, müssen wir uns entscheiden, welche Block ersetzt werden soll. In einem vollassoziativen Cache sind alle Blöcke potentielle Kandidaten für die Ersetzung. Wenn der Cache mengenassoziativ ist, müssen die Blöcke der Menge durchsucht werden. Wir haben bereits die beiden primären Strategien für die Ersetzung in einem mengenoder vollassoziativen Cache erwähnt: • Zufällig: Die zu ersetzenden Blöcke werden zufällig ausgewählt, möglicherweise mit Unterstützung der Hardware • Least Recently Used (LRU): Der zu ersetzende Block ist der, der die längste Zeit nicht in Gebrauch war. In der Praxis ist LRU zu kostspielig, um ihn eine Hierarchie mit mehr als nur einem kleinen Grad der Assoziativität (2 bis 4) zu integrieren, da der Speicher der Information zu aufwendig ist. Sogar bei vierfacher Mengenassoziativität wird LRU häufig nur angenähert, in dem man zum Beispiel verfolgt, welche Blockpaare zuletzt verwendet wurden

Technische Informatik I

283

13. Realisierung einer leistungsfähigen Speicherarchitektur (was nur ein Bit erfordert), um dann zu verfolgen, welcher Block in einem solchen Paar zuletzt verwendet wurde (was ein Block pro Paar erfordert). Bei noch größerer Assoziativität wird LRU noch gröber angenähert oder gleich durch zufällige Auswahl ersetzt. Bei Caches muss die Ersetzung in Hardware implementiert werden, weswegen die Strategie sehr einfach zu implementieren sein sollte. Zufällige Ersetzung lässt sich leicht in Hardware gießen, und für einen zweifach mengenassoziativen Cache hat die zufällige Ersetzung nur eine um den Faktor 1,1 höhere Fehlerrate als die Ersetzung durch LRU. Da die Caches immer größer werden, fällt die Fehlerrate für beide Ersetzungsstrategien und die absolute Differenz wird sehr klein. Tatsächlich ist die zufällige Ersetzung manchmal besser als einfache Annäherungen durch LRU, die z. B. in Hardware implementiert werden. Bei virtuellem Speicher wird immer eine Form dem LRU angestrebt, da selbst winzige Verkleinerungen der Fehlerrate wichtig sein können, wenn die Kosten für einen Fehler so groß sind. Referenzbits oder gleiche Funktionen werden oftmals von vornherein angeboten, um dem Betriebssystem das Verfolgen eine Menge von weniger gebräuchlichen Seiten zu finden. Da Fehler so teuer und relativ selten sind, ist die Annäherung an diese Information durch Software durchaus akzeptabel.

13.4.12. Schreibstrategie Eine Schlüsseleigenschaft jeder Speicherhierarchie ist ihre Behandlung von Schreibzugriffen. Wir haben bereits zwei grundlegende Möglichkeiten kennen gelernt: • Durchschreiben: Die Information wird sowohl in den Block im Cache als auch in den Block in der unteren Ebene der Speicherhierarchie geschrieben. • Zurückschreiben: Die Information wird nur in den Cache-Block geschrieben. Der veränderte Block wird nur dann in die untere Hierarchieebene geschrieben, wenn er ersetzt wird. Virtuelle Speichersysteme verwenden immer Zurückschreiben. Sowohl Zurückschreiben als auch Durchschreiben haben ihre Vorteile. Die wichtigsten Vorteile beim Zurückschreiben sind: • Individuelle Worte können durch den Prozessor mit der Frequenz geschrieben werden, mit der sich durch den Cache (und nicht durch Speicher) akzeptiert werden können. • Mehrere Schreibzugriffe in einem Block erfordern nur ein einziges Schreiben in die untere Ebene der Hierarchie. • Wenn Blöcke zurückgeschrieben werden, kann das System eine hohe Übertragungsbreite voll ausnutzen, da der gesamte Block geschrieben wird. Durchschreiben hat die folgenden Vorteile: • Fehler sind einfacher und günstiger handhabbar, da sie niemals das Zurückschreiben eines Blockes in die untere Ebene erfordern. • Durchschreiben ist einfacher zu implementieren als Zurückschreiben, obwohl in Hochleistungssystem ein zurückschreibender Cache einen Schreibpuffer benutzen muss. In virtuellen Speichersystemen ist nur das Zurückschreiben praktikabel, da die Wartezeit für das Schreiben in die untere Ebene viel zu groß ist. Da sich die Leistung von Prozessoren schneller erhöht als DRAM, übersteigt die Rate, mit der Prozessoren Schreibzugriffe

284

Technische Informatik I

13.5. Zusammenfassung Speicherarchitektur erzeugen, die Rate, mit der das Speichersystem sie verarbeiten kann. Das gilt auch wenn man den Speicher physisch und logisch verbreitert. Als Folge davon werden mehr und mehr Caches in Zukunft die zurückschreibende Strategie verwenden.

13.5. Zusammenfassung Speicherarchitektur Virtueller Speicher ist der Name für die Ebene der Speicherhierarchie, die das Zwischenlagern zwischen Hauptspeicher und Festplatte verwaltet. Virtueller Speicher erlaubt es einem einzelnen Programm, den Adressraum über die Grenzen des Hauptspeichers auszudehnen. Viel wichtiger in heutigen Rechnersystemen ist, dass virtueller Speicher die gemeinsame Nutzung des Hauptspeichers durch mehrere, gleichzeitig aktive Prozesse erlaubt, welcher zusammen weit mehr Hauptspeicher erfordern, als verfügbar ist. Um diese Teilung zu unterstützen, bietet virtueller Speicher auch Mechanismen zum Speicherschutz. Die Verwaltung der Speicherhierarchie zwischen Hauptspeicher und Platte ist eine Herausforderung, da die Seitenfehler einen hohen Preis haben. Mehrere Techniken werden verwendet, um die Fehlerrate zu verkleinern: 1. Blöcke, Seiten genannt, werden groß gemacht, um Vorteil aus räumlicher Lokalität zu ziehen und die Fehlerrate zu verkleinern. 2. Die Abbildung zwischen virtuellen und physischen Adressen, welche durch eine Seitentabelle implementiert wird, ist vollassoziativ, so dass eine virtuelle Seite überall im Hauptspeicher liegen kann. 3. Das Betriebssystem benutzt Techniken wie LRU und ein Referenzbit, um die eine Seite für die Ersetzung auszuwählen. Schreibzugriffe auf die Platte sind ebenfalls teuer, und der virtuelle Speicher verwendet das Zurückschreiben und verfolgt, ob eine Seite unverändert ist (durch Verwendung eines Dirty-Bits). So wird unnötiges Schreiben von Seiten zurück auf die Platte vermieden. Der virtuelle Speichermechanismus bietet auch eine Adressumsetzung einer virtuellen Adresse eines Programms in eine physische Adresse für den Zugriff auf den Speicher. Diese Adressumsetzung erlaubt geschützte und gemeinsame Nutzung des Hauptspeichers und bietet mehrere zusätzliche Vorteile, wie z. B. eine vereinfachte Speicherzuweisung. Um sicherzustellen, dass Prozesse voreinander geschützt werden, ist es erforderlich, dass nur das Betriebssystem die Adressumsetzungen verändern kann und Zugriffe von Benutzerprogrammen auf die Seitentabellen verhindert werden. Die kontrollierte gemeinsame Nutzung von Seiten von mehreren Prozessen kann durch Hilfe des Betriebssystems und Zugriffsbits in der Seitentabelle implementiert werden, die angeben ob ein Benutzerprogramm Lese- oder Schreibzugriff auf eine Seite hat. Wenn ein Prozessor jedes Mal auf Seitentabellen im Hauptspeicher zugreifen müsste um einen Zugriff umzusetzen, würde virtueller Speicher unnötige Arbeit verrichten. Stattdessen wird ein TLB als Cache für die Umsetzungen aus der Seitentabelle verwendet. Jede Adresse wird dann durch die Umsetzungen im TLB von einer virtuellen in eine physische Adresse umgewandelt. In einem Cache kann man Fehler in eine von drei Kategorien einteilen:

Technische Informatik I

285

13. Realisierung einer leistungsfähigen Speicherarchitektur • Obligatorische Fehler: Dies sind die Cachefehler, die durch den ersten Zugriff auf einen Block ausgelöst werden, die bisher noch nie im Cache war. Man nennt sie auch Kaltstartfehler. • Kapazitätsfehler: Diese sind Cachefehler, die ausgelöst werden, wenn der Cache nicht alle Blöcke enthalten kann, die während der Ausführung eines Prozesses benötigt werden. Kapazitätsfehler treten auf, da Blöcke ersetzt und später beim Zugriff neu geladen werden. • Konfliktfehler: Diese Cachefehler entstehen in einem mengenassoziativen Cache oder in einem Cache mit direkter Zuweisung, wenn mehrere Blöcke in die gleiche Menge wollen. Konfliktfehler, die nur in einem Cache mit direkter Zuweisung oder Mengenassoziativität entstehen, können in einem vollassoziativen Cache mit gleicher Größe behoben werden. Man nennt sie auch Kollisionsfehler. Die Herausforderung beim Entwurf von Speicherhierarchien ist, dass jede Änderung zur potentiellen Verkleinerung der Fehlerrate auch die Gesamtleistung negativ beeinflusst. Die Kombination von positiven und negativen Effekten ist es, was den Entwurf einer Speicherhierarchie so erschwert. Tabelle 13.9 zeigt diesen Zusammenhang. Veränderung

Auswirkung auf Fehlerrate

Größe erhöhen Assoziativität erhöhen

Verringert Kapazitätsfehler Verringert Fehlerrate durch Konfliktfehler Verringert Kaltstartfehler für viele Blockgrößen

Blöcke vergrößern

Möglicher negativer Leistungseffekt Kann Zugriffszeit erhöhen Kann Zugriffszeit erhöhen Kann die Fehlerstrafe vergrößern

Tabelle 13.9.: Einfluss von Speicherhierarchieveränderungen auf die Fehlerrate und Zugriffszeit bzw. Fehlerstrafe Tabelle 13.10 zeigt, wie einige der quantitativen Eigenschaften von Blockgröße, Fehlerstrafen und Fehlerrate variieren können. Wie werden die Hauptentwurfsalternativen als eine Reihe von vier Fragen betrachten, die sich bei der Entwicklung der zwei Ebenen einer Speicherhierarchie stellen. Der Einfachheit halber werden wir nur die CacheTerminologie verwenden. Merkmal

Typische Werte beim Cache

Gesamtgröße in Blöcken Gesamtgröße in Kilobyte Blockgröße in Bytes Fehlerstrafe in Taktzyklen

1.000–100.000 8–8.000 16–256 10–100

Fehlerraten

0,1 %–10 %

Typische Werte für Seitenverwaltung 2.000–250.000 8.000–8.000.000 4.000–64.000 1.000.000– 10.000.000 0,000.01 %– 0,000.1 %

Typische Werte für einen TLB 32–4.000 0,25–32 4–32 10–100 0,01 %–2 %

Tabelle 13.10.: Typische Werte einer Speicherhierarchie

286

Technische Informatik I

13.5. Zusammenfassung Speicherarchitektur Die Werte der Tabelle sind die typischen Werte des Jahres 1997. Dass Intervalle sehr groß sind, liegt zum Teil daran, dass sich die zusammenhängenden Werte über die Jahre verändert haben. Da zum Beispiel die Caches immer größer werden, um die Fehlerstrafen zu überwinden, wachsen auch die Blockgrößen. In Tabelle 13.11 stellen wir die Speicherhierarchien des Pentium Pro und PowerPC 604 vor. Der Pentium Pro und der PPC 604 unterscheiden sich in ihrer Adressumsetzung und die Unterschieden setzten sich in die TLB-Hardware fort. Die Architektur des PowerPC hat eine viel größere 52 Bit lange virtuelle Adresse gegenüber dem 32-Bit-Adressraum des Pentium Pro. Die Tabelle zeigt die wesentlichen Unterschiede. Eigenschaft Virtuelle Adresse Physische Adresse Seitengröße Aufbau des TLB

Intel Pentium Pro 32 Bits 32 Bits 4 KB, 4 MB Ein TLB für Anweisungen und ein TLB für Daten. Beide vierfach mengenassoziativ. Pseudo-LRU-Ersetzung Befehls-TLB: 32 Einträge Daten-TLB: 64 Einträge Fehlerbehandlung durch Software

PowerPC 604 52 Bits 52 Bits 4 KB, wählbar, und 256 MB Ein TLB für Anweisungen und ein TLB für Daten. Beide zweifach mengenassoziativ. LRU-Ersetzung Befehls-TLB: 128 Einträge Daten-TLB: 128 Einträge Fehlerbehandlung durch Hardware

Tabelle 13.11.: Vergleich Speicherarchitekturen von Pentium Pro und PowerPC 604 Beiden Maschinen bieten Unterstützung für große Seiten, welche für Dinge wie das Betriebssystem oder die Abbildung eines frame buffers benutzt werden. Das Schema mit den großen Seiten vermeidet die Vergabe zu vieler Seiten, um ein einzelnes immer vorhandenes Objekt abzubilden. Der PPC 604 erlaubt auch eine variable Seitengröße, um größere Seiten (immer noch Zweierpotenzen) verwenden zu können. Auf der primären Cache-Ebene unterscheiden sich die Rechner in Größe und Optimierungen zur Verringerung der Fehlerstrafe. Außer diesen Unterschieden sind die primären Caches der beiden Prozessoren recht simpel (Tabelle 13.12). Die Primärcaches beider Maschinen sind physisch indizierbar und markierbar (Tags), wie alle Caches, die wir in diesem Kapitel besprochen haben. Die Second-Level-Caches für den Pentium Pro und den PowerPC 604 enthalten sowohl Code als auch Daten und sind entweder 256 KB oder 512 KB groß.

Technische Informatik I

287

13. Realisierung einer leistungsfähigen Speicherarchitektur

Eigenschaft Cacheaufbau Cachegröße Cacheassoziativität Ersetzung Blockgröße Schreibstrategie

Intel Pentium Pro Getrennte Caches für Befehle und Daten Jeweils 8 KB Vierfach mengenassoziativ Angenäherte LRU-Ersetzung 32 Bytes Zurückschreiben

PowerPC 604 Getrennte Caches für Befehle und Daten Jeweils 16 KB Vierfach mengenassoziativ LRU-Ersetzung 32 Bytes Zurückschreiben oder Durchschreiben

Tabelle 13.12.: Vergleich Primär-Caches von Pentium Pro und PowerPC 604

288

Technische Informatik I

14. Parallelrechner Obwohl Rechner immer leistungsfähiger werden, wachsen die Anforderungen an sie mindestens im gleichen Maßstab: Astronomen wollen die Geschichte des gesamten Universum vom Urknall bis zum „Restaurant am Ende des Universum“ simulieren. Pharmazeuten wären gerne in der Lage, Medizin gegen bestimmte Krankheiten auf ihren Rechnern zu entwickeln, anstatt Horden von Ratten dafür opfern zu müssen. Flugzeugentwickler könnten treibstoffsparendere Typen entwickeln, wenn nur Rechner all die Arbeit machen würden, ohne die Notwendigkeit für teure Prototypen im Windkanal. Kurz gesagt ist die verfügbare Rechenkraft, wie groß sie auch sein mag, für einige Anwender (insbesondere aus Wissenschaft und Ingenieursbereichen) nie genug. Obwohl die Taktraten kontinuierlich steigen, kann die Geschwindigkeit der Schaltkreise nicht unbegrenzt erhöht werden. Die Lichtgeschwindigkeit ist bereits eines der Hauptprobleme der Entwickler von Hochleistungsrechnern und die Aussichten, dass man Elektronen und Photonen zu höheren Geschwindigkeiten antreiben kann, sind sehr gering. Die hohe Wärmeentwicklung macht aus Superrechnern schon heute Vorbilder für Hochleistungsklimaanlagen. Und da letzten Endes die Transistorgrößen immer weiter sinken, wird ein Transistor eines Tages aus so wenigen Atomen bestehen, dass quantenmechanische Effekte (wie das Unschärfeprinzip von Heisenberg) zu einem großen Problem werden können. Deswegen wenden sich Rechnerarchitekten im Hinblick auf immer größere Rechenaufgaben mehr und mehr Parallelrechnern zu. Während es vielleicht nicht möglich ist, einen Rechner mit nur einer CPU und einem Taktzyklus von 0,001 Nanosekunden zu bauen, könnte es machbar sein, ein Modell mit 1000 CPUs zu schaffen, die einen Taktzyklus von jeweils einer Nanosekunde haben. Obwohl der zweite Ansatz langsamere CPUs als der erste nutzt, ist die theoretische Rechenkapazität die gleiche. Darauf setzen die Entwickler ihre Hoffnungen. Parallelität kann auf verschiedenen Ebenen eingeführt werden. Auf der Befehlsebene können Pipelining und superskalares Design benutzt werden, um die Leistung gegenüber einem rein sequentiellen Ansatz um den Faktor 10 zu steigern. Um jedoch einen Faktor von Einhundert, Eintausend oder gar einer Million zu erreichen, ist es notwendig, dass komplette CPUs oder zumindest ein wesentlicher Teil von ihnen nachgebildet werden, und dass die Zusammenarbeit zwischen ihnen effizient gestaltet wird. Wenn man ein neues Parallelrechnersystem entwickelt, stellt man drei grundlegende Fragen: 1. Was ist die Art, Größe und Anzahl der Rechenelemente? 2. Was ist die Art, Größe und Anzahl der Speicherelemente? 3. Wie sind die Rechen- und Speicherelemente miteinander verbunden? Sehen wir uns jeden dieser Punkte kurz etwas genauer an. Die Rechenelemente können von kleinsten ALUs bis hin zu vollständigen CPUs reichen, deren Größe von einem kleinen Teil des Chips bis hin zu einem Kubikmeter voll Elektronik pro Element

289

14. Parallelrechner betragen kann. Wie man erwarten kann, ist es möglich, einen Rechner mit einer riesigen Anzahl, vielleicht sogar einer Million, Rechenelementen auszustatten, wenn die Größe dieser Elemente nur einen Bruchteil eines Chips einnimmt. Wenn ein Rechenelement ein vollständiger Rechner mit eigenem Speicher und eigenen E/A-Geräten ist, ist die Anzahl natürlich geringer, obwohl bereits Systeme mit zehntausend CPUs in Betrieb genommen wurden. Immer häufiger werden Parallelrechner aus handelsüblichen Teilen, insbesondere Prozessoren, gebaut. Die Fähigkeiten und Grenzen dieser Bauteile beeinflussen meist sehr stark das Design des Rechners. Speichersysteme sind oftmals in Module aufgeteilt, die parallel, aber unabhängig voneinander arbeiten, und den gleichzeitigen Zugriff von vielen CPUs aus ermöglichen. Diese Module können klein (einige Kilobyte) und groß (mehrere Megabyte) sein. Sie können dicht bei der CPU integriert sein oder sich auch auf einer anderen Platine befinden. Da große dynamische Speicher (DRAM) normalerweise viel langsamer als die CPUs sind, werden häufig ausgeklügelte Caching-Strategien benutzt, um den Speicherzugriff zu beschleunigen. Oftmals kommen dabei zwei, drei oder sogar vier Caching-Ebenen zum Einsatz. Obwohl eine gewisse Variation zwischen den CPU- und Speicherdesigns besteht, unterscheiden sich Parallelrechnersysteme am meisten in der Art, wie die Komponenten miteinander verbunden werden. Die Verbindungsschemata können in zwei allgemeine Kategorien eingeteilt werden: Statisch und Dynamisch. Bei den statischen Schemata werden alle Komponenten auf eine feste Weise verbunden, zum Beispiel als ein Stern, ein Ring oder ein Gitter. In den dynamischen Verbindungsschemata werden die Teile an ein Vermittlungsnetzwerk gehängt, das die Nachrichten zwischen den Komponenten dynamisch befördern kann. Jeder Ansatz hat seine eigenen Stärken und Schwächen, wie wir sehen werden. Parallelrechner können als eine Ansammlung von Chips betrachtet werden, die auf die eine oder andere Weise miteinander verdrahtet sind. Das ist im Wesentlichen eine BottomUp-Sicht (also von unten heraufgeschaut) der Welt. In einem Top-Down-Ansatz (von oben nach unten) würde man fragen: „Was soll parallel laufen?“ Hierbei haben wir wieder eine Fülle von Möglichkeiten. Einige Parallelrechner wurden entwickelt, um viele unabhängige Jobs (d. h. Aufgaben) gleichzeitig auszuführen. Diese Jobs haben nichts miteinander zu tun und kommunizieren nicht untereinander. Ein typisches Beispiel dafür wäre ein Rechner mit 8 bis 64 Prozessoren, der als großes UNIX-Zeitscheibensystem für die Behandlung von tausenden von entfernten Nutzern konzipiert wurde. Transaktionssysteme in Banken (z. B. automatische Kassensysteme), Fluglinien (Rechner für die Flugreservierung) und große Webserver fallen in diese Kategorie. Eine andere Möglichkeit für die Verwendung von Parallelrechnern ist Betrieb eines einzelnen Jobs, der aus mehreren Prozessen besteht. Als Beispiel denke man sich ein Schachprogramm, dass ein Spielbrett analysiert, in dem es eine Liste der von dort aus möglichen Spielzüge erstellt, und dann parallele Prozesse abspaltet, um jedes neue Spielbrett (rekursiv) parallel zu analysieren. Die Parallelität hier dient nicht der Behandlung von mehreren Nutzern sondern dem Geschwindigkeitsgewinn bei einem einzelnen Problem. Es existieren auch Maschinen, bei denen die Parallelität aus einem hohen Grade von Pipelining oder mehreren mit der gleichen Befehlsfolge beschäftigten ALUs gewonnen wird. Numerische Supercomputer mit spezieller Hardware für Vektorverarbeitung fallen in diese Kategorie (man denke an Cray-Supercomputer). Hier haben wir nicht nur ein zu lösendes Hauptproblem, sondern es arbeiten alle Teile des Rechners an fast dem gleichen

290

Technische Informatik I

14.1. Kommunikationsmodelle Aspekt des Problems zur selben Zeit (z. B. verschiedene Elemente derselben Vektoren parallel zu addieren). Obwohl es nur schwer exakt festzulegen ist, unterscheiden sich die drei Beispiele in einem Punkt, der Granularität (grain size) genannt wird. In mehrprozessorbasierten Zeitscheibensystemen ist die Einheit der Parallelität sehr groß: ein gesamtes Benutzerprogramm. Der parallele Betrieb von großen Softwareeinheiten mit geringer oder keiner Kommunikation zwischen den Einheiten wird grobgranulare Parallelität (coarse-grained parallelism) genannt. Das andere Extrem, wie es bei Vektorrechnern auftritt, bezeichnet man als feingranulare Parallelität (fine-grained parallelism). Die Granularität bezieht sich zwar auf Algorithmen und Software, es besteht jedoch ein direkter Bezug zur Hardware. Systeme mit einer geringen Anzahl von großen, unabhängigen Prozessoren, die nur mit geringen Bandbreiten verbunden sind, bezeichnet man als losen Verbund (loosely-coupled systems). Das Gegenstück sind die Systeme im engen Verbund (tightly-coupled systems), bei denen die Komponenten im Allgemeinen kleiner und dichter beisammen sind und häufig miteinander über ein Netzwerk hoher Bandbreite kommunizieren. In den meisten Fällen arbeiten Probleme mit grobgranularer Parallelität am besten mit loseverbundenen Systemen zusammen. Analog dazu sind engverbundene Systeme am besten für feingranulare Parallelitätsprobleme geeignet. Da es jedoch so viele verschiedene Algorithmen, Software und Hardware gibt, kann das höchstens ein allgemeiner Wegweiser sein. Das breite Spektrum der Granularität von Parallelitätsproblemen und der Möglichkeiten für den Verbund des Systems hat zu einer großen Anzahl von Architekturen geführt, die wir in diesem Kapitel untersuchen werden. In den folgenden Abschnitten werden wir einige Entwicklungsaspekte für Parallelrechner betrachten, wobei wir mit den Kommunikationsmodellen und Verbindungsnetzwerken beginnen. Anschließend werden wir uns die Leistungs- und Softwareaspekte ansehen. Letztendlich werden wir die Systematik von Parallelrechnerarchitekturen behandeln, welche uns durch den Rest des Kapitels führen wird.

14.1. Kommunikationsmodelle In jedem Parallelrechnersystem müssen die Prozessoren, die an verschiedenen Teilen derselben Aufgabe arbeiten, miteinander kommunizieren, um Informationen auszutauschen. Wie genau sie dies tun sollten, ist Gegenstand einer heißen Debatte in der Entwicklergemeinde. Zwei Ausprägungen, Multiprozessoren und Multicomputer, wurden vorgeschlagen und umgesetzt. In diesem Abschnitt werden wir beide untersuchen.

14.1.1. Multiprozessoren Beim ersten Design teilen sich alle Prozessoren einen gemeinsamen physischen Speicher, wie in Abbildung 14.1 dargestellt. Ein System, dass wie dieses auf gemeinsam genutztem Speicher basiert, bezeichnet man als Multiprozessor oder manchmal auch als SharedMemory-System (System mit geteiltem Speicher). Das Multiprozessormodell dehnt sich bis in die Software aus. Alle Prozesse, die zusammen auf einem Multiprozessor arbeiten, können sich einen einzelnen virtuellen Adressraum teilen, der auf den gemeinsamen Speicher abgebildet wird. Jeder Prozess kann ein Wort aus dem Speicher lesen oder in den Speicher schreiben, indem er einfach eine Lade-

Technische Informatik I

291

14. Parallelrechner

P

P

P

P

CPU

P

P

P

P Geteilter Speicher

P

P

P

P

P

P

P

P

Abbildung 14.1.: Multiprozessorsystem oder Speicheroperation ausführt. Mehr ist nicht nötig. Zwei Prozesse kommunizieren, indem einer von ihnen Daten in den Speicher schreibt und den anderen diese Daten zurücklesen lässt. Die Möglichkeit, dass zwei (oder mehr) Prozesse kommunizieren können, indem sie einfach den Speicher lesen oder beschreiben, ist der Grund, warum Multiprozessoren so populär sind. Es ist ein Modell, welches für die Programmierer einfach zu verstehen und auf eine breite Masse von Problemen anwendbar ist. Man stelle sich zum Beispiel ein Programm vor, dass ein Bild untersucht und alle darin enthaltenen Objekte auflistet. Eine Kopie des Bildes befindet sich im Speicher, wie in Abbildung 14.2 dargestellt. P

P

P

P

P

1

2

3

4

P

P

5

6

7

8

P

P

9

10

11

12

P

P

13

14

15

16

P

P

P

P

P

Abbildung 14.2.: Gleichzeitige Verarbeitung Multiprozessorsystem

CPU

eines

Bildes

durch

ein

Jeder der 16 Prozessoren fährt einen einzelnen Prozess, welchem eine der 16 Sektionen des Bildes zugewiesen wurde. Nichtsdestotrotz hat jeder Prozess Zugriff auf das gesamte

292

Technische Informatik I

14.1. Kommunikationsmodelle Bild, was insofern wichtig ist, als dass einige Objekte mehrere Sektionen im Bild einnehmen können. Wenn ein Prozess entdeckt, dass eins seiner Objekte sich über eine Sektionengrenze ausdehnt, kann er dem Objekt einfach in die nächste Sektion folgen, in dem er die Datenworte dieser Sektion liest. Wenn zum Beispiel Prozessor 0 (der in der oberen rechten Ecke) in Abbildung 14.2 entdeckt, dass ein Objekt in die Sektion von Prozessor 1 reicht, kann er trotzdem den entsprechenden Speicher lesen. Vorher muss er insbesondere herausfinden, welcher Prozessor überhaupt über die benötigten Daten verfügt und diesem dann eine Anforderungsnachricht für eine Kopie dieser Daten zusenden. Im Regelfall würde er dann solange blockieren (d. h. warten), bis die Anfrage beantwortet wurde. Wenn die Nachricht bei Prozessor 1 eintrifft, muss die dortige Software sie analysieren und die benötigten Daten zurücksenden. Wenn die Antwortnachricht zum Prozessor 0 zurückkommt, löst die Software die Blockierung und setzt die Ausführung fort. In diesem Beispiel werden manche Objekte durch mehrere Prozesse entdeckt, sodass einige Koordination nötig ist, um z. B. zu bestimmen, wie viele Häuser, Bäume und Flugzeuge es gibt. Viele Rechnerhersteller vertreiben Multiprozessoren. Einige Beispiele sind der Sun Enterprise 10000, der Sequent NUMA-Q, der SGI Origin 2000 und der HP/Convex Exemplar.

14.1.2. Multicomputer Das zweite Design für Parallelarchitekturen ist eins, in dem jede CPU ihren eigenen privaten Speicher hat, auf den nur sie selbst und keine andere CPU zugreifen kann. Solch ein Design nennt man Multicomputer oder manchmal distributed memory system (System mit verteiltem Speicher). Ein solches System ist in Abbildung 14.3 abgebildet.

M

P

M

P

M

P

M

P

M

M

M

M

Privater Speicher

P

P

P

P

CPU

Nachrichten weiterleitendes Netzwerk

P

P

P

P

M

M

M

M

P

M

P

M

P

M

P

M

Abbildung 14.3.: Multicomputersystem Multicomputer sind häufig im losen Verbund; dies ist jedoch keine Voraussetzung. Der Hauptaspekt, in dem sich ein Multicomputer von einem Multiprozessor unterscheidet, ist, dass jeder Prozessor des Multicomputers seinen eigenen privaten lokalen Speicher besitzt, auf den er mit dem einfachen Aufruf von Lade- und Speicheroperationen zugreifen kann. Andere Prozessoren haben nicht die Möglichkeit, mit diesen Befehlen auf

Technische Informatik I

293

14. Parallelrechner diesen Speicher zuzugreifen. Demnach haben Multiprozessoren einen einzigen (von allen Prozessoren geteilten) physischen Adressraum, während Multicomputer einen physischen Adressraum pro Prozessor haben. Da die Prozessoren in einem Multicomputer nicht durch Lesen und Schreiben im gemeinsamen Speicher kommunizieren können, benötigen sie ein Verbindungsnetzwerk. Beispiele solcher Multicomputer sind der IBM SP/2, der Intel/Sandia Option Red oder der Wisconsin COW. Die Abwesenheit von gemeinsamer Speicherhardware hat einige wichtige Folgen für die Struktur der Software. Die Möglichkeit, dass alle Prozesse lesend und schreibend auf einen gemeinsamen virtuellen Adressraum zugreifen, indem sie einfach Lade- oder Schreibbefehlen ausführen, ist auf einem Multicomputer nicht gegeben.

5

P

6

P

7

P

8

P

1

2

3

4

P

P

P

P

CPU

Nachrichten weiterleitendes Netzwerk

P

P

P

P

13

14

15

16

Abbildung 14.4.: Gleichzeitige Verarbeitung Multicomputersystem

P

9

P

10

P

11

P

12

eines

Bildes

durch

ein

Hier werden für die Kommunikation zwischen Prozessen oft Softwareprimitiven wie send und receive verwendet. Dies verpasst der Software eine andere, weitaus komplexere Struktur als bei einem Multiprozessorsystem. Es bedeutet auch, dass die korrekte Aufteilung und Ablage der Daten an den optimalen Stellen eine Hauptaufgabe bei einem Multicomputer ist. Bei einem Multiprozessor ist dies keine Herausforderung, da die Platzierung nicht die Korrektheit oder Programmierbarkeit beeinflusst, höchstens vielleicht die Leistung. Unter diesen Umständen stellt sich die Frage, warum man Multicomputer bauen sollte, wo doch Multiprozessoren so viel einfacher zu programmieren sind. Die Antwort ist einfach: Große Multicomputer sind viel einfacher und kostengünstiger zu bauen als Multiprozessoren mit der gleichen Anzahl von CPUs. Die Implementierung eines geteilten Speichers, selbst für einige hundert CPUs, ist ein erhebliches Unterfangen, während der Aufbau eines Multicomputers mit 10.000 Prozessoren recht unkompliziert ist. Folglich sitzen wir in der Klemme: Multiprozessoren sind schwer zu bauen, aber einfach zu programmieren, wohingegen Multicomputer leicht zu bauen, aber schwer zu programmieren sind. Diese Beobachtung führte dazu, dass sehr viel Aufwand betrieben wurde, um Hybridsysteme zu konstruieren, die relativ einfach zu bauen und zu pro-

294

Technische Informatik I

14.1. Kommunikationsmodelle grammieren sind. Diese Arbeit selbst erbrachte wiederum die Erkenntnis, dass geteilter Speicher auf verschiedenen Wegen implementiert werden kann, von denen jeder seine eigenen Vor- und Nachteile hat. Tatsächlich konzentriert sich heutzutage die Forschung im Bereich Parallelarchitekturen auf die Annäherung von Multiprozessoren und Multicomputern in Hybridformen, welche die Stärken beider Architekturen verbinden. Der heilige Gral dabei ist es, ein Design zu finden, das skalierbar ist, d. h. ein Design, welches besser und besser arbeitet, desto mehr Prozessoren hinzugefügt werden. Ein Ansatz zum Bau solcher Hybridsysteme basiert auf der Tatsache, dass moderne Rechnersysteme nicht monolithisch sind, sondern aus einer Reihe von Ebenen und Schichten aufgebaut sind. Diese Einsicht eröffnet die Möglichkeit, geteilten Speicher auf einer von mehreren Ebenen zu implementieren. In Abbildung 14.5 wird dies veranschaulicht.

Abbildung 14.5.: Verschiedene Schichten, auf denen geteilter Speicher implementiert werden kann: a) Hardware b) Betriebssystem c) Sprachspezifisches Laufzeitsystem In Teil a) der Abbildung sehen wir, wie der geteilte Speicher durch die Hardware als ein echter Multiprozessor implementiert wird. Bei diesem Entwurf gibt es eine einzelne Kopie des Betriebssystems mit einer einzelnen Menge von Tabellen (insbesondere der Speicherzuweisungstabelle). Wenn ein Prozess mehr Speicher benötigt, löst er einen Softwareinterrupt in das Betriebssystem aus und sucht dann in dessen Tabelle nach einer freien Seite. Diese Seite wird dann in Adressraum des Aufrufers abgebildet. Das Betriebssystem gibt den Speicher für den Prozess frei. Es muss dabei koordinieren, welcher Prozess welche Speicherseite belegt. Eine zweite Möglichkeit ist es, Multicomputerhardware zu benutzen und durch das Betriebssystem geteilten Speicher zu simulieren, indem ein einzelner systemweiter Adress-

Technische Informatik I

295

14. Parallelrechner raum geboten wird. Diesen Ansatz nennt man DSM (Distributed Shared Memory, geteilter gemeinsamer Speicher). Er geht auf Li und Hudak zurück, die zwischen 1986 und 1988 daran gearbeitet haben. Bei diesem Ansatz befindet sich jede Seite in den Speichern aus Abbildung 14.3. Jede Maschine hat ihren eigenen virtuellen Speicher und ihre eigenen Seitenverwaltungstabelle. Wenn ein Rechner ein Laden oder ein Speichern auf einer Seite ausführt, die er nicht besitzt, wird ein Softwareinterrupt ausgelöst. Das Betriebssystem lokalisiert dann diese Seite und fordert von der CPU, die sie innehat, sie über das Verbindungsnetzwerk zu senden. Wenn sie eintrifft, wird die Seite in den eigenen Speicher eingebunden und die gescheiterte Anweisung neu ausgeführt. Das Ergebnis ist, dass das Betriebssystem Seitenfehler mit Hilfe von entferntem Speicher anstatt von einem Magnetspeicher behebt. Für den Nutzer sieht es so aus, als besäße die Maschine geteilten Speicher. Die dritte Möglichkeit besteht darin, dass ein Laufzeitsystem auf Benutzerebene eine (möglicherweise sprachspezifische) Art geteilten Speicher erzeugt. Bei diesem Ansatz bietet die Programmiersprache eine Form der Abstraktion für geteilten Speicher, welcher dann von Compilern und Laufzeitsystemen umgesetzt wird. Ein Beispiel eines sprachspezifischen geteilten Speichers, welches durch ein Laufzeitsystem realisiert wird, ist das Orca-Modell für gemeinsame Datenobjekte. Bei Orca teilen sich Prozesse allgemeine Objekte und können objektspezifische Methoden auf ihnen ausführen. Wenn eine Methode den internen Zustand eines Objektes verändert, muss das Laufzeitsystem alle Kopien dieses Objektes auf allen Maschinen gleichzeitig anpassen. Da Objekte ein reines Softwarekonzept sind, kann diese Realisierung durch das Laufzeitsystem ohne Hilfe durch Betriebssystem oder Hardware geschehen.

14.2. Verbindungsnetzwerke In Abbildung 14.3 haben wir gesehen, dass Multicomputer durch Verbindungsnetzwerke zusammengehalten werden. Nun ist es Zeit, einen genaueren Blick auf diese Verbindungsnetzwerke zu werfen. Verbindungsnetzwerke können aus bis zu drei verschiedenen Komponentenklassen bestehen: 1. Schnittstellen 2. Verbindungen (links) 3. Schalter (switches) Schnittstellen sind die Geräte, welche die Nachrichten in und aus Prozessoren und Speicher transferieren. Bei vielen Entwürfen ist diese Schnittstelle ein Baustein oder eine Platine, die mit dem lokalen Bus des Prozessors verbunden ist und so mit dem Prozessor und ggf. mit dem Speicher Daten austauschen kann. Oftmals enthält diese Schnittstelle einen programmierbaren Prozessor und einigen privaten Speicher für eigene Zwecke. Normalerweise hat die Schnittstelle die Möglichkeit, Speicher zu lesen und zu beschreiben, um Datenblöcke umher zu bewegen. Die Verbindungen sind die physischen Kanäle, durch die sich die Bits bewegen. Sie können elektrisch oder auch optisch sein, seriell (ein Bit breit) oder parallel (mehrere Bits breit). Jede Verbindung hat eine maximale Bandbreite, welche sich aus der Anzahl der Bits ergibt, die pro Sekunde übertragen werden können. Verbindungen können simplex

296

Technische Informatik I

14.2. Verbindungsnetzwerke (unidirektional), halbduplex (nur eine Richtung gleichzeitig) oder vollduplex (beide Richtungen gleichzeitig) sein. Die Schalter sind Elemente mit mehreren Ein- und Ausgängen. Wenn ein Paket auf einem Eingang an einem Schalter ankommt, werden einige Bits des Paketes dazu benutzt, den Ausgang zu bestimmen, durch den das Paket versendet wird. Ein Paket kann klein (2 bis 4 Bytes) sein, aber auch größer z. B. 8 Kilobyte. Es besteht eine gewisse Analogie zwischen einem Verbindungsnetzwerk und den Straßen einer Stadt. Die Straßen sind wie Verbindungen: Es gibt Direktionalität (Einbahnstrassen oder normale), eine maximale Datenrate (Geschwindigkeitsbegrenzung) und eine Breite (Anzahl der Verkehrsspuren). Die Kreuzungen sind wie Schalter: An jeder Kreuzung entscheidet ein ankommendes Paket (Fußgänger oder Fahrzeug) in Abhängigkeit vom Ziel, welchen Ausgang (Straße) es benutzen wird. Die Topologie oder Struktur eines Verbindungsnetzwerkes beschreibt, wie die Verbindungen und Schalter angeordnet sind. Dies kann zum Beispiel eine Ring- oder Gitterstruktur sein. Topologische Entwürfe können als Graphen modelliert werden, wobei die Verbindungen als Kanten und die Schalter als Knoten modelliert werden. Jeder Knoten in einem Verbindungsnetzwerk hat eine gewisse Anzahl von Verbindungen, mit denen er verbunden ist. Diese bezeichnet man als Grad (Degree) oder Verzweigungsgrad (Fanout) des Knotens. Durch einen höheren Verzweigungsgrad ergeben sich die Möglichkeiten zu routen. Damit steigt die Fehlertoleranz, also die Fähigkeit, weiterhin korrekt zu arbeiten, wenn eine Verbindung ausfällt. Wenn jeder Knoten k Kanten hat und die Verdrahtung korrekt vorgenommen wurde, ist es möglich, das Netzwerk so zu entwerfen, dass es selbst bei Ausfall von k − 1 Verbindungen vollständig verbunden bleibt. Eine weitere Eigenschaft eines Verbindungsnetzwerkes (oder des Graphen) ist sein Durchmesser. Wenn wir den Abstand zwischen zwei Knoten als die Anzahl der Kanten messen, die durchlaufen werden müssen, um von einem Knoten zum anderen zu kommen, dann ist der Durchmesser eines Graphen der größte dieser Abstände. Der Durchmesser eines Verbindungsnetzwerkes ist mit der (maximalen) Paketverzögerung verbunden, die im schlimmsten Fall beim Senden eines Paketes von einem Prozessor zum anderen oder zu einem Speicher auftreten kann. Dies resultiert daraus, dass das Durchlaufen einer jeden Verbindung eine endliche Menge Zeit benötigt. Je kleiner der Durchmesser ist, desto besser ist die Leistung im schlimmsten Fall. Wenn es nur eine Achse gibt, sodass ein Paket entweder nur nach Ost oder West oder andersherum laufen kann, bezeichnet man das Netzwerk als eindimensional. Gibt es zwei Achsen, sodass das Paket sowohl nach Ost und West gehen kann, als auch nach Süd und Nord, handelt es sich um ein zweidimensionales Netzwerk, und so weiter. Abbildung 14.6 zeigt mehrere solcher Topologien. Es werden nur die Verbindungen und Schalter abgebildet. Die Speicher und Prozessoren (nicht im Bild enthalten) wären normalerweise durch Schnittstellen mit den Schaltern verbunden. Teil a) der Abbildung ist eine nulldimensionale Stern-Konfiguration, bei der die Prozessoren und Speicher an den äußeren Knoten angeschlossen würden und der zentrale Knoten nur eine vermittelnde Funktion hätte. Obwohl dies ein einfacher Aufbau ist, wird der zentrale Schalter für große Systeme schnell zum Flaschenhals. Aus einer fehlertoleranten Perspektive ist dies also ein armseliger Entwurf, da ein einziger Fehler im Zentralknoten das ganze System lahm legen würde.

Technische Informatik I

297

14. Parallelrechner

a)

b)

c)

d)

e)

f)

g)

h)

Abbildung 14.6.: Verschiedene Netzwerktopologien: a) Stern b) Vollständige Verbindung c) Baum d) Ring e) Gitter f) Doppeltorus g) Würfel h) Vierdimensionaler Hyperwürfel

Im Teil b) haben wir einen weiteren nulldimensionalen Aufbau, allerdings das andere Extrem: Eine vollständige Verbindung. Hierbei hat jeder Knoten eine direkte Verbindung zu jedem anderen Knoten. Dieser Entwurf maximiert die Bandbreitenaufteilung (Bisektionsbandbreite), minimiert den Durchmesser und ist äußerst fehlertolerant (sechs Verbindungen können verloren gehen, ohne dass die Verbindung eines einzigen Knoten abreißt). Unglücklicherweise ist die Anzahl der benötigen Verbindungen für k Knoten gleich 21 (k · (k − 1)), was für größere k schnell aus dem Rahmen läuft. Eine dritte nulldimensionale Topologie ist der Baum, wie in Teil c) von Abbildung 14.6 abgebildet. Ein Problem bei diesem Aufbau ist, dass die Bandbreitenverteilung gleich der Verbindungskapazität ist. Darum wird es normalerweise eine Menge Verkehr an der Spitze des Baums geben, was die wenigen Knoten an der Oberseite zu Flaschenhälsen macht. Eine Lösung für dieses Problem ist es, die Bandbreitenverteilung zu erhöhen, indem den oberen Verbindungen mehr Bandbreite zugewiesen wird. Wenn zum Beispiel die Verbindungen auf der untersten Ebene eine Kapazität b haben, hätte die nächsthöhere Ebene eine Kapazität 2b und die Verbindungen auf der höchsten Ebene 4b. Solch einen Aufbau (im Englischen nennt man ihn fat tree) benutzte man früher in kommerziellen Multicomputern wie dem (heute veralteten) Thinking Machines CM-5.

298

Technische Informatik I

14.2. Verbindungsnetzwerke Der Ring aus Abbildung 14.6d) ist eine eindimensionale Struktur laut unserer Definition, da jedes Paket die Wahl hat, nach links oder rechts zu gehen. Das Gitter (oder Geflecht) aus Abbildung 14.6e) ist ein zweidimensionaler Aufbau, der in vielen kommerziellen Systemen verwendet wurde. Es ist sehr geradlinig, leicht zu skalieren (bis hin zu sehr großen Systemen) und hat einen Durchmesser, der lediglich proportional zur Wurzel der Knotenanzahl wächst. Eine Variante des Gitters ist der (zweidimensionale) Torus aus Abbildung 14.6f), wobei es sich um ein Gitter handelt, dessen gegenüberliegende Ränder miteinander verbunden wurden. Der Torus ist nicht nur fehlertoleranter als ein herkömmliches Gitter, auch der Durchmesser ist geringer, da entgegengesetzte Ecken nun mit nur zwei Sprüngen (Hops) kommunizieren können. Der Würfel (Abbildung 14.6g) ist eine geradlinige dreidimensionale Topologie. Wir haben einen Würfel der Größe 2×2×2 dargestellt, jedoch könnte der allgemeine Fall ein k ×k ×k Würfel sein. Und in Abbildung 14.6h schließlich haben wir einen vierdimensionalen Würfel konstruiert, indem wir die entsprechenden Ecken eines dreidimensionalen Würfels miteinander verbunden haben. Wir könnten leicht einen fünfdimensionalen Würfel bauen, indem wir die Struktur aus h) klonen und die zusammengehörenden Knoten verbinden würden, was einen Block von vier Würfeln ergibt. Um in die sechste Dimension vorzustoßen, würden wir einfach diesen Viererblock replizieren und die entsprechenden Knoten mit denen des ersten Blocks verbinden. Das Verfahren lässt sich beliebig weiterführen. Einen n -dimensionalen Würfel, der auf diese Weise erzeugt wurde, nennt man (n-dimensionalen) Hyperwürfel. Viele Parallelrechner nutzen diese Struktur, da der Durchmesser linear mit der Dimensionalität wächst. Anders gesagt ist der Durchmesser der Logarithmus der Knotenanzahl zur Basis zwei, sodass ein 10-dimensionaler Hyperwürfel 1024 Knoten bei einem Durchmesser von nur 10 besäße. Die Verzögerungszeiten sind dabei außerordentlich gering. Im Gegensatz hätten 1024 Knoten in einer 32×32 breiten Gitterstruktur einen Durchmesser von 62, was mehr als sechsfach so schlecht wie der Hyperwürfel ist. Der Preis, den man für den kleineren Durchmesser zahlt, ist der Verzweigungsgrad. Folglich ist die Anzahl der Verbindungen (und somit der Kosten) beim Hyperwürfel viel größer. Nichtsdestotrotz ist der Hyperwürfel die erste Wahl für Hochleistungssysteme.

14.2.1. Switching Ein Verbindungsnetzwerk besteht aus Schaltern und den Verbindungen zwischen ihnen. In Abbildung 14.7 sehen wir ein kleines Netzwerk mit vier Schaltern. Jeder Schalter im Beispiel hat vier Eingänge und vier Ausgänge. Zusätzlich umfasst jeder Schalter einige Prozessoren und Verbindungsschaltkreise (nicht dargestellt). Die Aufgabe des Schalters ist es, ankommende Pakete auf jedem Eingang zu akzeptieren und jedes davon durch den korrekten Ausgang weiterzusenden. Jeder Ausgang ist durch eine serielle oder parallele Leitung mit einem Eingang eines anderen Schalters verbunden. Serielle Leitungen transportieren nur ein Bit gleichzeitig. Parallele Leitungen übertragen mehrere Bits gleichzeitig und können die Verbindung durch Signale steuern. Sie haben somit bei gleicher Taktrate eine größere Leistung als serielle Leitungen, sind aber auch durch das Problem der Streuung betroffen (also ob alle Bits gleichzeitig eintreffen) und außerdem viel teurer. Mehrere Switching-Strategien sind möglich. Bei einer, die man Verbindungs-Switching (circuit switching) nennt, wird vor dem Senden eines Paketes der gesamte Pfad von der

Technische Informatik I

299

14. Parallelrechner Ausgang CPU 1

Vier-Port-Schalter

Eingang A

B

C

D

CPU 2

Abbildung 14.7.: Beispielnetzwerk mit vier Schaltern Quelle bis zum Ziel vorher reserviert. Alle Ein- und Ausgänge und Puffer werden vereinnahmt, sodass zum Zeitpunkt des Übertragungsbeginns die Verfügbarkeit der benötigten Routen garantiert werden kann. Dadurch können die Bits mit maximaler Geschwindigkeit von der Quelle durch alle Schalter zum Ziel gelangen. Abbildung 14.7 stellt Verbindungs-Switching dar, indem eine Verbindung von CPU 1 zu CPU 2 als ein dicker schwarzer Pfeil gezeichnet wurde. Hier wurden drei Eingänge und drei Ausgänge reserviert. Verbindungs-Switching kann mit der Reservierung einer Route eines Karnevalsumzug durch die Stadt verglichen werden, bei dem die Polizei alle Seitenstraßen mit Barrikaden absperrt. Es erfordert Vorausplanung, sobald diese aber abgeschlossen wurde, kann sich der Umzug mit maximaler Geschwindigkeit bewegen, ohne durch anderen Verkehr gestört zu werden. Der Nachteil ist, dass die Vorausplanung zwingend notwendig und weiterer Verkehr explizit verboten ist, auch wenn andere Umzüge (Pakete) in Sicht sind. Eine zweite Switching-Strategie ist das Store-And-Forward-Packet-Switching. Dabei ist keine Reservierung im Voraus nötig. Stattdessen sendet die Quelle das vollständige Paket zum ersten Schalter, wo es in seiner Gesamtheit gespeichert wird. Im Teil a) von Abbildung 14.8 ist CPU 1 die Quelle und liefert das gesamte Paket, welches für CPU 2 bestimmt ist. Es wird innerhalb des Schalters A gepuffert. Nachdem das Paket vollständig im Schalter A aufgenommen wurde, wird es zu Schalter C bewegt, wie im Teil b) der Abbildung gezeigt. Nach dem das gesamte Paket vollständig im Schalter C eingetroffen ist, wird es wiederum zu Schalter D versendet (Teil c) der Abbildung). Von dort aus wird es schließlich zum Ziel (CPU 2) versendet. Man beachte, dass keine Vorbereitung notwendig ist und keine Ressourcen vorher reserviert werden müssen.

300

Technische Informatik I

14.2. Verbindungsnetzwerke a)

b) CPU 1 Ausgang

CPU 1

Vier-Port-Schalter

Eingang

Ein Paket

A

B

C

D

A

B

C

D

CPU 2 CPU 2

Ein Paket

c) CPU 1 A

B

C

D

CPU 2

Ein Paket

Abbildung 14.8.: Store-and-Forward-Packet-Switching

Store-and-Forward-Schalter müssen Pakete puffern, weil ein Paket, dass von einer Quelle dargeboten wird (sei es ein Prozessor, ein Speichermodul oder ein anderer Schalter), einen Ausgang benötigen kann, der gerade zum Versand eines anderen Paketes verwendet wird. Wenn keine Pufferung vorhanden wäre, müssten ankommende Pakete, die einen besetzen Ausgang benötigen, verworfen werden, was zu einem unzuverlässigen Netzwerk führen würde. Dabei kommen drei verschiedene Pufferstrategien zum Einsatz. In der ersten, der Eingabepufferung, werden einem Eingang ein oder mehrere Puffer als FIFO-Warteschlange zugewiesen. Das Paket wartet einfach in der Warteschlange, wenn der von ihm benötigte Ausgang belegt ist. Das Problem bei diesem Entwurf ist, dass ein Paket, das auf seine Auslieferung wartet, dem Paket dahinter, welches über einen freien Ausgang raus kann, den Weg versperrt. Diese Situation wird Listenkopfblockierung genannt. Es kann mit einer Folge von Autos verglichen werden, die sich in einer Straße stauen, da das erste Auto links abbiegen

Technische Informatik I

301

14. Parallelrechner möchte, welches aufgrund von Gegenverkehr aber warten muss. Obwohl das zweite und die darauffolgenden Fahrzeuge geradeaus wollen, werden sie durch das erste blockiert. Diese Blockierung kann durch Ausgabepufferung eliminiert werden. Bei diesem Aufbau werden die Puffer mit den Ausgängen verknüpft, statt mit den Eingängen. Wenn die Bits eines ankommenden Paketes eintreffen, werden sie in einem Puffer für den entsprechenden Ausgang gesichert. Auf diese Weise können die Pakete für Ausgang m nicht die Pakete für Ausgang n blockieren. Sowohl Eingabe- als auch Ausgabepufferung weisen den Ein- bzw. Ausgängen eine feste Anzahl von Puffern zu. Wenn mehr Pakete gespeichert werden müssen, als dort Platz vorhanden ist, müssen Pakete verworfen werden. Eine Art, diese Situation zu verbessern ist die Verwendung von gemeinsamen Puffern, bei der eine einzelne Ansammlung von Puffern bei Bedarf dynamisch an die Ein- und Ausgänge zugewiesen wird. Jedoch erfordert dieses Schema eine komplexere Administration, um die Übersicht über die Puffer zu behalten. Es erlaubt einer einzelnen beschäftigten Verbindung auch, sich alle Ressourcen einzuverleiben, und so andere Verbindungen auszuhungern. Weiterhin muss der Schalter das größtmögliche Paket aufnehmen können, unter Umständen auch mehrere Pakete mit Maximalgröße, was den Speicherbedarf erhöht und die maximale Paketgröße runterschraubt. Obwohl Store-And-Forward-Packet-Switching flexibel und effizient ist, hat es das Problem der wachsenden Verzögerungen im Verbindungsnetzwerk. Nehmen wir an, dass die Zeit für einen einzelnen Sprung für ein Paket in Abbildung 14.8 T Nanosekunden ist. Da die Pakete viermal kopiert werden müssen, um von Prozessor 1 zu Prozessor 2 (über A, C und D) zu kommen, und kein Kopiervorgang begonnen werden kann, bevor der vorige abgeschlossen ist, beträgt die Gesamtverzögerung 4T . Ein Weg aus diesem Dilemma ist der Entwurf eines Hybridnetzwerkes, bei dem einige Eigenschaften des VerbindungsSwitching und einige des Paket-Switchings zum Tragen kommen. Zum Beispiel kann ein Paket logisch in kleinere Einheiten zerlegt werden. Sobald die erste Einheit bei einem Schalter ankommt, kann es zum nächsten Schalter verschickt werden, noch bevor das letzte Paketteil eingetroffen ist. Dieser Ansatz unterscheidet sich vom Verbindungs-Switching, da keine Ressourcen zwischen den Endpunkten vorher reserviert werden müssen. Folglich ist die gemeinsame Nutzung der Ressourcen (Ein-/Ausgänge und Puffer) möglich. Dieser Ansatz benutzt einige Ideen, die auch beim Pipelining in einem Prozessor zum Einsatz kommen. Zu jedem Zeitpunkt verrichtet jeder Schalter nur einen Bruchteil der Arbeit, aber zusammen erreichen sie eine höhere Leistung, als ein einzelner es alleine könnte.

14.2.2. Routing-Algorithmen Bei jedem Netzwerk mit einer Dimensionalität größer gleich Eins muss eine Auswahl getroffen werden, welchen Weg ein Paket von der Quelle zum Ziel nehmen soll. Oft existieren mehrere Wege. Die Regel, die die Folge von Knoten bestimmt, die ein Paket von der Quelle zum Ziel durchlaufen muss, nennt man Routing-Algorithmus (Wegbestimmungsalgorithmus). Gute Routing-Algorithmen werden immer gebraucht, da oft mehrere Wege verfügbar sind. Ein guter Routing-Algorithmus kann die Last auf mehrere Verbindungen verteilen, um die volle verfügbare Bandbreite auszunutzen. Weiterhin muss der Routing-Algorithmus

302

Technische Informatik I

14.2. Verbindungsnetzwerke Verklemmungen im Verbindungsnetzwerk verweiden. Eine Verklemmung tritt auf, wenn mehrere Pakete, die gleichzeitig unterwegs sind, die Ressourcen so vereinnahmt haben, dass keins von ihnen weiterkommen kann und sie alle auf ewig blockieren. Ein Beispiel für eine Verklemmung in einem Verbindungsnetzwerk mit VerbindungsSwitching ist in Abbildung 14.9 dargestellt (Verklemmungen können natürlich auch bei Packet-Switching auftreten, jedoch sind sie dort nicht so anschaulich erklärbar). Hier versucht jede CPU ein Paket nach „schräg gegenüber“ zu versenden. Jeder von ihnen hat es geschafft, den Ein- und Ausgang auf seinem lokalen Schalter zu reservieren, sowie den Eingang auf dem folgenden. Sie sind jedoch nicht in der Lage, sich den notwendigen Ausgang auf dem zweiten Schalter zu beschaffen, also warten sie, bis dieser frei wird. Unglücklicherweise beginnen alle vier CPUs gleichzeitig mit diesem Vorgang, sodass alle blockieren und das Netzwerk in eine „Systemverklemmung“ gerät. Ausgang CPU 1

Vier-Port-Schalter

Eingang A

B

CPU 2

C

D

CPU 3

CPU 4

Abbildung 14.9.: Verklemmung in einem Verbindungsnetzwerk mit VerbindungsSwitching Routing-Algorithmen können in Quellen-Routing oder verteiltes Routing unterteilt werden. Dabei wird beim Quellen-Routing von der Quelle die gesamte Route durch das Netzwerk im Voraus bestimmt, welche durch eine Liste von Ein-/Ausgangsnummern ausgedrückt werden, die an jedem Schalter auf dem Weg benutzt werden. Beim verteilten Routing entscheidet jeder Schalter, durch welchen Ausgang er jedes ankommende Paket senden soll. Wenn es die gleiche Entscheidung für jedes Paket trifft, das an ein bestimmtes Ziel adressiert ist, spricht man von statischem Routing. Wenn die aktuelle Last mit in die Überlegung einbezogen wird, ist das Routing angepasst.

Technische Informatik I

303

14. Parallelrechner

14.3. Leistung Der Hintergrund beim Bau eines Parallelrechners ist es, ihn schneller als eine Einzelprozessormaschine zu machen. Wenn er dieses einfache Ziel nicht erreicht, lohnt sich das Vorhaben nicht. Darüber hinaus sollte er dieses Ziel auf eine kostengünstige Weise erreichen. Eine Maschine, die doppelt so schnell wie ein Einzelprozessor ist, dabei aber 50-mal so viel kostet, wird nicht gerade ein Verkaufsschlager werden. In diesem Abschnitt werden wir einige Leistungsaspekte von Parallelrechnerarchitekturen unter die Lupe nehmen.

14.3.1. Hardwaremetriken Aus der Sicht der Hardware sind die wichtigsten Maße für die Leistung die Geschwindigkeit des Prozessors und der Ein-/Ausgabe sowie die Leistung des Verbindungsnetzwerkes. Die Geschwindigkeit von Prozessor und Ein-/Ausgabe ist die gleiche wie beim Einzelprozessor, sodass die Schlüsselparameter, die in einem Parallelsystem von Interesse sind, mit der Verbindung abhängen. Es gibt dort zwei Punkte, Verzögerung und Bandbreite, die wir uns ansehen werden. Die Rundreise-Wartezeit (round-trip latency) ist die Zeit, die ein Prozessor für das Versenden eines Paketes und das Empfangen der Antwort benötigt. Wenn das Paket in den Speicher versendet wird, ergibt sich die Wartezeit aus der Zeit, die zum Lesen oder Schreiben eines Wortes (oder eines Wortblocks) benötigt wird. Wenn es an einen anderen Prozessor verschickt wird, ergibt sie sich aus der Kommunikationszeit für Pakete dieser Größe. Normalerweise ist nur die Wartezeit für minimale Pakete von Interesse, oftmals nur ein Wort oder einen Cacheblock groß. Die Wartezeit setzt sich aus verschiedenen Faktoren zusammen und ist für Packet-Switching, Verbindungs-Switching und virtuelle Schnitte unterschiedlich. Bei Verbindungs-Switching ist die Wartezeit die Summe der Einrichtzeit und der Übertragungszeit. Um eine Verbindung einzurichten, muss ein Testpaket verschickt werden, um Ressourcen zu reservieren und dann einen Bericht zu erstatten. Nachdem das geschehen ist, muss dass Datenpaket zusammengesetzt werden. Wenn dieser Vorgang fertig ist, können die Bits mit Höchstgeschwindigkeit fließen. Wenn also die gesamte Einrichtzeit T s ist, das Paket p Bits groß ist und die Bandbreite b Bits/Sekunde beträgt, so ist die Einwegwartezeit T s + p/b . Wenn die Verbindung vollduplex ist, gibt es keine Einrichtzeit für die Antwort, sodass die minimale Wartezeit für ein p -Bit-Paket und der Erhalt einer p -Bit-Antwort gleich T s + 2p/b Sekunden ist. Für Packet-Switching ist es nicht notwendig, ein Testpaket im Voraus an die Zieladresse zu versenden, aber es benötigt immer noch ein wenig Einrichtzeit T a , um das Paket zusammenzusetzen. Hierbei ist die Einwegübertragungszeit T a + p/b . Dies ist jedoch nur die Zeit, um das Paket in den ersten Schalter zu übertragen. Es gibt eine endliche Verzögerung innerhalb des Schalters Td , und dann wird der Vorgang zum nächsten Schalter hin wiederholt und so weiter. Die Verzögerung Td setzt sich aus der Verarbeitungszeit und Warteschlangenzeit zusammen, wenn auf das Freiwerden des Ausgangs gewartet wird. Wenn es n Schalter gibt, beträgt die totale Wartezeit T a +n ·(p/b +Td )+ p/b , wobei der letzte Summand das Kopieren vom letzten Schalter zum Ziel beschreibt.

304

Technische Informatik I

14.3. Leistung Die Verzögerungen für den virtuellen Schnitt und Wurmlochrouting sind in besten Fall ungefähr T a + p/b , da kein Prüfpaket zum Einrichten der Verbindung verschickt wird und keine Schalterverzögerungen auftreten. Im Grunde besteht die Wartezeit nur aus der ersten Einrichtzeit zum Zusammensetzen des Paketes plus der Zeit, die für das Hinauswerfen der Bits gebraucht wird. In jedem Fall muss eine Übertragungszeit eingerechnet werden, aber diese ist für gewöhnlich sehr klein. Das andere Hardwaremaß ist die Bandbreite. Viele Parallelprogramme, besonders im Bereich der Naturwissenschaften, bewegen eine Menge Daten umher, sodass die Anzahl der Bytes pro Sekunde, die das System bewegen kann, für die Leistung sehr wichtig ist. Es existieren mehrere Maße für die Bandbreite. Die Bisektionsbandbreite haben wir bereits kennen gelernt (siehe Topologien oben). Eine weitere ist die Gesamtbandbreite (aggregate bandwidth), welche sich aus der Summe der Kapazitäten aller Leitungen errechnet. Diese Zahl gibt die maximale Anzahl der Bits an, die auf einmal unterwegs sein können. Noch ein weiteres Maß ist die durchschnittliche Bandbreite jedes Prozessors. Wenn jeder Prozessor in der Lage ist, 1 MB/s auszugeben, hilft es wenig, dass das Netzwerk eine Bisektionsbandbreite von 100 GB/s besitzt. Die Kommunikation wird also dadurch begrenzt, wie viele Daten jeder Prozessor ausgeben kann. In der Praxis ist es sehr schwer, eine Bandbreite zu erreichen, die der theoretischen Bandbreite auch nur nahe kommt. Verwaltungsaufgaben reduzieren die Leistung. Zum Beispiel gibt es immer einigen Overhead (Überbau) pro Paket: Das Zusammensetzen, das Bauen des Kopfes und das Versenden. Beim Versenden von 1024 4-Byte-Paketen wird man niemals die gleiche Bandbreite erreichen wie beim Versenden von einem einzigen 4096-Byte-Paket. Unglücklicherweise ist es besser, kleinere Pakete zu versenden, um geringe Wartezeiten zu erhalten, weil größere Pakete die Leitungen und Schalter zu lange blockieren. Folglich gibt es einen natürlichen Konflikt zwischen dem Erreichen von kleinen durchschnittlichen Wartezeiten und großer Bandbreitenausnutzung. Für einige Anwendungen ist das eine wichtiger als das andere, und für andere Anwendungen ist es genau umgekehrt. Es lohnt sich jedoch anzumerken, dass man jederzeit mehr Bandbreite erkaufen kann (durch das Hinzufügen von mehr und breiteren Leitungen), dies aber nicht für die Verzögerung gilt. Folglich ist es besser, sich eher auf die Verkleinerung der Wartezeiten zu konzentrieren und sich später erst um die Bandbreite zu sorgen.

14.3.2. Softwaremetriken Hardwaremetriken wie Wartezeit und Bandbreite betrachten nur, was die Hardware zu leisten imstande ist. Jedoch haben Nutzer eine andere Sicht auf das System. Sie wollen wissen, um wie viel schneller ihre Programme auf einem Parallelrechner gegenüber einem Einzelprozessorsystem laufen. Für sie ist das Schlüsselmaß die Geschwindigkeitszunahme: Um wie viel schneller läuft ein Programm auf einem n-Prozessorsystem als auf einem Einprozessorsystem? Typischerweise werden diese Ergebnisse in Graphen wie dem in Abbildung 14.10 angegeben. Hier sehen wir verschiedene parallele Programme, die auf einem Multicomputer mit 64 Pentium-Prozessoren laufen. Jede Kurve zeigt die Geschwindigkeitszunahme eines Programms mit k Prozessoren als Funktion von k . Die perfekte Geschwindigkeitszunahme wird durch die gepunktete Linie dargestellt, bei der die Verwendung von k Prozessoren das Programm um den Faktor k beschleunigt. Kein Programm erreicht diese perfekte Geschwindigkeitszunahme, aber einige kommen ihr sehr nahe. Das N-Körperproblem parallelisiert sehr gut, Awari (ein afrikanisches Brett-

Technische Informatik I

305

14. Parallelrechner spiel) hält sich halbwegs gut, aber das Invertieren einer Skyline-Matrix lässt sich nicht schneller als mit der fünffachen Geschwindigkeit berechnen, egal, wie viele Prozessoren verfügbar sind. Die Programme und Ergebnisse werden in einem Artikel von Henri Bal und anderen mit dem Titel „Parallel Retrograde Analysis on a Distributed System“ zusammengefasst.

60

Beschleunigung

N-KörperProblem

lineare Beschleunigung

50 40

Awari

30 20 10 0

Skyline-MatrixInversion 0

10

20

30 40 Anzahl CPUs

50

60

Abbildung 14.10.: Anzahl der CPUs im Vergleich zur Geschwindigkeitssteigerung Der Grund, warum eine perfekte Geschwindigkeitszunahme nahezu unmöglich zu erreichen ist, ist, dass beinahe alle Programme einige sequentielle Komponenten haben, meistens die Initialisierungsphase, das Einlesen von Daten oder das Sammeln von Ergebnissen. Der Besitz von vielen Prozessoren hilft hier nichts. Man nehme an, dass ein Programm für T Sekunden auf einem Einzelprozessor läuft. Das Programm hat einen Anteil f von sequentiellen Code und einen Anteil 1− f , der möglicherweise parallelisierbar ist. Abbildung 14.11 zeigt dies. Inhärent sequentieller Anteil

Möglicherweise parallelisierbarer Anteil

f

1-f T

Abbildung 14.11.: Ein Programm hat einen sequentiellen und einen parallelisierbaren Teil.

306

Technische Informatik I

14.3. Leistung Wenn der parallelisierbare Teil auf n Prozessoren ohne Overhead läuft, kann seine Ausführungszeit bestenfalls von (1− f )T auf (1− f )T /n reduziert werden, wie in Abbildung 14.12. n CPUs aktiv

...

Eine CPU aktiv

f

1-f

fT

(1-f)T/n

Abbildung 14.12.: Effekt des parallelen Laufs des Programms Dies ergibt eine gesamte Ausführungszeit der sequentiellen und parallelen Teile von f T + (1 − f )T /n . Die Beschleunigung durch die Parallelisierung ist die Ausführungszeit T des Originalprogramms geteilt durch diese neue Ausführungszeit: Beschleunigung =

f T + (1 − f )T n = f T + (1 − f )T /n 1 + (n − 1) f

Dieses Ergebnis ist als Amdahls Gesetz bekannt. Für f = 0 erhalten wir lineare Geschwindigkeitszunahme, aber für f > 0 ist eine perfekte Zunahme durch die linearen Komponenten nicht mehr möglich. Amdahls Gesetz ist nicht der einzige Grund, warum eine perfekte Zunahme der Geschwindigkeit nahezu unmöglich zu erreichen ist. Kommunikationswartezeiten ungleich Null, endliche Kommunikationsbandbreiten und algorithmische Leistungsschwächen können ebenfalls eine Rolle spielen. Weiterhin können, selbst wenn 1000 Prozessoren zur Verfügung stehen, nicht alle Programme so geschrieben werden, dass sie Verwendung von so vielen Prozessoren Sinn machen. Ebenso kann der Overhead beim Starten aller Prozesse erheblich sein. Des weiteren parallelisieren die meisten bekanntesten Algorithmen nicht gut, so das ein schlechterer Algorithmus im parallelen Fall benutzt werden muss. Für viele Anwendungen ist es jedoch hochgradig wünschenswert, das Programm n mal schneller laufen zu lassen, selbst wenn dafür 2n Prozessoren benötigt werden.

14.3.3. Methoden zur Performanzsteigerung Der direkteste Weg zum Erhöhen der Leistung ist das Hinzufügen von mehr Prozessoren zum System. Jedoch muss diese Zugabe so erfolgen, dass keine Flaschenhälse geschaffen werden. Ein System, zu dem man mehr Prozessoren hinzufügen kann und entsprechend mehr Reichenkraft erhält, nennt man skalierbar.

Technische Informatik I

307

14. Parallelrechner Um einige Folgen der Skalierbarkeit zu entdecken, denke man sich vier Prozessoren, die mit einem Bus verbunden sind, wie in Abbildung 14.13a) dargestellt. Nun stelle man sich vor, das System auf 16 Prozessoren zu skalieren, indem man 12 Prozessoren hinzufügt (Abbildung 14.13b). Wenn die Bandbreite des Bus b MB/s ist, so haben wir mit der Vervierfachung der Prozessoren die verfügbare Bandbreit pro Prozessor von b/4 MB/s auf b/16 MB/s reduziert. Solch ein System ist nicht skalierbar.

a)

b)

c)

d)

Abbildung 14.13.: a) Ein Bus-basiertes System mit vier Prozessoren. b) Ein solches System mit 16 Prozessoren. c) Ein Gitter-basiertes System mit vier Prozessoren und d) mit 16 Prozessoren Nun tun wir das gleiche mit einem Gitter-basierten System (Abbildung 14.13c) und d)). Bei dieser Topologie werden mit den neuen Prozessoren auch neue Leitungen hinzugefügt, sodass das Skalieren des Systems nicht die Gesamtbandbreite pro Prozessor verringert, wie dies beim Bus der Fall ist. Tatsächlich erhöht sich die Leitungsrate von 1,0 bei 4 Prozessoren (4 Prozessoren, 4 Verbindungen) auf 1,5 bei 16 Prozessoren (16 Prozessoren, 24 Leitungen), sodass das Hinzufügen von Prozessoren die Gesamtbandbreite pro Prozessor verbessert. Natürlich ist die Bandbreite nicht der einzige Aspekt. Das Hinzufügen von Prozessoren zum Bus erhöht nicht den Durchmesser des Verbindungsnetzwerkes oder die Wartezeit beim Ausbleiben von Netzwerkverkehr, wie es beim Gitter der Fall ist. Für ein n × n Gitter ist der Durchmesser 2(n −1), sodass die schlimmste (und durchschnittliche) Wartezeit sich ungefähr mit der Wurzel der Prozessorenanzahl erhöht. Für 400 Prozessoren ist der Durchmesser 38, wobei er für 1600 Prozessoren 78 ist, sodass eine Quadrierung der Prozessorenanzahl den Durchmesser annäherungsweise linear erhöht und somit auch die durchschnittliche Wartezeit. Idealerweise sollte ein skalierbares System die gleiche durchschnittliche Bandbreite pro Prozessor und eine konstante durchschnittliche Wartezeit beibehalten, wenn Prozessoren hinzugefügt werden. In der Praxis ist die Versorgung mit ausreichend Bandbreite pro

308

Technische Informatik I

14.3. Leistung Prozessor machbar, jedoch erhöht sich in allen realen Aufbauten die Wartezeit mit der Größe. Sie logarithmisch wachsen zu lassen, wie dies beim Hyperwürfel der Fall ist, ist der bestmögliche Fall. Das Problem mit wachsenden Wartezeiten bei skalierendem System ist, dass sich Wartezeiten oftmals fatal auf die Leistung in fein- und mittelgranularen Anwendungen auswirken. Wenn ein Programm Daten benötigt, die sich nicht in seinem lokalen Speicher befinden, gibt es oft eine erhebliche Verzögerung beim Holen dieser Daten, und je größer das System ist, desto größer ist auch diese Verzögerung, wie wir gerade gesehen haben. Das Problem besteht gleichermaßen für Multicomputer und Multiprozessoren, da in beiden Fällen der physische Speicher ausnahmslos in weitverteilte Module zerlegt wurde. Eine Konsequenz aus dieser Beobachtung ist, dass die Systementwickler oft große Anstrengungen unternehmen, um die Wartezeit zu reduzieren oder zumindest zu verstecken. Sie verwenden dabei mehrere Techniken, die im Folgenden erwähnt werden. Die erste Technik, die Wartezeit zu verstecken, ist Datenreplikation. Wenn Kopien eines Datenblocks an mehreren Stellen im Speicher behalten werden, kann der Zugriff von diesen Stellen aus beschleunigt werden. Eine solche Replikationstechnik ist Caching, wobei eine oder mehrere Kopien von Datenblöcken dicht an den Stellen gehalten werden, wo sie verwendet werden sollen und wo sie „hingehören“. Jedoch gibt es noch eine weitere Strategie, bei der gleichrangige Kopien (gleichrangig bedeutet gleicher Zustand) unterhalten werden. Im Gegensatz zu der Primär/Sekundärbeziehung beim Caching. Wenn mehrere Kopien (in welcher Form auch immer) verwaltet werden, sind die Schlüsselaspekte, wo die Datenblöcke abgelegt werden, wann, und von wem. Die Antworten reichen von dynamischer Platzierung über Anforderung durch die Hardware bis hin zu vorsätzlicher Platzierung zur Ladezeit durch das Befolgen von Compileranweisungen. In jedem Fall ist das Beibehalten von Konsistenz von enormer Wichtigkeit. Eine zweite Technik zum Verbergen der Wartezeit ist Prefetching (Vorladen). Wenn ein Datenfeld geladen werden kann, bevor es gebraucht wird, kann der Ladevorgang mit der normalen Ausführung überlappen, sodass das Feld vorhanden ist, wenn es benötigt wird. Das Vorladen kann automatisch oder unter Programmkontrolle erfolgen. Wenn ein Cache nicht nur das referenzierte Wort, sondern eine gesamte Cachezeile lädt, ist es reine Spekulation, dass die folgenden Worte ebenfalls bald benötigt werden. Das Vorladen kann auch explizit gesteuert werden. Wenn der Compiler bemerkt, dass er einige Daten benötigt, kann er eine explizite Anweisung zum Holen einfügen und diese Anweisung ausreichend weit vorher einfügen, sodass die Daten rechtzeitig dort sein werden. Diese Strategie erfordert es, dass der Compiler ein umfassendes Verständnis der zugrundeliegenden Maschine und ihres Taktes hat, ebenso wie Kontrolle darüber, wo die Daten hinterlegt werden. Solche spekulativen Ladeanweisungen arbeiten am besten, wenn mit Sicherheit feststeht, dass die Daten gebraucht werden, denn das Auftreten eines Seitenfehlers bei einer Ladeanweisung für einen Pfad, der gar nicht eingeschlagen wird, ist sehr kostspielig. Eine dritte Technik, die die Wartezeit verbergen kann, ist Multithreading. Die meisten modernen Systeme unterstützen das Konzept der Multiprogrammierung, bei dem mehrere Prozesse gleichzeitig (d. h. pseudoparallel im Zeitteilverfahren) laufen. Wenn das Umschalten zwischen Prozessen schnell genug erfolgt, indem z. B. jedem Prozess seine eigene Speichertabelle und seine eigenen Hardwareregister zugewiesen werden, dann kann beim Blockieren eines Prozesses (der auf das eintreffen von entfernten Daten wartet), die Hardware schnell auf einen umschalten, der bereit zur Weiterführung ist. Im schlimms-

Technische Informatik I

309

14. Parallelrechner ten Fall bearbeitet der Prozessor den ersten Befehl von Thread 1, den zweiten Befehl von Thread 2 und so weiter. Dabei kann der Prozessor ausgelastet werden, auch angesichts von hohen Speicherwartezeiten für die einzelnen Threads.

14.4. Taxonomie von Parallelrechnern Obwohl noch viel mehr über die Software für Parallelrechner gesagt werden könnte, kehren wir zum Hauptthema dieses Kapitels zurück: Der Architektur von Parallelrechnern. Viele Arten von Parallelrechnern wurden über die Jahre angepriesen und gebaut, sodass sich natürlich die Frage stellt, ob sie sich in eine Taxonomie einteilen lassen. Viele Forscher haben dies mit uneinheitlichen Ergebnissen (Flynn, 1972; Gajski und Puer, 1985; und Treleaven, 1985) versucht. Unglücklicherweise ist bisher noch kein Carolus Linnaeus für Parallelrechner hervorgetreten. Das einzige verwendete Schema ist das von Flynn, und selbst das ist höchstens eine sehr grobe Annäherung. Es wird in Tabelle 14.1 dargestellt. Instruktionsströme 1 1 mehrere mehrere

Datenströme 1 mehrere 1 mehrere

Bezeichnung SISD SIMD MISD MIMD

Beispiele klassische Von-Neumann-Maschine Vektor-Computer, Matrixprozessor keine realisierten Rechner Multiprozessoren, Multicomputer

Tabelle 14.1.: Flynns Taxonomie für Parallelrechner Flynns Einteilung basiert auf zwei Konzepten: Befehls- und Datenströmen. Ein Befehlsstrom entspricht einem Programmzähler. Ein System mit n Prozessoren hat n Programmzähler und folglich n Befehlsströme. Die Befehls- und Datenströme sind bis zu einem gewissen Grad unabhängig, sodass vier Kombinationen existieren, wie in Tabelle 14.1 aufgeführt. SISD ist einfach der klassische, sequentielle Von-Neumann-Rechner. Er hat einen Befehls- und einen Datenstrom und macht nur eine Sache gleichzeitig. SIMD-Maschinen haben eine einzelne Kontrolleinheit, die einen Befehl ausgibt, aber mehrere ALUs, die diesen Befehl auf mehreren Datenmengen gleichzeitig ausführen. Der ILLIAC IV, der 1974 für die NASA gebaut wurde, war ein Prototyp für SIMD-Maschinen. Es gibt viele moderne SIMD-Maschinen, die für wissenschaftliche Berechnungen benutzt werden. MISD-Maschinen sind eine ein wenig seltsame Kategorie mit mehreren Befehlen, die auf dem gleichen Stück Daten arbeiten. Es ist nicht geklärt, ob solche Maschinen existieren (d. h. ob eine Maschine in eine solche Kategorie passt), obwohl einige Leute Maschinen mit Pipelining zu den MISD zählen. Schließlich gibt es noch MIMD; wobei diese Maschinen aus mehreren unabhängigen Prozessoren bestehen, die als Teil eines größeren Systems fungieren. Die meisten Parallelrechner fallen in diese Kategorie. Sowohl Multiprozessoren als auch Multicomputer sind hier einzuordnen. Flynns Einteilung endet hier, wir haben sie jedoch in Abbildung 14.14 erweitert. SIMD wurde in zwei Untergruppen zerteilt. Die erste ist für numerische Supercomputer und andere Maschinen, die mit Vektoren arbeiten, um die gleiche Operation auf jedem Vektorelement auszuführen. Die zweite Untergruppe ist für parallele Maschinen wie den

310

Technische Informatik I

14.4. Taxonomie von Parallelrechnern

Parallele Computerarchitekturen

SIMD

VektorProzessor

SISD

ArrayProzessor

UMA

BUS

MISD

MultiProzessoren

COMA

Switched

CC-NUMA

NUMA

NC-NUMA

MIMD

MultiComputer

MPP

Grid

COW

HyperCube

Abbildung 14.14.: Taxonomie von Parallelrechnern ILLIAC IV, in dem eine Hauptkontrolleinheit mehreren unabhängigen ALUs die gleiche Anweisung zuweist. In unserer Einteilung wurde die MIMD-Kategorie in Multiprozessoren (mit gemeinsamen Speicher) und Multicomputer (nachrichtenversendende Maschinen) aufgeteilt. Es existieren drei Arten von Multiprozessoren, je nach dem, wie der gemeinsame Speicher darin implementiert wurde. Sie heißen UMA (uniform memory access, gleichförmiger Speicherzugriff), NUMA (non-uniform memory access, uneinheitlicher Speicherzugriff), und COMA (cache only memory architecture, Nur-Cache-Speicherarchitektur). Diese Kategorien existieren, weil in großen Multiprozessoren der Speicher normalerweise in mehrere Module aufgeteilt wird. UMA-Maschinen haben die Eigenschaft, dass jeder Prozessor die gleiche Zugriffszeit auf jedes Speichermodul hat. In anderen Worten: Jedes Speicherwort kann genauso schnell gelesen werden wie jedes andere Speicherwort. Wenn dies technisch unmöglich ist, wird der schnellste Zugriff auf die Geschwindigkeit des langsamsten heruntergesetzt, sodass Programmierer den Unterschied nicht bemerken. Das ist es, was „gleichförmig“ hierbei bedeutet. Diese Gleichförmigkeit macht die Leistung vorhersagbar und dies ist ein wichtiger Faktor für das Schreiben von effizientem Code. Im Gegensatz dazu besitzt eine NUMA-Architektur diese Eigenschaft nicht. Oftmals gibt es ein Speichermodul dicht bei jedem Prozessor und der Zugriff auf dieses Modul ist viel schneller als der Zugriff auf weiter entferntere. Das Ergebnis ist, dass es aus Leistungsgründen eine Rolle spielt, wo Code und Daten platziert werden. COMA-Maschinen sind ebenfalls ungleichförmig, jedoch auf eine andere Art. Die zweite Hauptkategorie von MIMD-Maschinen besteht aus den Multicomputern, die im Gegensatz zu den Multiprozessoren keinen gemeinsamen Speicher auf Architekturebene haben. In anderen Worten: Das Betriebssystem eines Multicomputers kann nicht

Technische Informatik I

311

14. Parallelrechner auf den Speicher eines anderen Prozessors zugreifen, indem es einfach eine Ladeanweisung ausführt. Es muss eine explizite Nachricht versenden und auf die Antwort warten. Die Fähigkeit, ein entferntes Datenwort durch eine simple Ladeanweisung zu lesen, ist es, die Multiprozessoren von Multicomputern unterscheidet. Wie bereits erwähnt, können selbst bei einem Multicomputer einige Nutzerprogramme die Fähigkeit haben, auf entfernte Daten mit Lade- und Speicherungsbefehlen zuzugreifen, jedoch wird diese Illusion durch das Betriebssystem geboten, nicht durch die Hardware. Dieser Unterschied ist zwar klein, aber von großer Wichtigkeit. Da Multicomputer keinen direkten Zugriff auf entfernten Speicher haben, werden sie manchmal NORMA (no remote memory access, kein Zugriff auf entfernten Speicher) genannt. Multicomputer lassen sich grob in zwei Kategorien einteilen. Die erste Kategorie enthält die MPPs (massively parallel processing), sehr teure Supercomputer bestehend aus vielen Prozessoren im engen Verbund durch ein proprietäres Hochgeschwindigkeitsnetzwerk. Der Cray T3E und der IBM SP/2 sind bekannte Beispiele. Die andere Kategorie besteht aus normalen PCs oder Workstations (vielleicht in einem entsprechenden einzelnen Gehäuse), die durch kommerzielle handelsübliche Verbindungstechnologie verbunden sind. Logisch gesehen gibt es keinen Unterschied, aber riesige Supercomputer, die mehrere Millionen Euro kosten, werden anders verwendet als Netzwerke aus PCs, die von den Nutzern für einen Bruchteil des Preises eines MPP zusammengesetzt wurden. Diese „hausgemachten“ Maschinen haben verschiedene Namen, wie zum Beispiel NOW (network of workstations) oder COW (cluster of workstations) oder auch TINA (Tina is no acronym) als Clusterrechner der Universität Magdeburg.

14.5. Zusammenfassung Ausgehend von der Beobachtung, dass Rechner immer leistungsfähiger werden und die Anforderungen an sie mindestens im gleichen Maßstab wachsen, haben wir gesehen, dass sich Rechnerarchitekten im Hinblick auf immer größere Rechenaufgaben mehr und mehr Parallelrechnern hinzuwenden. Hierbei wurde die Parallelität auf verschiedenen Ebenen eingeführt. Es wurden die Fragen nach der Art, Größe und Anzahl der Speicherelemente und die Verbindung mit den Rechenelementen beantwortet. Obwohl eine gewisse Variation zwischen den CPU- und Speicherdesigns besteht, unterscheiden sich Parallelrechnersysteme am meisten in der Art, wie die Komponenten miteinander verbunden sind. Zwei Ausprägungen, wie die Prozessoren miteinander kommunizieren – Multiprozessoren und Multicomputer – wurden vorgestellt. Die wichtigsten Merkmale sind im Folgenden kurz zusammengefasst: • In Multiprozessorsystemen teilen sich alle Prozessoren einen gemeinsamen Speicher. • In Multicomputersystemen besitzt jeder Prozessor seinen eigenen Speicher, auf den die anderen Prozessoren nicht direkt zugreifen können. Stattdessen sind sie durch ein Verbindungsnetzwerk verbunden. • Der Vorteil von Multiprozessorsystemen ist, dass sie einfach zu programmieren sind, der von Multirechnersystemen, dass sie einfach zu bauen sind. • Der Nachteil von Multiprozessorsystemen ist, dass sie schwer zu bauen sind, der von Multirechnersystemen ist ihre schwierige Programmierung.

312

Technische Informatik I

14.5. Zusammenfassung Einen Schwerpunkt in diesem Kapitel bildeten die Verbindungsnetzwerke. Sie können aus bis zu fünf verschiedenen Komponentenklassen bestehen: Prozessoren, Speichermodule, Schnittstellen, Verbindungen und Schalter. Die Topologie oder Struktur eines Verbindungsnetzwerkes beschreibt, wie die Verbindungen und Schalter angeordnet sind. Dies kann zum Beispiel eine Ring- oder Gitterstruktur sein. Wir haben gesehen, dass topologische Entwürfe als Graphen modelliert werden können, wobei die Verbindungen als Kanten und die Schalter als Knoten modelliert werden. Pakete, die über diese Verbindungsnetzwerke gesendet werden, folgen bestimmten Routen. Routing Algorithmen bestimmen die Folge von Knoten, die ein Paket von der Quelle zum Ziel zu nehmen hat. Ein guter Algorithmus kann die Last auf mehrere Verbindungen verteilen. Hierbei haben wir Quellen Routing und Verteiltes Routing kennen gelernt. Zur Bestimmung der Leistung wurden die Begriffe Hardwaremetriken und Softwaremetriken eingeführt. Hardwaremetriken wie Wartezeit und Bandbreite betrachten allerdings nur, was die Hardware zu leisten imstande ist. Für den Nutzer interessanter ist aber, um wie viel schneller seine Programme auf einem Parallelrechner gegenüber einem Einzelprozessorsystem ausgeführt werden. Wünschenswert wäre Idealerweise ein skalierbares System, welches, wenn Prozessoren hinzugefügt werden, die gleiche durchschnittliche Bandbreite pro Prozessor und eine konstante durchschnittliche Wartezeit beibehält. Wie wir gesehen haben, ist die Versorgung mit ausreichend Bandbreite pro Prozessor in der Praxis noch machbar, allerdings erhöht sich in allen realen Aufbauten die Wartezeit mit der Größe des Systems. Um diesem Problem zu begegnen wurden Techniken, wie das Prefetching und das Multithreading erläutert, die die Wartezeit verbergen sollen. Es wurde eine Möglichkeit der Klassifizierung auf der Grundlage von Flynns Taxonomie von Parallelrechnern aufgezeigt und erweitert, sodass ein detaillierterer Überblick über das Feld der Parallelrechner geschaffen werden konnte.

Technische Informatik I

313

15. Literatur 15.1. Empfohlene Literatur • Tanenbaum: Structured Computer Organization, Prentice Hall • Nicholas P. Carter: Computerarchitektur - IT-Studienausgabe, mitp • Clements: The Principles of Computer Hardware, Oxford University Press • B. Becker, R. Drechsler, P. Molitor: Technische Informatik - Eine Einführung, Pearson Studium • Hamacher, Vranesic, Zaky: Computer Organization, McGraw-Hill • Comer, Douglas E.: Essentials of Computer Architecture, Pearson Prentice Hall

15.2. Ergänzende Literatur • Dworatschek: Grundlagen der Datenverarbeitung, de Gruyter • Oberschelp/Vossen: Rechneraufbau und Rechnerstrukturen, Oldenbourg • Paul: Elektrotechnik und Elektronik für Informatiker, Teubner • H.-D. Wuttke, K. Henke: Schaltsysteme - Eine automatenorientierte Einführung, Pearson Studium

315

Index Äquivalenz von Schaltnetzen, 90 Überlagerung, 266 Überlauf, 143 address mapper, 206 Adressdekoder, 33 Adresse, 37 Adressraum, 39 Adressregister, 27 Adressumsetzer, 206 Adressumsetzung, 267 aggregate bandwidth, 305 Akkumulator, 23 Alphabet, 9 ALU, 18, 23 Amdahls Gesetz, 307 AND, 83 Angepasstes Routing, 303 Anweisung dekodieren, 226 Anweisung holen, 226 Anweisungsdekodierer, 212 Arithmetic Logic Unit, 23 Arithmetik-Logik-Einheit, 23 ASCII-Code, 180 Assemblerdirektive, 65 Assemblersprache, 64 asynchron, 117 Ausführen, 226 Ausgabepufferung, 302 Ausnahmeprogrammzähler, 280 Baum, 298 bedingte Verzweigung, 25 bedingter Sprung, 25 bedingter Sprung-Befehl, 56 Befehl, 203 Befehls-Ausführungsphase, 54 Befehlscode, 64 Befehlsholphase, 54

Befehlsregister, 22 Big Endian, 39 Binary-Coded Decimal, 138 Bisektionsbandbreite, 298, 305 Bit, 10 Bit, 36 bit-pair-recoding, 162 Bitmap, 182 Blase, 228 Blasenspeicher, 32 Block, 236 boolesche Funktion, 94 Booth-Algorithmus, 155 Bridging Fault, 129 bubble, 228 bubble memory, 32 Bus, 18 Byte, 36 Cache, 237 Cache mit direkter Zuweisung, 238 cache only memory architecture, 311 Cachespeicherzugriff, 311 Carry-Flipflop, 23 carry-save addition, 163 CCR, 23 circuit switching, 299 CISC, 219 Code, 179 COMA, 311 Complex Instruction Set Computer, 219 Computer mit komplexem Befehlssatz, 219 Computer mit verkleinertem Befehlssatz, 219 Condition Code Register, 23 condition select, 208 Control Unit, 22 COW, 312 critical word first, 250

317

Index FIFO, 74 fine-grained parallelism, 291 First In, First Out, 74 Flüchtiger Speicher, 30 Flag, 23 Flipflop, 109 früher Neustart, 249 funktionale Vollständigkeit, 95

CU, 22 Datenabhängigkeit, 229 Datenpfadzyklus, 20 Datenworte, 20 Defekt, 129 Degree, 297 direct mapped cache, 238 direkter Zugriff, 30 Dirty-Bit, 273 disjunktive Normalform, 95 Distanz (Code), 183 distributed memory system, 293 Durchmesser, 297 Durchschreiben, 244 Dynamischer Speicher, 31 early restart, 249 Echo, 70 EEPROM, 48 Ein-Adress-Befehl, 21 eindimensionales Netzwerk, 297 einfache Genauigkeit, 173 Eingabepufferung, 301 Einheitsdistanzcode, 183 Elektrischer Speicher mit einer gespeicherten Ladung, 31 Elektrischer Speicher mit Rückführung, 31 end-around-carry, 141 Enger Verbund, 291 EPROM, 48 error correction codes, 189 error detection codes, 189 execute, 226 Exportierter Parameterraum, 223 Fairclough, 219 Fanout, 297 fat tree, 298 Fehlerrate, 236 Fehlerstrafe, 236 feingranulare Parallelität, 291 Fensterüberlauf, 225 Fensterunterlauf, 225 Fensterzeiger, 224 Ferromagnetismus, 31 Festkommaarithmetik, 169 Festkommazahlen, 169

318

Gültigkeitsbit, 239 Gatter, 11 Geflecht, 299 Gemeinsame Puffer, 302 Gerätezustandsregister, 72 Gesamtbandbreite, 305 Geteilter Speicher, 291 Gitter, 299 gleichförmiger Speicherzugriff, 311 Gleitkommazahlen, 171 Globaler Raum, 223 Grad, 297 grain size, 291 Granularität, 291 grobgranulare Parallelität, 291 Halbaddierer, 144 halbduplex, 297 Hamming-Distanz, 183 hit, 236 hit rate, 236 hit time, 236 Hyperwürfel, 299 immediate addressing mode, 25 Importierter Parameterraum, 223 incrementer, 206 Index, 241 Indexadressierung mit Verschiebung, 62 Indexregister, 60 Inkrementer, 206 instruction decode, 226 instruction fetch, 21, 226 Instruction Register, 22 interleaving, 251 Interne Übergabe, 230 Interpretation (Makrobefehl), 203 Interrupt, 209 Inversionsblase, 84 IR, 22

Technische Informatik I

Index kanonische disjunktive Normalform, 95 Kaufmännisches Runden, 176 Kein Zugriff auf entfernten Speicher, 312 Kellerspeicher, 72 kombinatorisches Schaltnetz, 83 kritisches Wort zuerst, 250 Lader, 68 Last In, First Out, 72 Least Recently Used, 263 Lese/Schreib-Signal, 206 LIFO, 72 lineare Befehlsfolge, 55 Linkregister, 76 Linksschieberegister, 123 Listenkopfblockierung, 301 Little Endian, 39 load control, 208 Lokaler Raum, 223 Lokalitätsprinzip, 233 loosely-coupled systems, 291 Loser Verbund, 291 low-active, 112 LRU, 263

Minterm, einschlägig, 94 MISD, 310 miss, 236 miss penalty, 236 miss rate, 236 mnemonisches Symbol, 64 MPP, 312 Multilevel Caching, 252 Multiplexer, 86 Multithreading, 309 NAND, 83 next microinstruction address, 208 NICHT, 83 no remote memory access, 312 non-uniform memory access, 311 NOR, 83 NORMA, 312 NOT, 83 NOW, 312 NUMA, 311 Nur-Lese-Speicher, 30

Magnetischer Speicher, 31 Makrobefehl, 203 MAR, 22 massively parallel processing, 312 Master-Slave-Flipflop, 119 Mathematisches Runden, 176 MBR, 22 Mechanismus, 203 Memory Address Register, 22 Memory Buffer Register, 22 memory mapped I/O, 71 memory mapping, 267 microinstruction register, 206 microprogram counter, 206 microprogram memory, 206 Mikrobefehl, 203 Mikroprogramm, 206 Mikroprogrammregister, 206 Mikroprogrammspeicher, 206 Mikroprogrammzähler, 206 MIMD, 310 minimal, Schaltnetz, 91 Minterm, 94

Objektprogramm, 64 ODER, 83 operand fetch, 226 operand store, 226 Operanden holen, 226 Operanden speichern, 226 OR, 83 Oszillationsproblem, 118 Overlay, 266 p-Bit-Fehler prüfbar, 191 page, 267 page fault, 267 page offset, 268 page table register, 270 pages, 43 Parameterübergabe, 77 Partielle Adresskodierung, 41 Pfadsensitivierung, 129 physische Adresse, 29, 267 physischer Adressraum, 37 physischer Speicher, 267 Pipelining, 227 Power-On Self-Test (POST), 128 Prefetching, 309 programmgesteuerte Ein-/Ausgabe, 69

Technische Informatik I

319

Index Programmzähler, 38 Prozessorstack, 77 Quellen-Routing, 303 Quellprogramm, 64 Queue, 74 Räumliche Lokalität, 234 Räumlicher Speicher, 32 RAM, 30 random access, 30 random access memory, 30 read-only memory, 30 Rechtsschieberegister, 123 Reduced Instruction Set Computer, 219 Referenzübergabe, 78 refresh, 31 Register, 18, 123 Register-Register-Operation, 19 Register-Speicher-Operation, 20 Ringpuffer, 75 ripple counter, 126 RISC, 219 ROM, 30, 47 round-trip latency, 304 Routing-Algorithmus, 302 RS-Flipflop, 111 RS-Flipflop, getaktet, 114 Rundreise-Wartezeit, 304 Schaltfunktion, 94 Schieben, 123 Segmentierung, 269 Seite, 267 Seiten, 43 Seitenfehler, 267 Seitentabelle, 270 Seitentabellenregister, 270 Seitenversatz, 268 selbsterhaltend, 110 Semantik, 179 sequentielle Schaltung, 109 sequentieller Zugriff, 30 Sequenzer, 213 Serieller Zugriff, 30 Shared-Memory-System, 291 Sichere Pop-Operation, 74 Sichere Push-Operation, 74 Signifikant, 173

320

SIMD, 310 simplex, 296 SISD, 310 skalierbar, 307 Speicher mit wahlfreiem Zugriff, 30 Speicherabbildung, 39, 267 Speicheradressregister, 22 Speicherauffrischung, 31 Speicherhierarchie, 234 Speicherpufferregister, 22 Speicherzelle, 29 Sprunganweisung, 56 Sprungversatz, 68 Störung, 129 Stack, 72 stack pointer, 73 Stack: Bottom, 72 Stack: pop, 72 Stack: push, 72 Stack: Top, 72 Stackrahmen, 78 Stapel, 72 Statischer Speicher, 30 Statisches Routing, 303 Statusregister, 57 Steuereinheit, 22 Store-And-Forward-Packet-Switching, 300 Streuung, 299 Struktur, 297 Struktureller Speicher, 31 Stuck-at-one, 129 Stuck-at-zero, 129 subroutine, 75 Subroutinenaufruf, 75 Subroutinenverschachtelung, 76 Symbol, 9 Symboltabelle, 68 synchron, 117 Syntax, 64, 179 Tag, 239 Tag-Feld, 241 Taktflanke, 120 tightly-coupled systems, 291 TLB, 274 Topologie, 297 Torus, 299 translation lookaside buffer, 274

Technische Informatik I

Index zweidimensionales Netzwerk, 297 Zwischenspeicher, 237

Treffer, 236 Trefferrate, 236 Trefferzeit, 236 Tri-State-Gatter, 204 UMA, 311 UND, 83 uneinheitlicher Speicherzugriff, 311 uniform memory access, 311 Unterbrechung, 209 Unterprogramm, 75 Unterprogrammaufrufmethode, 76 valid bit, 239 Verbindungs-Switching, 299 Verbindungsnetzwerk, 296 Vergesslichkeit, 29 Verklemmung, 303 Verteilter Speicher, 293 verteiltes Routing, 303 Verzweigungsgrad, 297 virtueller Speicher, 266 volatile memory, 30 vollduplex, 297 Von-Neumann-Runden, 175 Vorladen, 309 Würfel, 299 Wahlfreier Zugriff, 30 Wahrheitstabelle, 84 Warteschlange, 74 Wegbestimmungsalgorithmus, 302 Wertübergabe, 78 window pointer, 224 Wort, 122 Wort, 37 write-back, 245 write-through, 244 XOR, 84 Zeitdiagramm, 34 Zeitliche Lokalität, 233 Zugriffsfehler, 236 Zugriffszeit, 29 Zurückschreiben, 245 Zustandsbits, 57 Zustandsregister, 23, 57 Zwei-Schritt-Assembler, 68

Technische Informatik I

321

Schlussbemerkung Dieses Skript stellt kein Lehrbuch im Sinne eines vollständigen Kompendiums dar, welches den Vorlesungsstoff komplett abbildet, sondern ist nur eine Grundlage, ein Startpunkt. Es kann gut verwendet werden, um mit Informationen während der Vorlesung ergänzt zu werden.

Der Inhalt dieses Skripts fußt in wesentlichen Teilen auf den Skripten früherer Vorlesungen zu diesem Thema. Den damaligen Autoren sind wir zu Dank verpflichtet.

323