Skript - Universität Paderborn

Transcription

Skript - Universität Paderborn
Hardware/Software
Codesign
Skript zur Vorlesung
SS 2010
Christian Plessl
[email protected]
Paderborn Center for Parallel Computing
Universität Paderborn
v 1.1
Vorwort
Dieses Skript stellt einen Teil der Unterlagen für die Vorlesung Hardware/Software Codesign dar. Zusätzlich werden Kopien der Vortragsfolien und fallweise auch von ausgewählten Publikationen zur Verfügung gestellt. Das Skript kann als Referenz für manche
in der Vorlesung behandelten Themenbereiche dienen, deckt aber nicht alle Bereiche ab.
In dem behandelten Gebiet werden sehr viele englische Fachbegriffe verwendet, für
die entweder gar keine oder nur eine kaum gebrauchte deutsche Übersetzung existiert.
In diesem Skript wird deshalb auf eine Übersetzung dieser Begriffe ins Deutsche verzichtet.
Das vorliegende Skript basiert auf den Vorlesungsunterlagen von Dr. Marco Platzner,
der die Vorlesung Hardware/Software Codesign aufgebaut und viele Jahre an der ETH
Zürich und an der Uni Paderborn gelesen hat. An dieser Stelle sei ihm dafür gedankt,
dass er seine Unterlagen zur Verfügung gestellt hat. Besonderer Dank geht auch an Dr.Ing. Jürgen Teich und Dr. Matthias Gries, die durch ihre Beiträge und Korrekturen zur
Verbesserung dieses Skriptes beigetragen haben.
Christian Plessl
i
ii
Inhaltsverzeichnis
1
Einleitung
1.1 Hardware/Software Codesign . . . . . . . . . . . . . . . . . . . . . . . . .
1.2 Codesign von eingebetteten Systemen . . . . . . . . . . . . . . . . . . . . .
2
Zielarchitekturen für HW/SW-Systeme
2.1 Grundstruktur von HW/SW-Systemen
2.2 Implementierungsarten . . . . . . . . .
2.2.1 General-Purpose Prozessoren . .
2.2.2 Microcontroller . . . . . . . . . .
2.2.3 DSPs . . . . . . . . . . . . . . . .
2.2.4 ASIPs . . . . . . . . . . . . . . . .
2.2.5 FPGAs . . . . . . . . . . . . . . .
2.3 Systemaufbau . . . . . . . . . . . . . . .
2.3.1 Systems-on-a-Chip . . . . . . . .
2.3.2 Board-level Systeme . . . . . . .
3
4
1
1
2
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
5
5
5
9
15
18
25
28
32
32
34
Systementwurf – Methoden und Modelle
3.1 Entwurfsmethoden . . . . . . . . . . . . . . . . . .
3.1.1 Erfassen und Simulieren . . . . . . . . . . .
3.1.2 Beschreiben und Synthetisieren . . . . . .
3.1.3 Spezifizieren, Explorieren und Verfeinern .
3.2 Abstraktion und Entwurfsrepräsentationen . . . .
3.2.1 Modelle . . . . . . . . . . . . . . . . . . . .
3.2.2 Synthese . . . . . . . . . . . . . . . . . . . .
3.2.3 Optimierung . . . . . . . . . . . . . . . . .
3.3 Graphenmodelle für Kontroll- und Datenfluss . .
3.3.1 Datenflussgraphen (DFGs) . . . . . . . . .
3.3.2 Kontrollflussgraphen (CFGs) . . . . . . . .
3.3.3 Kontroll/Datenflussgraphen (CDFGs) . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
39
39
39
39
40
41
41
43
50
51
52
52
53
.
.
.
.
.
59
59
59
60
63
67
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Compiler und Codegenerierung
4.1 Compiler – Aufbau . . . . . . . . . . . . . . . .
4.1.1 Aufgaben eines Compilers . . . . . . .
4.1.2 Phasen eines Compilers . . . . . . . . .
4.1.3 Zwischencode . . . . . . . . . . . . . . .
4.1.4 Grundblöcke und Kontrollflussgraphen
iii
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
INHALTSVERZEICHNIS
iv
4.2
4.3
4.4
4.5
5
6
Codegenerierung . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2.1 Modellmaschine . . . . . . . . . . . . . . . . . . . . . .
4.2.2 Einfacher Codegenerator . . . . . . . . . . . . . . . . .
4.2.3 Registerbindung . . . . . . . . . . . . . . . . . . . . . .
4.2.4 Codegenerierung für DAGs . . . . . . . . . . . . . . . .
4.2.5 Codegenerierung mit Dynamische Programmierung .
Codeoptimierung . . . . . . . . . . . . . . . . . . . . . . . . . .
4.3.1 Peephole Optimierung . . . . . . . . . . . . . . . . . . .
4.3.2 Lokale Optimierung . . . . . . . . . . . . . . . . . . . .
4.3.3 Globale Optimierung . . . . . . . . . . . . . . . . . . .
Codegenerierung für Spezialprozessoren . . . . . . . . . . . .
4.4.1 Nicht-homogene Registersätze, irreguläre Datenpfade
4.4.2 Zuweisung von Speicheradressen und Adressregistern
4.4.3 Codekompression . . . . . . . . . . . . . . . . . . . . .
Retargetable Compiler . . . . . . . . . . . . . . . . . . . . . . .
4.5.1 Codegenerierung durch Baumübersetzung . . . . . . .
4.5.2 Prozessormodellierung . . . . . . . . . . . . . . . . . .
4.5.3 Fallbeispiele . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
70
72
74
76
80
82
85
86
88
89
91
93
97
108
109
110
113
114
Systempartitionierung
5.1 Modelle für die Systemsynthese . . . . . . . . . . . . . .
5.2 Partitionierung . . . . . . . . . . . . . . . . . . . . . . . .
5.3 Allgemeine Partitionierungsalgorithmen . . . . . . . . .
5.3.1 Konstruktive Partitionierungsverfahren . . . . . .
5.3.2 Iterative Partitionierungsverfahren . . . . . . . . .
5.3.3 Partitionierung mit Evolutionären Algorithmen .
5.3.4 Partitionierung mit linearer Programmierung . .
5.4 Algorithmen zur HW/SW-Partitionierung . . . . . . . .
5.5 Entwurfssysteme zur funktionalen Partitionierung . . .
5.5.1 Funktionale Partitionierung im Hardwareentwurf
5.5.2 Funktionale Partitionierung im Systementwurf .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
115
115
120
121
122
124
127
127
128
129
129
131
Abschätzung der Entwurfsqualität
6.1 Parameter von Schätzverfahren . . . . . . .
6.2 Qualitätsmasse . . . . . . . . . . . . . . . .
6.2.1 Performancemasse . . . . . . . . . .
6.2.2 Kostenmasse . . . . . . . . . . . . .
6.3 Abschätzung von Hardware . . . . . . . . .
6.3.1 Abschätzung der Taktperiode . . . .
6.3.2 Abschätzung der Latenz . . . . . . .
6.3.3 Abschätzung der Ausführungszeit .
6.3.4 FSMD Modell . . . . . . . . . . . . .
6.3.5 Abschätzung der Fläche . . . . . . .
6.4 Abschätzung von Software . . . . . . . . .
6.4.1 Programmpfadanalyse . . . . . . . .
6.4.2 Modellierung der Mikroarchitektur
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
135
135
137
138
142
142
142
144
144
144
145
147
148
151
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
INHALTSVERZEICHNIS
6.4.3
7
v
Speicherbedarf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
Weiterführende Hw/Sw-Codesign Themen
7.1 Interface- und Kommunikationssynthese . . . . . . . .
7.1.1 Kommunikationsmodelle . . . . . . . . . . . . .
7.1.2 Interfacesynthese . . . . . . . . . . . . . . . . . .
7.2 Emulation und Rapid Prototyping . . . . . . . . . . . .
7.2.1 Simulation vs. Emulation digitaler Schaltungen
7.2.2 Aufbau von Emulationssystemen . . . . . . . .
7.2.3 Beispiele für Emulationssysteme . . . . . . . . .
7.2.4 Einsatz von Emulationssystemen . . . . . . . . .
7.2.5 Rapid Prototyping Systeme . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
155
155
155
157
159
159
160
164
166
167
Kapitel 1
Einleitung
Dieses Kapitel stellt das Gebiet HW/SW Codesign und seine Motivation und Zielsetzung vor. Danach werden HW/SW Codesign Themen beim Entwurf eingebetteter Systeme und die Struktur der Vorlesung diskutiert.
1.1
Hardware/Software Codesign
Der Begriff Codesign wird häufig als integrierter Entwurf von Systemen, bestehend aus
sowohl Hardware- als auch Softwarekomponenten aufgefasst. Hardware/Software Systeme
existieren bereits seit vielen Jahren. Neu sind jedoch Entwurfsmethoden, die es erlauben, HW- und SW-Komponenten eines Systems gemeinsam zu entwerfen und dabei
Entwurfsalternativen abzuwägen.
Die Silbe CO im Wort Codesign erlaubt zahlreiche Interpretationen, die zusammen
gesehen die wichtigsten Eigenschaften dieses neuen Forschungszweiges umreissen:
• co (zusammen): Codesign bedeutet den gemeinsamen Entwurf von HW und SW.
Entwurfsalternativen - welche Funktion in HW, welche in SW - können untersucht
und verglichen werden.
• coordinated (koordiniert): Codesign unterstützt einen systematischen Entwurfsfluss
für HW/SW-Systeme, der den breiten Einsatz rechnergestützter Werkzeuge erlaubt.
• concurrent (nebenläufig): Concurrent tritt hier in zwei Bedeutungen auf. Einerseits
arbeiten die HW- und SW-Komponenten eines Systems nebenläufig. Andererseits
unterstützt Codesign concurrent engineering, d.h., die Entwicklerteams für HWund SW-Komponenten arbeiten gleichzeitig an ihren Entwurfsaufgaben. Dies steht
im Gegensatz zu klassischen Methoden, bei denen meist zuerst die HW und danach die SW entwickelt wurde.
• complex (komplex): Codesign Methoden sind vor allem für komplexe Systeme notwendig.
• correct (korrekt): Codesign soll zu korrekten HW/SW Systemen führen. Die Korrektheit eines Systems erfordert Validation durch Co-Simulation/Co-Emulation
oder Co-Verifikation.
1
KAPITEL 1. EINLEITUNG
2
Historisch gesehen ist HW/SW Codesign ein Forschungsgebiet, dessen Entstehung,
Motivationen und Zielsetzungen durch folgende Entwicklungen geprägt sind:
• Technologiefortschritte, zunehmende Komplexität und Vielfalt der Anwendungen: Durch
Fortschritte in der Mikroelektronik, z.B. Submicron-technologien, können immer
mehr Transistoren auf einem Chip integriert werden. Dadurch werden neue Systemrealisierungen, wie Ein-Chip-Lösungen oder benutzerkonfigurierbare Prozessoren, möglich. Diese neuen Möglichkeiten finden zahlreiche neue Anwendungen.
Gerade im Bereich eingebetteter Systeme und Echtzeitsysteme werden die Problemstellungen zunehmend komplexer und vielfältiger. Die Komplexität ergibt
sich einerseits aus der Grösse der Systeme, andererseits aus ihrer Heterogenität.
Der Entwurf von solchen komplexen und heterogenen Systemen bedarf systematischer Methoden und rechnergestützter Werkzeuge.
• Zunehmende Automatisierung höherer Entwurfshierarchien: Fortschritte in automatisierten und formalen Methoden führten für HW zu Logik- und Architektursynthese, für SW zu optimierenden Compilern, die sehr schnell bzw. automatisch
an neue Prozessorarchitekturen angepasst werden können, und zu Spezifikationssprachen, die zum Teil eine formale Verifikation von Entwürfen erlauben. Aufbauend auf diese Werkzeuge ist es nun möglich, auch den Entwurf auf Systemebene
zunehmend zu automatisieren bzw. zumindest durch Werkzeuge zu unterstützen.
• Entwurf kostenoptimaler Realisierungen: Die Wettbewerbsfähigkeit eines Systems ist
wesentlich bestimmt durch die Kosten und die time-to-market, die Zeit von der
Konzeption eines Systems bis zu dessen Erscheinen auf dem Markt. Um sowohl
Kosten als auch time-to-market zu verringern, benötigt man rechnergestützte Entwurfsverfahren auf möglichst vielen Ebenen des Entwurfs.
HW/SW Codesign Problemstellungen treten sowohl bei general-purpose systems als
auch bei embedded systems auf. Im Bereich der general-purpose Systeme (PCs, Workstations, etc.) geht es um den gemeinsamen Entwurf von Prozessor und Compiler
bzw. Betriebssystem. HW/SW Entwurfsalternativen betreffen die Auswahl der Instruktionen, die Nutzung von Parallelität durch Pipelining und mehrere skalare Einheiten
und Caching-Strategien. Im Bereich der embedded systems (Mobiltelefon, Robotersteuerungen, etc.) gibt es zwei bedeutende HW/SW Codesign Bereiche: Das ist einerseits der
gemeinsame Entwurf von eingebetteten Spezialprozessoren und optimierenden Compilern. Man ist besonders daran interessiert, die Codegeneratoren der Compiler möglichst schnell (idealerweise automatisch) an neue Prozessorarchitekturen anzupassen.
Der Entwurf von Systemen auf Systemebene (system level design) ist das zweite wesentliche HW/SW Codesign Thema im Bereich der eingebetteten Systeme. In der Systemsynthese sollen möglichst viele verschiedene Entwurfsalternativen untersucht und
verglichen werden.
1.2
Codesign von eingebetteten Systemen
Einige der im Rahmen des Systementwurfs wichtigen Aufgaben sind in Abb. 1.1 dargestellt.
1.2. CODESIGN VON EINGEBETTETEN SYSTEMEN
3
VerhaltensSpezifikation
Allokation
"
System-Reprasentation
"
Schatzung
Partitionierung
Software-Synthese
Protokoll- und
Schnittstellensynthese
Hardware-Synthese
Abbildung 1.1: Grobe Darstellung des Entwurfsablaufs auf Systemebene
Die gesamte Entwurfsmethodik auf Systemebene sollte eine einfache und effiziente
Möglichkeit bieten, verschiedene Entwurfsalternativen zu untersuchen. Die Voraussetzung dafür ist zunächst eine (ausführbare) Spezifikation des gewünschten Systemverhaltens. Anforderungen an eine solche Spezifikationssprache sind Simulierbarkeit, die
Möglichkeit zur formalen Verifikation, leichte Erlernbarkeit und Verständlichkeit, die
Möglichkeit zur Anbindung an CAD-Werkzeuge und Vollständigkeit (Beschreibung aller relevanten Systemeigenschaften).
Die folgenden Schritte hängen eng miteinander zusammen. In einer Allokationsphase müssen zunächst die Komponenten der Architektur ausgewählt werden, wie Prozessoren, Speicher und anwendungsspezifische integrierte Schaltungen. Diese Komponenten sind charakterisiert durch eine Vielzahl von Parametern, wie z.B. Zahl der abgearbeiteten Instruktionen pro Zeiteinheit bei Prozessoren, Grösse der Siliziumfläche bei anwendungsspezifischen integrierten Schaltungen oder Zugriffszeiten bei Speichern. Die
Softwarekomponenten können auf einer Vielzahl verschiedener Prozessoren implementiert werden, von CISC/RISC-Prozessoren, über Microcontroller oder digitale Signalprozessoren bis hin zu anwendungsspezifischen Instruktionssatzprozessoren. Die Hardwarekomponenten können entweder als anwendungsspezifische integrierte Schaltungen
oder auch in Form von programmierbaren Hardwarebausteinen realisiert werden. Auf
die Eigenschaften dieser unterschiedlichen Implementierungsvarianten wird im Kapitel
Zielarchitekturen für Hw/Sw Systeme eingegangen.
Eine Systemspezifikation wird in Hardware und Software-Komponenten aufgeteilt.
Dieser Prozess der HW/SW-Partitionierung und die verwendeten Algorithmen werden
im Kapitel Systempartitionierung behandelt.
Die Software-Komponenten werden auf einem oder mehreren Prozessoren ausgeführt. Für die Generierung von ausführbarem Code benötigt man Compilertechniken.
Die Grundaufgaben der Softwarecompilation sowie Verfahren der Codegenerierung
und Codeoptimierung, speziell für eingebettete Prozessoren, sind Gegenstand des Compiler und Codegenerierung.
Da jede neue Allokation und jede neue Partitionierung eine neue mögliche Systemimplementierung erzeugen, die man mit anderen Implementierungen vergleichen
4
KAPITEL 1. EINLEITUNG
will, muss man eine Schätzung der Systemeigenschaften durchführen. Jeder Satz von
Schätzwerten wird anschliessend mit den gegebenen Anforderungen verglichen und
aus den Implementierungen eine optimale ausgewählt. Schätzverfahren für HW und
SW sind Gegenstand des Kapitels Schätzung der Entwurfsqualität.
Nach der Auswahl einer Systemimplementierung muss die Spezifikation soweit
verfeinert werden, dass sie die strukturellen Eigenschaften der Implementierung auf
Systemebene nachbildet. Diesen Schritt nennt man Synthese. Da vom Entwurf auf Systemebene bis zu einer physikalischen Realisierung noch sehr viele Schritte notwendig
sind, erfordert ein systematisches Vorgehen die Einführung von Abstraktionsebenen
und Modellen. Im Kapitel Systementwurf - Methoden und Modelle werden die wichtigsten
Abstraktionsebenen beim Entwurf von Systemen und die Aufgabe und Bedeutung von
Syntheseverfahren vorgestellt. Dabei zeigt es sich, dass im Bereich des Hardwareentwurfs und des Softwareentwurfs im wesentlichen die gleichen Aufgaben gelöst werden
müssen. Lediglich die Modelle, die Nebenbedingungen und die Zielfunktionen bei der
Synthese unterscheiden sich und haben zu unterschiedlichen Optimierungsalgorithmen
geführt.
Am Ende der Vorlesung wird auf weiterführende Teilbereiche im Gebiet HW/SWCodesign eingegangen. Eine spezielle Syntheseaufgabe ist es, für die spezifizierten Kommunikationskanäle und Protokolle, über welche die Komponenten eines Systems kommunizieren, die benötigte Hardware und Software zu generieren. Eine besondere Rolle im Systementwurf kommt der Validierung zu. Zum Teil kann dafür formale Verifikation eingesetzt werden. Häufiger angewendete Validierungsmethoden sind aber CoSimulation und Co-Emulation. Speziell die Co-Emulation bzw. das rasche Erzeugen von
HW/SW-Protoypen (Rapid Prototyping) hat in den letzten Jahren stark an Bedeutung
gewonnen.
Kapitel 2
Zielarchitekturen für
HW/SW-Systeme
In diesem Kapitel werden die wichtigsten Implementierungsmöglicheiten für HW/SWSysteme vorgestellt. Nach der Spezifikation muss die Funktionalität eines Systems in
Software und/oder Hardware implementiert werden. Softwarekomponenten werden
auf Prozessoren implementiert. Neben general-purpose Prozessoren kommen im Bereich
der eingebetteten Systeme vor allem Spezialprozessoren zum Einsatz. Dies sind insbesondere Microcontroller und Digitale Signalprozessoren (DSPs), und noch stärker spezialisierte application-specific instruction set processors (ASIPs). Hardwarekomponenten können
in dedizierter Hardware, z.B. als application-specific integrated circuits (ASICs) oder auch in
programmierbarer Hardware realisiert werden. Der Systemaufbau eines HW/SW Systems
kann als System-on-a-Chip (Ein-Chip-System) oder als board-level System erfolgen.
2.1
Grundstruktur von HW/SW-Systemen
Abb. 2.1 zeigt den typischen Aufbau eines eingebetteten Systems. Die Komponenten des
Systems sind Sensoren, Aktoren, Interfaces und das digitale Zielsystem mit den Kommunikationsschnittstellen. Die Interfaces in dieser Abbildung sind die Verbindungsstelle
zwischen der digitalen und der analogen Welt. Die Kommunikationsports stellen Verbindungsmöglichkeiten zu anderen digitalen Systemen her. Sie sind – im eigentlichen
Sinne des Wortes – auch Interfaces (Schnittstellen). In Tabelle 2.1 sind einige Beispiele
für eingebettete Systeme angeführt.
2.2
Implementierungsarten
Die Funktionalität des digitalen HW/SW-Systems wird in Software- und/oder Hardwarekomponenten implementiert. Die einzelnen Implementierungsarten sind in Abb. 2.2
dargestellt.
5
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
6
digital domain
sensors
interface
digital
target
system
analog domain
interface
actors
communication
ports
Abbildung 2.1: Grundstruktur eines typischen Hardware/Software-Systems
Beispiel
Sensoren
Aktoren
Interfaces
Kommunikationsports
Laserdrucker
Hitzesensoren
Füllstandsmesser
Motoren
Heizelemente
Anzeigen
Zündung
Einspritzung
A/D-Wandler
D/A-Wandler
Pulsformer
A/D-Wandler
Ereigniszähler
D/A-Wandler
A/D-Wandler
D/A-Wandler
Ethernet-Transceiver
parallele Schnittstelle
serielle Schnittstelle
Autosteuerung
Mobiltelefon
Drehzahlmesser
Positionsgeber
Druckmesser
Tastenfeld
Akkuladestandsm.
Mikrophon
Lautsprecher
Display
Vibrationsgeber
CAN-Bus Transceiver
serielle Schnittstelle
Tabelle 2.1: Beispiele für eingebettete Systeme und deren Komponenten
Software Softwareimplementierungen basieren auf Prozessoren. Das Kennzeichen von
Prozessoren ist ihre Programmierbarkeit, d.h., sie haben einen mehr oder weniger vielfältigen Instruktionssatz, der es erlaubt, unterschiedlichste Anwendungen (Programme)
zu realisieren. Entsprechend der Spezialisierung des Instruktionssatzes auf bestimmte
Anwendungsbereiche unterscheidet man verschiedene Prozessortypen: Die allgemeinste Klasse sind general-purpose (GP) Prozessoren. Bei den spezialisierten Prozessoren
sind vor allem zwei Prozessortypen von Bedeutung: Microcontroller und digitale Signalprozessoren (DSPs). Eine noch weitere Spezialisierung führt zu den application-specific
instruction set processors (ASIPs). ASIPs sind für eine sehr kleine Klasse von Anwendungen optimiert. Alle diese Prozessortypen sind heute als Mikroprozessoren (der Prozessor ist ein Chip) verfügbar, viele auch als processor cores (Prozessorkerne). So kann
z.B. ein RISC core zusammen mit einem DSP core, Speicherblöcken und Interfaces auf
einem Chip integriert werden. Man spricht dann von einem system-on-a-chip (SoC).
Hardware Hardware-Implementierungen von speziellen Funktionen werden vor allem in application-specific integrated circuits (ASICs) ausgeführt. ASICs haben keinen
Instruktionssatz und sind daher nicht programmierbar. Field-programmable Gate Arrays (FPGAs) als wichtigste Vertreter programmierbarer Hardwarebausteine bilden die
2.2. IMPLEMENTIERUNGSARTEN
7
SOFTWARE
general-purpose processors
RISC, CISC
microcontrollers
digital signal processors (DSPs)
application-specific instruction-set processors (ASIPs)
> flexibility
> performance
> power consumption
programmable hardware
FPGAs
application-specific integrated circuits (ASICs)
HARDWARE
Abbildung 2.2: Vergleich der HW/SW Implementierungsarten
Schnittstelle zwischen Software und Hardware. Bestimmte Typen von FPGAs haben
aufgrund ihrer extrem kurzen turn-around Zeiten (Zyklus: Entwurf - Programmierung Test) zu neuen Möglichkeiten in der Emulation und im Test von Systemen geführt.
Integrierte Schaltungen Abb. 2.3 zeigt eine Übersicht über die möglichen Entwurfsstile für integrierte Schaltungen [56]. Man unterscheidet zwischen voll-kundenspezifischem
(custom design, full-custom design) und halb-kundenspezifischem (semicustom design) Entwurf.
Beim custom Design werden alle Schritte im Entwurf einer Schaltung bis hin zum
Layout vom Designer durchgeführt. Dies erlaubt Optimierungen auf allen Ebenen des
Entwurfs und resultiert in Schaltkreisen mit maximaler Performance oder minimaler
Leistungsaufnahme. Dafür sind die Kosten sehr hoch, was bedeutet, dass custom Designs nur bei entsprechend grossen Stückzahlen rentabel sind.
Beim semicustom Entwurfsstil werden nicht alle Schritte in der Entwicklung und
Fertigung für jeden Schaltkreis neu durchlaufen. Man unterscheidet zwei Arten, das cellbased Design, bei dem nur die Entwurfszeit verkürzt wird, und das array-based Design,
bei dem auch die Fertigungszeit verkürzt wird.
Beim cell-based Design wird die Entwurfszeit gesenkt, indem ein neuer Schaltkreis
aus bereits vorhandenen, in Bibliotheken abgelegten Zellen zusammengesetzt wird. Diese Zellen müssen dann noch plaziert und verdrahtet werden. Es gibt hier zwei Unter-
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
8
design styles
semicustom
custom
array-based
cell-based
standard cells
macro cells
MPGAs
FPGAs
Abbildung 2.3: Entwurfsstile für integrierte Schaltungen
gruppen, standard-cell Design und macro-cell Design. Beim standard-cell Design sind die
Library-Zellen Gatter und Flip-Flops, beim macro-cell Design können diese Zellen die
Komplexität von Prozessorkernen erreichen. Typisch für macro-cell Design ist die Verwendung von Makrozell-Generatoren. Diese Generatoren können ausgehend von parametrisierbaren Beschreibungen von Zellen Layouts synthetisieren. Cell-based und custom Design können auch kombiniert werden. Man spricht dann vom structured custom
design. Diese Kombination wird oft beim Entwurf von Mikroprozessoren verwendet.
Performance-kritische Teile (execution units, FP-units) werden als custom Designs, regelmässigere Teile als cell-based Designs entworfen.
Beim array-based Design wird nicht nur die Entwurfszeit, sondern auch die Fertigungszeit verringert, indem man bereits teilweise vorgefertigte Schaltungsstrukturen
verwendet. Hier gibt es wiederum zwei Untergruppen, mask-programmable gate arrays
(MPGAs) und field-programmable gate arrays (FPGAs). Beide Arten basieren auf Schaltungen, bei denen Grundelemente in einer Matrixstruktur auf dem Chip angeordnet sind.
Zwischen den Elementen (entlang der Zeilen und Spalten des arrays) stehen Kanäle
für Verbindungen zur Verfügung. Bei MPGAs sind die Grundelemente Gatter und FlipFlops. Die Spezialisierung eines MPGAs erfolgt durch die Verbindung (Verdrahtung) der
Elemente. Diese Verbindungen werden durch einige wenige Fertigungsprozesse (Kontaktebenen, Metallebenen) beim Hersteller gemacht. Der Name MPGA weist darauf hin,
dass die Programmierung der Schaltung durch die Masken (für die Kontakt- und Metallebenen) erfolgt. FPGAs bestehen aus generischen Logikblöcken und Verbindungsstrukturen, die beide bereits auf dem Chip vorgegeben sind. Die Programmierung des
FPGAs, d.h., das Setzen der Funktion der einzelnen Logikblöcke und das Verbinden von
Leitungen, erfolgt beim Anwender. Da bei FPGAs der Fertigungsprozess unabhängig
von der Applikation ist, können die Fertigungskosten über eine sehr grosse Stückzahl
amortisiert werden. Die Bezeichnung FPGA weist darauf hin, dass die Programmierung
beim Anwender (in the field, im Felde) durchgeführt wird. Tabelle 2.2 zeigt einen Vergleich der verschiedenen Entwurfsstile. Der Parameter density gibt an, wie viele nutzbare
Transistoren pro Flächeneinheit verfügbar sind. Die manufacturing time ist die Zeitspanne von der Bestellung bis zur Auslieferung der Chips. Bei MPGAs und FPGAs wird
2.2. IMPLEMENTIERUNGSARTEN
9
parameter
custom
cell-based
MPGA
FPGA
density
performance
design time
manufacturing time
cost at low volume
cost at high volume
very high
very high
very long
medium
very high
low
high
high
short
medium
high
low
high
high
short
short
high
low
medium-low
medium-low
very short
very short
low
high
Tabelle 2.2: Vergleich der Entwurfsstile
diese Zeit durch Vorfertigung kurz gehalten.
Das Wort ASIC steht für application-specific integrated circuit. ASICs sind daher das
Gegenteil von general-purpose circuits. Ein Schaltkreis, der für eine spezielle Anwendung
entworfen wird, ist ein ASIC – ganz unabhängig davon, mit welchem Entwurfsstil der
Entwurf durchgeführt wird. Es ist oft der Fall, dass ASICs in einem semicustom Entwurfsstil und general-purpose Schaltungen als custom Designs entworfen werden. Deshalb wird semicustom oft fälschlicherweise mit ASIC gleichgesetzt. Das dem nicht so ist,
zeigen Gegenbeispiele: Es gibt - wenn auch wenige - ASICs, die als custom Designs entworfen wurden. Diese Beispiele findet man in Gebieten, wo maximale Performance und
minimale Leistungsaufnahme Priorität haben und die Kosten eine untergeordnete Rolle
spielen. Dies gilt für die Raumfahrt, wo die Kosten für ein Chip Design im Verhältnis zu
den Missionskosten sehr klein sind. Andererseits gibt es zunehmend general-purpose
Prozessoren, deren regelmässige Strukturen im cell-based Entwurfsstil entworfen werden, z.B. der ALPHA AXP Prozessor. Auch FPGAs werden nicht zu den ASICs gezählt.
Aus der Sicht des HW/SW Codesign sind vor allem die typischen ASIC-Entwurfsstile
(cell-based Designs und MPGAs) sowie FPGAs von Interesse.
Kriterien Performance und die Flexibilität sind gegenläufige Entwurfsziele; sie bilden einen sogenannten trade-off. Das bedeutet, dass man nie beide zugleich maximieren
kann. Je spezialisierter eine Lösung für eine bestimmte Anwendung ist, desto höher
wird ihre Performance für diese Anwendung sein. Je flexibler andererseits eine Lösung
ist, desto geringer wird ihre Performance für eine bestimmte Anwendung sein.
Weitere wichtige Parameter sind Kosten, Leistungsaufnahme und time-to-market.
Über diese Parameter kann man schwer allgemeine Aussagen treffen, ohne die benötigte
Funktionalität und die zugrundeliegenden Stückzahlen zu kennen. Tendenziell steigen
die Kosten und die time-to-market mit zunehmendem Spezialisierungsgrad, während
die Leistungsaufnahme sinkt. Das gilt nur unter der Annahme, dass man eine bestimmte, bekannte Menge von Funktionen implementieren will und für Prozessorlösungen auf
bereits exisitierende Prozessoren zurückgreifen kann.
2.2.1
General-Purpose Prozessoren
General-purpose Prozessoren (GP-Prozessoren) sind Hochleistungs-Mikroprozessoren,
die vor allem in PCs und Workstations eingesetzt werden. Auf diesen Computern werden die verschiedensten Anwendungen ausgeführt, von Textverarbeitung, Datenbanken,
10
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
CAD-Werkzeugen, über Multimedia bis hin zu wissenschaftlichen Berechnungen. Daraus ergibt sich, dass GP-Prozessoren sowohl ein hohe Performance als auch eine grosse
Flexibilität aufweisen müssen. Für GP-Prozessoren ist es wichtiger, dass sie für einen
grossen Mix von Anwendungen eine möglichst hohe Performance aufweisen, als für
eine spezielle Klasse von Anwendungen ein optimale.
Die hohen Performance- und Flexibilitätsanforderungen führen dazu, dass GPProzessoren zu einem Grossteil mit hochoptimierten Schaltungsstrukturen implementiert werden und die Entwicklung heute deutlich mehr als 100 Personen auf 2-3 Jahre
beschäftigt. Die daraus resultierenden hohen Kosten machen GP-Prozessoren nur in
sehr grossen Stückzahlen rentabel.
Für die hohe Performance der GP-Prozessoren gibt es zwei Quellen: Die Nutzung
der jeweils aktuellsten Halbleitertechnologie und Fortschritte in der Rechnerarchitektur
(computer architecture). Wichtige Architektureigenschaften von GP-Prozessoren sind die
Nutzung von Parallelität und mehrstufige Speicherhierarchien [63]. Parallelität wird in
zwei Formen genützt:
• (super)pipelining
Moderne GP-Prozessoren verwenden sehr tiefe Instruktionspipelines, um den
Instruktionsdurchsatz zu erhöhen. Techniken zur Vorhersage von Sprungzielen
(branch prediction) erhöhen zusätzlich die Performance.
• superskalar
Moderne GP-Prozessoren verwenden mehrere parallel arbeitende Ausführungseinheiten. Die Ablaufplanung der Instruktionen auf den skalaren Einheiten wird
vom Prozessor dynamisch durchgeführt.
Mehrstufige Speicherhierarchien umfassen Register, eine oder mehrere Ebenen von
caches und den Hauptspeicher. Durch dieses Konzept wird die Performancelücke zwischen den schnellen Prozessoren und den relativ dazu langsamen Hauptspeichern geschlossen.
Für Echtzeitanwendungen sind GP-Prozessoren nur bedingt geeignet, da die Ausführungszeiten schlecht vorhersagbar sind. Die Ausführungszeit eines Programmstücks
auf einem GP-Prozessor hängt von einer Reihe von dynamischen Effekten ab, wie instruction scheduling, branch prediction und caching. Genau jene Architektureigenschaften, die den GP-Prozessoren ihre hohe Performance bringen, führen dazu, dass man
die Ausführungszeiten eine Codestückes nicht vorhersagen kann. Insbesondere kann
ein Programmstück bei mehreren Ausführungen verschiedene Ausführungszeiten haben. Für Echtzeitsysteme mit harten deadlines muss jedoch die Ausführungszeit eines Blockes bekannt bzw. beschränkt sein. Man kann natürlich GP-Prozessoren verwenden, wenn man die worst-case Ausführungszeit bestimmen kann. Diese ist jedoch
schwer abzuschätzen bzw. nur unter bestimmten Annahmen zu berechnen. Eine weitere Schwierigkeit beim Einsatz von GP-Prozessoren in eingebetteten Systemen sind die
relativ komplexen Speicher- und I/O-Interfaces der Prozessoren.
Tabelle 2.3 zeigt eine Auswahl moderner GP-Prozessoren. Die dritte Spalte gibt die
Anzahl parallel arbeitender skalarer Einheiten und die maximale Pipelinetiefe an. Die
Anzahl parallel arbeitender skalarer Einheiten ist i.allg. kleiner als die Anzahl der verfügbaren Skalareinheiten des Prozessors, weil z.B. nicht alle Einheiten gleichzeitig die
2.2. IMPLEMENTIERUNGSARTEN
11
execution Phase ausführen können. Die maximale Pipelinetiefe bezieht sich auf die
Floating-point Einheit, bei den Integer-Einheiten ist die Pipeline i.allg. kürzer.
processor
21164 ALPHA
(Digital)
21264 ALPHA
(Digital)
R10000
(MIPS)
PowerPC750
(IBM/Motorola)
PA-8500
(HP)
UltraSparc III
(SUN)
Pentium III
(Intel)
K6-III
(AMD)
type
skalar units ×
pipeline depth
clock
[MHz]
1st level cache
instr./ data
2nd level cache
64 bit RISC
4 × 10
600
8 KB / 8 KB
96 KB intern
64 bit RISC
4×7
600
64 KB / 64 KB
extern
64 bit RISC
4 × 10
250
32 KB / 32 KB
512 KB-16 MB extern
32 bit RISC
3×6
466
32 KB / 32 KB
256 KB-1 MB extern
64 bit RISC
4 × N.A.
440
0,5 MB / 1 MB
none
64 bit RISC
4×9
400
16 KB / 16 KB
512 KB-16 MB extern
32 bit CISC
3 × 12
500
16 KB / 16 KB
512 KB extern
32 bit CISC
6×7
450
32 KB / 32 KB
256 KB intern
Tabelle 2.3: Moderne GP-Prozessoren (N.A. = not available)
Beispiel 2.1 Der Aufbau des PowerPC750 ist in Abb. 2.4 beschrieben und das Layout in Abb. 2.5
dargestellt.
Der PowerPC besitzt eine RISC-Architektur. Der Prozessor kann mit Taktfrequenzen von 200-466
MHz getaktet werden. Die Pipeline ist maximal 6-stufig und hat die 4 Hauptstufen fetch, decode/dispatch,
execute und complete/write back. Die superskalare Architektur besitzt 6 funktionale Einheiten (Branch
(BPU), 2 Integer-Einheiten (IUs), 1 Gleitkommaeinheit (FPU), 1 Load/Store-Einheit (LSU) und eine
Systemregistereinheit (SRU)), von denen z.B. maximal 4 in der instruction fetch Phase sein können.
Am Layout in Abb. 2.5 erkennt man, dass bei heutigen Prozessoren das Steuerwerk einen beträchtlichen Anteil der Chipfläche belegt. Weiterhin ist für diese Klasse von Prozessoren typisch, dass die
Speicherorganisation hierarchisch ist. So gibt es neben den Registern eine Cache-Hierarchie, die sowohl
Instruktions- als auch Datencache betrifft.
Multimedia-Instruktionssätze In den letzten Jahren haben Multimedia-Anwendungen stark an Bedeutung gewonnen. Beispiele für Multimedia-Anwendungen sind SprachEin/Ausgabe, Audio- und Video-Playback, DVD, Bildverarbeitung, Videokonferenzsysteme, etc. Während bisher diese Anwendungen auf DSPs oder ASICs implementiert
wurden, besteht nun der Wunsch nach multimediafähigen general-purpose Computern
(PCs, Workstations). Multimedia-Anwendungen sind Verfahren der digitalen Signalverarbeitung, deren Eigenschaften sich wie folgt zusammenfassen lassen:
• Datentypen kleiner Bitbreite (8 oder 16 bit)
• grosse Datenmengen
Additional Features
• viel Datenparallelität
• rechenzeitintensive Algorithmen
+
+ x ÷
Reorder Buffer
(6 Entry)
Completion Unit
Integer Unit 2
Integer Unit 1
I
32-Bit
Reservation Station
Reservation Station
2 Instructions
• Time Base Counter/Decrementer
• Clock Multiplier
• JTAG/COP Interface
• Thermal/Power Management
• Performance Monitor
DTLB
SRs
(Original)
DBAT
Array
Data MMU
32-Bit
CR
System Register
Unit
Reservation Station
GPR File
PA
Tags
MPC750 RISC Microprocessor Technical Summary
Abbildung 2.4: Aufbau eines PowerPC750
32-Kbyte
D Cache
64-Bit
32-Bit
64-Bit
Instruction Fetch Queue
17-Bit L2 Address Bus
64-Bit L2 Data Bus
Data Load Queue
L1 Castout Queue
FPR File
ITLB
SRs
(Shadow)
Tags
L2 Castout Queue
L2 Tags
L2CR
L2 Controller
Not in the MPC740
FPSCR
FPSCR
+ x ÷
Floating-Point
Unit
32-Kbyte
I Cache
128-Bit
(4 Instructions)
Reservation Station
L2 Bus Interface
Unit
64-Bit
IBAT
Array
Instruction MMU
Rename Buffers
(6)
60x Bus Interface Unit
Store Queue
(EA Calculation)
+
Load/Store Unit
Reservation Station
(2 Entry)
CTR
LR
32-Bit Address Bus
32-/64-Bit Data Bus
EA
Rename Buffers
(6)
64-Bit
(2 Instructions)
BHT
64 Entry
BTIC
Branch Processing
Unit
Instruction Unit
Dispatch Unit
Instruction Queue
(6 Word)
Fetcher
12
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
Figure 1. MPC750 Microprocessor Block Diagram
3
2.2. IMPLEMENTIERUNGSARTEN
13
Abbildung 2.5: Layout des PowerPC750
• Verzweigungen mit sehr gut vorhersagbaren Sprungzielen
• Echtzeitbedingungen
• mehrere parallele Datenströme (z.B. Video und Audio)
• grosse I/O-Bandbreite
Die Hersteller von GP-Prozessoren haben auf die Bedeutung dieser neuen Anwendungen reagiert und für ihre Prozessoren Multimedia-Instruktionssatzerweiterungen
entwickelt [14]. Diese Erweiterungen basieren auf dem sub-word execution model, d.h., die
Datenpfade der Prozessoren, die 32 bzw. 64 Bit breit sind, werden in mehrere kleinere
Einheiten (sub-words) aufgetrennt, und die neuen Multimedia-Instruktionen führen Berechnungen parallel auf den sub-words durch. Abb. 2.6 zeigt die sub-words eines 64 bit
Datentyps, die Abb. 2.7 und 2.8 einige typische sub-word Instruktionen.
Beispiele für Instruktionssatzerweiterungen sind MMX für x86 (Intel), MAX-2 für
den PA-RISC (HP), VIS für UltraSparc und MDMX für MIPS. Obwohl diese Erweiterungen sehr populär geworden sind (speziell im Marketing), gibt es einige offene Fragen:
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
14
Abbildung 2.6: Sub-words eines 64 bit Datentyps
Subword instruction: ADD R3
R1
R2
R3
R1, R2
a3
a2
a1
a0
+
+
+
+
b3
b2
b1
b0
=
=
=
=
(a3+b3)
(a2+b2)
(a1+b1)
Subword instruction: MPYADD R3
R1
a3
R2
b3
R1, R2
a2
a1
b2
b1
*,+
=
R3
(a2*b2)+(a3*b3)
(a0+b0)
a0
*,+
b0
=
(a0*b0)+(a1*b1)
Abbildung 2.7: Sub-word Instruktionen ADD und MULT/ADD
• Ist das sub-word execution model ausreichend?
Es gibt eine Reihe von alternativen Architekturen, um Parallelität zu nutzen,
z.B. ALU-arrays. Das sub-word execution model ist ein Kompromiss zwischen
den existierenden Datenfpaden und der Nutzung von Parallelität. Will man mehr
Parallelität nutzen, wird man auf andere Konzepte übergehen müssen.
• Welche Programmiersprachen braucht man für Multimedia-Anwendungen?
Programmiersprachen, die Multimedia unterstützen, müssen Eigenschaften aufweisen, die es in gegenwärtigen general-purpose Programmiersprachen nicht gibt.
Beispiele sind die Möglichkeit, Datentypen mit beliebiger Bitbreite definieren zu
2.2. IMPLEMENTIERUNGSARTEN
15
Subword instruction: UNPACK R3
R1
a3
R3
R1
a2
a1
a1
a0
a0
Subword instruction: PERMUTE R3
R1 (pattern 0 1 2 3)
R1
a3
a2
a1
a0
R3
a0
a1
a2
a3
Abbildung 2.8: Sub-word Instruktionen UNPACK und PERMUTE
können oder eine overflow-Semantik, die den Multimedia/DSP-Algorithmen angepasst ist. Werden z.B. arithmetische Operationen auf einem unsigned integer
Datentyp ausgeführt, so sollte das Ergebnis bei einem overflow der grösste darstellbare Wert sein bzw. bei einem underflow der kleinste darstellbare Wert.
• Wie konstruiert man Compiler für Multimedia-Anwendungen?
Gegenwärtige Compiler bieten keine Unterstützung für sub-word Parallelität. Um
einen Performancegewinn zu erzielen, muss man optimierte, in Assembler geschriebene Routinen aufrufen. Ziel ist es, Compiler zu entwickeln, die automatisch
erkennen, wenn sich mehrere Operationen zu einer sub-word Instruktion gruppieren lassen.
2.2.2
Microcontroller
Microcontroller sind Prozessoren, die für den speziellen Anwendungsbereich der Steuerung von Prozessen zugeschnitten sind. Diese Anwendungen führen zu Programmcode
mit folgender Charakteristik:
• Der Code ist kontrollfluss-dominiert. Es gibt viele Verzweigungen, Sprünge, logische Operationen, aber nur wenige arithmetische Operationen.
• Der Datendurchsatz ist relativ gering.
• Die Anwendungen bestehen aus vielen Tasks.
Microcontroller unterstützen diese Anwendungen durch folgende Eigenschaften:
• Der Instruktionssatz enthält viele Instruktionen für Logik-Operationen und Operationen auf einzelnen Bits.
• Die Register sind im RAM realisiert. Ein Kontextwechsel wird durch einfache
Zeigeroperationen bewerkstelligt. Dies erlaubt einen sehr schnellen Kontextwechsel und garantiert eine kurze Interruptlatenz.
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
16
• Periphere Einheiten sind integriert (A/D-Wandler, D/A-Wandler, Timer, Transceiver). Es gibt spezielle Instruktionen für den Zugriff auf die Peripherie (I/O).
Bei Microcontrollern lassen sich zwei Segmente unterscheiden: low-cost und highperformance Microcontroller.
Low-cost Microcontroller Diese klassischen Microcontroller besitzen eine Wortbreite
von 4-8 Bit und sind für steuerungsdominante Systemfunktionen optimiert. Ein typisches Beispiel für dieses Segment ist der Microcontroller 8051 (siehe Bsp. 2.2). Die Performanceanforderungen in diesem Segment sind gering. Der wesentliche Parameter ist
die Codegrösse, die die Chipfläche und damit die Kosten dominiert. Abb. 2.9 zeigt das
Layout eines Controllers mit dem 8051 als core Prozessor. Aus diesem Bild wird ersichtlich, dass die Speicherblöcke einen wesentlichen Anteil an der Gesamtchipfläche haben.
Beispiel 2.2 Der Prozessor 8051 ist einer der in steuerungsdominanten Anwendungen am häufigsten
eingesetzten Microcontroller. Er besitzt eine Wortbreite von 8 Bit. Die weiteren Eigenschaften lassen sich
wie folgt zusammenfassen:
• CISC, 8-Bit Register
• 8 Bänke mit jeweils 8 Registern, realisiert als RAM, Umschaltung der Bänke durch Interrupts oder
Unterprogramme (sog. Registerwindowing)
• Adressierungsarten: direkt, indirekt, immediate, relativ
• Transportbefehle: Memory-Memory, Memory-Register und Register-Register
• I/O-Ports haben einen separaten Adressraum, insb. gibt es Spezialbefehle für Zugriffe auf I/O-Ports,
sogar auf einzelne Bits
• dichte Instruktionscodierung: 1-3 Bytes/Instruktion
• mehrere Power-down Modi
High-performance Microcontroller. Es gibt auch Microcontrollerfamilien, die eine Wortbreite von 16-64 Bit besitzen. Beispiele sind die Prozessorfamilien Motorola MC683xx, Siemens x166 und Intel x196. Anwendungsgebiete für high-performance
Microcontroller sind Systeme, die neben steuerungsdominanten Funktionen zusätzlich
noch folgende Erfordernisse haben:
• hohe Datenraten, z.B. in der Automobiltechnik
• hohe Datenraten und viele Datenmanipulationsoperationen, z.B. bei Anwendungen der Telekommunikation
• hohe Berechnungsanforderungen, z.B. bei Anwendungen der Signalverarbeitung
und Regelungstechnik
Beispiel 2.3 Als Beispiel wird die Familie MC683xx von Motorola betrachtet. Die CPU besitzt eine
Wortbreite von 32-Bit (CPU 32). Die weiteren Eigenschaften lassen sich wie folgt zusammenfassen:
• 68000-Prozessor, erweitert durch die meisten der Eigenschaften des 68030
2.2. IMPLEMENTIERUNGSARTEN
17
Abbildung 2.9: Layout des Microcontrollers SIECO51 (Siemens Automotive). Der core
dieses Controllers ist ein 8051
• CISC-Prozessor: erreicht hohe Codedichte
• Pipelining
• Standardregister (Register nicht im RAM). Damit ist der Kontextwechsel langsamer als bei den
4-8 Bit Mikrocontrollern.
• Unterstützung für Betriebssysteme: virtuelles Speichermodell mit zwei Programmodi: user- und
privileged mode
IMB
inter module bus
serial I/O
time
processing
unit
TPU
IMB control
RAM
CPU32
I/O - channel 0
.
.
.
I/O - channel 15
Abbildung 2.10: Architektur des Motorola MC68332-Prozessors
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
18
Abb. 2.10 zeigt als Beispiel die Architektur des MC68332. Dieser Microcontroller zielt auf Anwendungsbereiche ab, bei denen eine Mischung von berechnungsintensiven Aufgaben und komplexen I/OOperationen vorliegt. Die dargestellte Einheit TPU (time processing unit) kann selbständig mehrere
I/O-Operationen durchführen. Dadurch sind weniger Tasks und Taskwechsel auf der CPU nötig. Die
TPU besitzt 16 Kanäle, die intern aus einem Zähler und einem Komparator (capture & compare) bestehen. Die Zähler können über externe Ereignisevents bzw. in konstanten Zeitabständen getriggert werden
und generieren beim Nulldurchgang ein Ereignisevent an eine zusätzlich existierende mikroprogrammierte Steuerungseinheit, die zyklisch (round-robin) alle Kanäle überwacht und I/O-Operationen durchführt.
Diese können einen oder mehrere Kanäle betreffen. Da die 16 Kanäle zyklisch von einer einzigen Steuerungseinheit bedient werden, ergeben sich hohe Latenzen. Es gibt aber die Möglichkeit, die Kanäle in
Prioritätsklassen einzuteilen.
Die Komponenten kommunizieren über einen Intermodulbus (IMB). Die Schwierigkeiten in der Verwendung der peripheren Coprozessoren (wie z.B. der TPU) sind die hohen Latenzen für I/O-Operationen
und die Codegenerierung. Die TPU z.B. wird durch das Schreiben mehrerer Register konfiguriert; für die
Programmierung von speziellen Funktionen hat die TPU einen eigenen, kleinen Programmspeicher.
Die Familien MC68332 und Siemens x166 sind Mitglieder von Baukastensystemen,
bestehend aus:
• Modulbus inkl.
– Bussystem (Motorola IMB, Siemens X-Bus), nach aussen geführt zur Erweiterung,
– Interruptsystem (Vektor, flexible Priorisierung),
– Kommunikationsmodell.
• Prozessorkern (cores): 16 Bit, 32 Bit, 64 Bit
• Speicherkomponenten: ROM, RAM, EPROM
• periphere Einheiten: TPU, SIO, DMA, etc.
• Coprozessoren, z.B. Fuzzycontrol, Graphik, etc.
• benutzerdefinierte Standardzell/Gatearray-Blöcke
2.2.3
DSPs
DSPs sind Mikroprozessoren, die für den speziellen Anwendungsbereich der digitalen
Signalverarbeitung zugeschnitten sind. Diese Anwendungen führen zu Programmcode
mit folgender Charakteristik:
• viele arithmetische Operationen, vor allem Multiplikationen und Additionen
• regelmässige Operationen auf mehrdimensionalen Feldern
• wenig Verzweigungen, aber mit sehr gut vorhersagbaren Sprungzielen
• hohe Nebenläufigkeit
• sehr grosse Datenmengen
2.2. IMPLEMENTIERUNGSARTEN
19
Wesentliche Merkmale von DSPs sind [46]:
• schnelle MAC-Operation (multiply & accumulate)
Die MAC-Operation ist eine Operatorverkettung, d.h., in einem Befehlszyklus werden 2 Operanden multipliziert und das Resultat in einem Register akkumuliert.
Dafür ist ein Multiplikationswerk in Hardware notwendig. DSPs waren die ersten
Mikroprozessoren, die Hardware-Multiplizierer - auch für Gleitkomma - hatten.
• Speicherarchitektur mit Mehrfachzugriffen pro Befehlszyklus
Beim Ausführen einer MAC-Instruktion benötigt man Zugriff auf eine Instruktion
und zwei Operanden in einem Zyklus, was eine Speicherarchitektur mit Mehrfachzugriff voraussetzt. Eine solche Architektur muss für Instruktionen und Daten
getrennte Busse haben. Man nennt dies Harvard-Architektur. Um auf zwei Operanden zugreifen zu können, muss es auch mehrere Datenbusse geben. DSPs der ersten Generation hatten tatsächlich getrennte externe Busse für Instruktionen und
Daten. Moderne DSPs verwenden nur intern eine Harvard-Architektur, aber dafür
extern oft zwei identische Speicherschnittstellen, über die gleichzeitig auf verschiedene Speicherbausteine zugegriffen werden kann.
• zero overhead loops
In Schleifen mit bekannter Anzahl von Durchläufen gibt es üblicherweise einen
Schleifenzähler, der mit jedem Durchlauf dekrementiert und mit 0 verglichen wird.
Erreicht der Schleifenzähler 0, wird mit dem nächsten Befehl fortgefahren, sonst
wird zum Schleifenanfang zurückgesprungen. DSPs unterstützen solche Schleifen
durch Spezial-Register, die mit der Anfangs- und End-Adresse der Schleife sowie
dem Zähler geladen werden. Bei jedem Schleifendurchlauf wird automatisch und
parallel zur eigentlichen Instruktionsabarbeitung der Zähler dekrementiert und
die Adresse der Instruktion nach dem Durchlaufen der Schleife (Zurückspringen
oder nicht) berechnet. Dadurch sind keine Zyklen für die Schleifensteuerung notwendig (zero overhead).
• spezialisierte Adressierungsarten
DSPs bieten eine Reihe spezieller Adressierungsarten. Die Adressgeneratoren arbeiten parallel zur eigentlichen Instruktionsabarbeitung. Dadurch spart man Prozessorzyklen für die Adressrechnung. Ein Beispiel für solche Adressierungsarten sind die verschiedenen Formen von Autoinkrement/Autodekrement um eine
Adresse bzw. um eine programmierbare Schrittweite. Zwei weitere, wesentliche
DSP Adressierungsarten sind die circular-Adressierung (z.B. für Filter) und die
bitrevers-Adressierung (z.B. für FFT).
Bezüglich der Datentypen und arithmetischen Operationen kann man fixed-point
(Festkomma) und floating-point (Gleitkomma) DSPs unterscheiden. Bei einem Zahlenformat bestimmt die Mantissenbreite die Genauigkeit und die Exponentenbreite die Dynamik der darstellbaren Zahlen. Bei gleicher Hardwarefläche ist eine fixed-point ALU
schneller als eine floating-point ALU und hat auch eine wesentlich grössere Mantissenbreite und damit Genauigkeit. Andererseits ist bei gleicher Geschwindigkeit oder
Genauigkeit eine floating-point ALU wesentlich grösser und damit teurer. Für viele
20
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
Anwendungen der Signalverarbeitung ist Festkomma-Arithmetik ausreichend, obwohl
der Entwurf von z.B. Filtern durch Rundungs- und Skalierungsprobleme umständlicher werden kann. In kostensensitiven Applikationen werden deshalb vorwiegend fixedpoint DSPs eingesetzt.
Für die meisten DSPs sind heute schon C-Compiler verfügbar, die inzwischen auch
relativ effizient sind. Trotzdem lassen sich viele der speziellen Architektureigenschaften nur nutzen, wenn man in Assembler programmiert. Üblicherweise werden DSPProgramme in C geschrieben, und danach wird eine Laufzeitanalyse (Profiling) durchgeführt. Die zeitkritischen Funktionen werden dann durch handoptimierte Assemblerprogramme ersetzt. Für viele Anwendungen von DSPs, z.B. Bildverarbeitung, werden
umfangreiche Funktionsbibliotheken von handoptimierten Assemblerroutinen angeboten, die in C-Programme eingebunden werden können. Nachdem sich viele DSP Anwendungen mit Datenflussmodellen beschreiben lassen, gibt es auch eine Reihe von
Codegeneratoren, die ausgehend von einer Datenflussbeschreibung optimierten Code
erzeugen.
Das Gebiet der digitalen Signalverarbeitung hat in den letzten Jahren sehr stark an
Bedeutung gewonnen. Damit zusammenhängend wurden viele neue DSPs entwickelt,
teils mit sehr innovativen und interessanten Architekturen [25]. Im folgenden werden
drei Trends der letzten Jahrzehnts behandelt: Multi-DSP Systeme, DSPs mit VLIWArchitektur und Desktop-DSP.
Multi-DSP Systeme Für high-performance Anwendungen (hohe Abtastraten, sehr
grosse Datenmengen, komplexe Algorithmen) passiert es schnell, dass ein einzelner
DSP zuwenig Rechenleistung bietet. Falls sich die Anwendungen parallelisieren lassen, was bei Signalverarbeitungsalgorithmen meistens der Fall ist, kann man oft durch
Verwendung mehrerer Prozessoren (Multi-DSP System) die Performanceanforderungen
erfüllen. Anfang der 90er Jahre wurden einige DSPs für diesen high-performance Markt
entwickelt, die Unterstützung für den Aufbau von Multi-DSP Systemen bieten. Beispiele
sind der TMS320C40 (Texas Instruments) oder der ADSP21060 (Analog Devices). Diese Prozessoren haben bidirektionale Kommunikationsschnittstellen, über die sich die
Prozessoren direkt verbinden lassen. Es sind also keine externen Buffer, Protokollumsetzer, Synchronisationseinheiten, etc. notwendig. Dadurch kann ein Multi-DSP System
mit verteiltem Speicher sehr einfach aufgebaut und erweitert werden.
Beispiel 2.4 Als Beispiel wird der Signalprozessor ADSP21060 (SHARC) von Analog Devices betrachtet. Es handelt sich um eine Load/Store-Architektur mit einer 32-Bit Fliesspunkt-ALU (siehe Abb. 2.11).
Das Operationswerk (links) besitzt 3 parallele Einheiten: eine ALU, eine Schiebeeinheit (Multi-Bit)
und eine Multipliziereinheit. Die entsprechenden Befehle des Instruktionssatzes sind Ein-Zyklen-Befehle.
Die Prozessorfrequenz beträgt 40 MHz. Weiterhin unterstützt der Instruktionssatz spezielle Instruktionen, z.B. zur Betragsbildung. An Datenregistern liegen zwei Bänke mit jeweils 16 40-Bit-Registern (interne Wortbreite) vor. Die zweite Bank kann z.B. bei einem Kontextwechsel zwischen zwei Tasks eingesetzt
werden.
Spezielle Befehle zur Schleifenabarbeitung werden im Programmsequenzer unterstützt, insb. von geschachtelten Schleifen. Der kleine Instruktionscache mit 32 Einträgen unterstützt die Abarbeitung kleiner,
häufig iterierter Schleifen. Es existieren zwei unabhängige Adressgenerierungseinheiten. Das Speichersystem besteht aus einem grossen Zwei-Port-Speicher, organisiert in zwei Bänken, was typisch für DSP-
2.2. IMPLEMENTIERUNGSARTEN
21
Abbildung 2.11: Signalprozessor ADSP21060 (SHARC)
Prozessoren ist. Der erste Port wird über einen Crossbar-Switch für Daten- und Adressbus gemultiplext.
Der zweite Port ist exklusiv für den dargestellten I/O-Prozessor reserviert.
Desweiteren besitzt der SHARC-Prozessor 6 4-Bit-Link Ports, die Datenübertragungen mit einer
Datenrate von 40 MBytes/s pro Kanal erlauben. Alle Instruktionen sind 48-Bit Wörter, wobei in einer
Instruktion maximal drei parallele Operationen initiiert werden können. Schliesslich besitzt der Prozessor
auch eine komplexe DMA-Einheit.
Ein DSP, der mehrere Prozessoren auf einem Chip integriert, ist der TMS320C80
(Texas Instruments). Auf diesem DSP sind vier 32-bit Festkomma DSPs und ein 32-bit
RISC Prozessor integriert. Der TMS320C80 ist spezialisiert auf Anwendungen in der
Video- und Bild-Verarbeitung.
Beispiel 2.5 Beim Prozessor TMS320C80 von Texas Instruments handelt es sich um eine Mehrprozessorarchitektur mit 4 32-Bit Festkomma-Prozessoren und einem 32-Bit Gleitkommaprozessor (Master
DSP) auf einem Chip, siehe Abb. 2.12.
Die Speicherarchitektur dieses DSP ist äusserst komplex (lokale Register in den DSPs, 4x2 KBytes
RAM-Bänke (512 Worte/Bank), 2 KBytes Instruktionscache pro Prozessor (256 Worte)). Damit existiert
gegenüber dem SHARC eine höhere Nebenläufigkeit im Speicherzugriff, allerdings ist der Speicher viel
kleiner. Zur Verbindung der Prozessoren existiert ein nahezu vollständiger Crossbar-Switch. Dieser reduziert Kommunikationsbeschränkungen und Zugriffskonflikte auf die Shared Memories. Dadurch soll eine
hohe interne Speicher- und Kommunikationsbandbreite erreicht werden.
Als Controller dient ein 32-Bit RISC Prozessor. Die 4 Festkommaprozessoren (Advanced DSP) besitzen folgende Eigenschaften (siehe Abb. 2.13):
• Multiplizierer, Schiebeeinheit, ALU mit 3 Eingängen, die in kleinere 8-Bit Einheiten aufgespaltet
werden kann
• Unterstützung spezieller Operationen auf Bitebene (u.a. Pixelexpander)
• 2 Adress-ALUs für Indexberechnungen
22
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
Abbildung 2.12: Aufbau des TMS320C80 (MVP) von Texas Instruments
Abbildung 2.13: Datenpfad eines DSPs im TMS320C80
• Instruktionswortbreite 64 Bit (horizontal) für parallele Datenoperation, Datentransfer und einen
globalen Datentransfer.
DSPs mit VLIW-Architektur Die neueste Entwicklung bei DSPs ist die Prozessorfamilie TMS320C6x (Texas Instruments), die mit ihrer VLIW (very long instruction word)
Architektur eine radikale Abkehr von bisherigen DSP-Architekturen darstellt. Ein VLIW
Prozessor besitzt mehrere Funktionseinheiten, die unabhängig voneinander durch verschiedene Bits im Instruktionswort gesteuert werden. Dadurch wird das Instruktions-
2.2. IMPLEMENTIERUNGSARTEN
23
wort sehr lang. Bislang konnten sich VLIW-Architekturen kommerziell aus folgenden
Gründen nicht durchsetzen: i) Die Anwendungen müssen genügend Parallelität besitzen, damit sich die parallel arbeitenden Funktionseinheiten rentieren. ii) Die Codedichte
ist sehr klein, da bei vielen Instruktionen nicht alle Einheiten genutzt werden können
und NOP (no operation) Instruktionen eingefügt werden müssen. iii) Der Compiler
hat die schwierige Aufgabe, Parallelität zu erkennen und den Instruktionen statisch
entsprechende Funktionseinheiten zuzuweisen. Im Gegensatz zu superskalaren Prozessoren erkennen VLIW-Prozessoren nicht selbst die Datenabhängigkeiten und Konflikte
zwischen den Instruktionen. Effiziente Compiler für VLIW-Architekturen sind deshalb
schwer zu entwickeln.
Bei den Anwendungen der Signalverarbeitung liegt meist genügend Parallelität vor,
die Probleme der geringen Codedichte und eines effizienten Compilers bestehen nach
wie vor. Eine Innovation des TMS320C6x ist das Instruktionsformat (siehe Abb. 2.15).
Ein Instruktionswort besteht aus 256 Bit (8 Instruktionen a 32 Bit). Die Instruktionen
für die einzelnen Funktionseinheiten haben aber keine feste Position innerhalb des 256
Bit Wortes, sondern sind verschiebbar. Für welche Funktionseinheit die Instruktion bestimmt ist, ergibt sich aus der Operation und einem Feld in der Instruktion. Instruktionen werden immer als 256 Bit Worte gelesen (fetch paket). Die einzelnen Instruktionen
müssen nicht alle in einem Zyklus abgearbeitet werden, sondern jede Instruktion gibt
durch ein Bit an, ob sie parallel zur vorhergehenden Instruktion im 256 Bit Instruktionswort ausgeführt werden kann. Instruktionen, die parallel ausgeführt werden können,
werden als execution paket bezeichnet. Durch dieses Verfahren soll die Codedichte erhöht werden.
Beispiel 2.6 Abb. 2.14 zeigt die Architektur des VLIW-DSPs TMS320C6x von Texas Instruments. Der
Prozessor besitzt 8 Funktionseinheiten, die jeweils von einer 32-Bit Instruktion gesteuert werden. Die 8
Einheiten sind in zwei Blöcke geteilt, die jeweils identische Einheiten beinhalten. Der Prozessor hat ein
Load/Store Architektur mit 2 Registerbänken. Jede Bank besitzt 16 general-purpose Register, d.h., jedes
Register kann für jede Operation verwendet werden. Der TMS320C6x hat keines der typischen DSPMerkmale, es gibt keine MAC-Instruktion, keine zero overhead loops und keine spezialisierten Adressierungsarten. Die Architektur setzt ähnlich wie RISC-Architekturen auf einfache Instruktionen und eine
sehr tiefe pipeline (11-stufig), die zu hohen Taktfrequenzen führen. Der TMS320C6x kann mit 200 MHz
betrieben werden.
Desktop-DSP Wie schon im Abschnitt 2.2.1 erwähnt, werden Anwendungen der Signalverarbeitung zunehmend auf GP-Prozessoren gerechnet. Beim Desktop-Computing
(PCs, Workstations) hat man ohnehin einen GP-Prozessor und möchte gleich mit diesem Prozessor DSP-orientierte Applikationen rechnen, anstatt Zusatzhardware mit extra
Prozessoren zu verwenden. Das reduziert die Kosten und die Leistungsaufnahme. Moderne GP-Prozessoren eignen sich recht gut für Signalverarbeitung aufgrund folgender
Eigenschaften:
• hohe Taktfrequenzen (2-5 mal höher als bei typischen DSPs)
• Integer-Multiplikation in einem Zyklus
24
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
Abbildung 2.14: Architektur des TMS320C6x core
Abbildung 2.15: Instruktionswort des TMS320C6x
• Adressgenerierung wird zum Teil parallel durchgeführt, indem der Prozessor Instruktionen dynamisch auf mehrere skalare Einheiten verteilt
• Schleifenoverhead wird reduziert durch branch prediction und Instruktionsabarbeitung auf mehreren skalare Einheiten
Viele GP-Prozessoren haben darüberhinaus spezielle Instruktionen, die Parallelität auf der Ebene von sub-words nutzen (z.B. MMX), die für die Signalverarbeitung sehr vorteilhaft sind. Abb. 2.16 zeigt einen Performancevergleich verschiedener
DSPs und GP-Prozessoren. Der Nachteil von GP-Prozessoren für DSP Anwendungen liegt in der schlechten Vorhersagbarkeit von Laufzeiten, dem Fehlen von guten
Entwicklungstools (Compiler) für DSP-spezifische Anwendungen und dem schlechten
Preis/Leistungs Verhältnis (siehe Abb. 2.17). Für die Zukunft wird erwartet, dass GPProzessoren mehr und mehr Desktop-DSP Anwendungen erobern und auch mehr DSPArchitektureigenschaften bekommen werden. Andererseits entwickeln sich DSPs auch
weiter und übernehmen zunehmend Eigenschaften von RISC bzw. VLIW-Architekturen.
2.2. IMPLEMENTIERUNGSARTEN
25
Abbildung 2.16: Vergleich der Performance von DSPs und GP-Prozessoren. Aufgetragen
ist die Ausführungszeit für eine 256-Punkt komplexe Radix-2 FFT (Quelle: [7], * = Code und Daten werden bereits vor der Programmausführung in den Cache geladen, †=
Performance geschätzt)
2.2.4
ASIPs
ASIPs (application-specific instruction-set processors) sind Prozessoren, die auf eine bestimmte Klasse von Anwendungen zugeschnitten sind. ASIPs sind noch stärker spezialisiert als Microcontroller und DSPs. Man kann ASIPs nach folgenden Eigenschaften
klassifizieren:
• Datentypen: Festkomma- oder Gleitkomma-Arithmetik, Bitbreiten.
• Codetyp: Mikrocode oder Makrocode. Beim Mikrocode, zutreffend für die meisten
Typen von ASIPs, benötigen alle Instruktionen einen Maschinenzyklus. Beim Makrocode kann eine Instruktion mehrere Zyklen benötigen (z.B. bei Einheiten mit
Instruktionspipelining). In einer Instruktion stecken dann alle Informationen zur
Ansteuerung des Datenpfades über mehrere Maschinenzyklen hinweg.
• Speicherorganisation: Man unterscheidet hier zwischen Load/Store- und Mem/RegArchitekturen. ASIPs sind üblicherweise Load/Store-Architekturen. Für diese
Klasse ist charakteristisch, dass alle Maschinenoperationen mit Registeroperanden
arbeiten. Der Transfer von Daten aus dem und zum Speicher erfolgt ausschliesslich
mit Load-Befehlen bzw. Store-Befehlen. Bei Mem/Reg-Architekturen können Maschinenbefehle auch auf Speicheroperanden arbeiten. ASIPs besitzen häufig keinen Cache; die Speicher (RAM, ROM, Register) werden in den meisten Fällen auf
26
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
Abbildung 2.17: Vergleich der Preis/Leistungsverhältnisses von DSPs und GPProzessoren. Aufgetragen ist das Produkt aus Chipkosten und Ausführungszeit für ein
FIR Filter (Quelle: [7], * = Code und Daten werden bereits vor der Programmausführung
in den Cache geladen, †= Performance geschätzt)
dem Chip realisiert. Zur Speicherorganisation gehört auch die Registerstruktur, die
entweder heterogen oder homogen sein kann. Bei homogenen Registersätzen kann
jedes Register universell, d.h. für alle Operationen, eingesetzt werden. Abb. 2.18
zeigt eine Architektur mit heterogenem Registersatz. Hier kann z.B. der linke Operand des Multiplizierers nur aus den Registern R1, MR oder AR kommen.
• Instruktionsformat: Codiert oder orthogonal. Bei einem codierten Instruktionsformat werden die Felder einer Instruktion abhängig vom Operationscode interpretiert. Bei einem orthogonalen Instruktionsformat, wie es z.B. VLIW-Maschinen (very long instruction word) aufweisen, können Teile des Instruktionswortes unabhängig voneinander gesetzt werden. Dadurch ist es möglich, mit einem Instruktionswort mehrere unabhängige Funktionseinheiten anzusteuern.
• Besonderheiten: ASIPs besitzen je nach Applikationsgebiet eine Reihe weiterer Besonderheiten. Das können beispielsweise spezielle arithmetische Einheiten, spezielle Datentypen, besondere Adressierungsarten, Unterstützung von Schleifenkonstrukten, etc., sein.
Beispiel 2.7 Abbildung 2.18 zeigt eine ASIP-Architektur, die aus einem Operationswerk (rechter
Block), einem Steuerwerk (linker Block) und Peripherieeinheiten besteht. Der Prozessor besitzt eine
Harvard-Architektur, die gegenüber der klassischen von-Neumann-Architektur die Eigenschaft aufweist, dass auf Instruktionsspeicher und Datenspeicher getrennt zugegriffen werden kann. Dadurch kann
in einem Zyklus eine Instruktion und Daten gelesen werden. Eine besondere Eigenschaft des Datenpfads sind einige spezielle Verbindungsmöglichkeiten von funktionalen Einheiten und Registern sowie
eine gekoppelte Multiplizier-Addiereinheit. Der ASIP besitzt eine Load/Store-Architektur mit FestkommaArithmetik und einem heterogenen Registersatz (Register A1, A2, AR, R1, R2, MR). Als periphere Kom-
2.2. IMPLEMENTIERUNGSARTEN
27
ponenten sind A/D-Wandler, D/A-Wandler, Timer, serielle/parallele Schnittstellen und DMA-Controller
vorgesehen.
D/A- A/D
Timer
DMA
Peripherieeinheiten
SER/PAR
Operationswerk
Steuerwerk
Registerstruktur
A1
A2
R1
R2
Verbindungsstruktur
Instruktionssatz
MUL
Datenspeicher
Decoder
Sequencer
Programmspeicher
Speicherstruktur
F
ALU
ADD
SH
SAT
funktionale
Einheiten
AR
MR
Versorgungsspannung VDD
Taktperiode T
Abbildung 2.18: Beispiel einer ASIP-Architektur
Aus Kostengründen ist ein ASIP oft nur ein „abgespeckter“ Prozessor und damit
günstiger als ein GP-Prozessor, aber aufgrund seiner (wenn auch beschränkten) Programmierbarkeit immer noch flexibler als dedizierte Hardware. Die Gründe für die Entwicklung von ASIPs sind:
• Flexibilität vs. Performance: GP-Prozessoren sind zu langsam, Mikrocontroller bzw.
DSPs sind für das Anwendungsgebiet nicht geeignet oder sie sind ebenfalls zu
langsam.
• Kosten: Durch Anpassung des Prozessors an die Anwendungen können im
Vergleich zu allgemeineren Prozessoren Pins eingespart, ein kleineres Gehäuse
(package) verwendet und eine einfachere Interface- und Speicherarchitektur implementiert werden. Diese Punkte führen alle zu einer Kostenreduktion.
• Leistungsaufnahme: Durch Weglassen unnötiger Blöcke wird auch die Leistungsaufnahme reduziert. Das ist besonders wichtig bei mobilen Systemen, Systemen
mit möglichst langer Missionsdauer oder Anwendungen, bei denen thermische
Probleme zu erwarten sind.
Die Unterschiede zu GP-Prozessoren bestehen in folgenden Merkmalen:
• Instruktionssatz: ASIPs bieten Instruktionen, die Operatorverkettungen darstellen,
z.B. Multipliziere & Akkumuliere, Vektoroperationen, etc. Dadurch wird der Code kürzer, und es gibt weniger Instruktionsfetchzyklen, wodurch die Performance
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
28
erhöht wird. Ähnlich wie DSPs nutzen ASIPs Parallelität, indem Operationen und
Adressberechnungen parallel durchgeführt werden. Dazu sind spezielle Adressgeneratoren notwendig.
• Funktionseinheiten: Je nach Anwendungsgebiet besitzen ASIPs spezialisierte Funktionseinheiten, z.B. für Operationen auf Zeichenketten, Pixeloperationen, Verkettung von arithmetischen Operationen. Durch Adaption der Datentyp-Wortlängen
auf die Erfordernisse der Anwendung werden Kosten, Leistungsverbrauch und
Programmausführungszeit reduziert.
• Speicherstruktur: Ebenfalls zugeschnitten auf den Anwendungsbereich sind die Anzahl und Grösse der Speicherbänke, die Anzahl und Wortbreite der Speicherports,
die Zugriffsarten (read, write, burst modes, read-modify-write, etc.), die logische
Funktion der Speicher (RAM, FIFO, QUEUE, etc.).
• Verbindungsstruktur: Ein optimierter Datenpfad mit reduzierter Verbindungsstruktur und Spezialregistern kann die Komplexität des Steuerwerkes und die Zykluszeit von Instruktionen reduzieren.
ASIPs sind also wesentlich stärker spezialisiert als andere Prozessortypen und unterscheiden sich von ASIP zu ASIP sehr stark. Das Charakterisieren von für einen Anwendungsbereich typischen Operationen und Instruktionssätzen und das Entwickeln einer
möglichst guten Prozessorarchitektur gemeinsam mit einem optimierenden Compiler
ist eines der zentralen Probleme des Gebietes Hardware/Software Codesign.
2.2.5
FPGAs
FPGAs (Field-Programmable Gate Arrays) bestehen aus einer regelmässigen Anordnung von Logikblöcken und dazwischenliegenden horizontalen und vertikalen Verbindungsstrukturen [9]. An den Rändern des arrays befinden sich spezielle I/O-Blöcke
(siehe Abb. 2.19).
Die Logikblöcke von FPGAs können sehr verschieden sein. Sie enthalten jedoch immer kombinatorische Logik und Flip-Flops. Für die Implementierung von kombinatorischer Logik stehen Look-Up Tables (LUTs) und Multiplexer zur Verfügung. Abb. 2.20
zeigt den Aufbau eines Logikblocks (CLB) der Xilinx XC4000 Serie. Dieser CLB beinhaltet drei LUTs, zwei davon mit je 4 Eingängen und eine mit 3 Eingängen sowie zwei
Flip-Flops. Eine 4-input LUT kann jede Boolesche Funktion von 4 Eingängen realisieren.
Alle drei LUTs gemeinsam können jede Boolesche Funktion von 5 Eingängen realisieren,
oder aber auch bestimmte Funktionen von bis zu 9 Eingängen.
Abb. 2.21 zeigt einen I/O-Block der Xilinx XC4000 Serie. Die I/O-Blöcke können in
einer Reihe von Parametern konfiguriert werden, z.B. Richtung (in/out/bidirectional),
Modus, Signalanstiegszeit, etc.
Abb. 2.22 zeigt die Verbindungstruktur der Xilinx XC4000 Serie. Heutige FPGAs verwenden einen beträchtlichen Teil ihrer Fläche (> 95% ) für diese Verbindungsstruktur.
2.2. IMPLEMENTIERUNGSARTEN
29
Abbildung 2.19: Struktur eines FPGAs
Abbildung 2.20: Aufbau eines Logikblocks (CLB) der Xilinx XC4000 Serie
FPGAs werden durch das Setzen von Schaltern programmiert. Es gibt unterschiedliche Schalter-Technologien, die in Tabelle 2.4 aufgeführt sind. Die meisten FPGAFamilien verwenden antifuse oder SRAM-Switches. Bei anti-fuse wird durch das Programmieren (Anlegen einer höheren Spannung, “Brennen”) zwischen zwei Punkten eine
Verbindung hergestellt (im Gegensatz zu einer fuse, bei der die Verbindung durch das
“Brennen” getrennt wird). Diese Verbindung ist permanent, d.h., das FPGA kann nur
einmal programmiert werden. Dafür bleibt die Programmierung auch bei Abschalten
30
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
Abbildung 2.21: Aufbau eines I/O-Blocks der Xilinx XC4000 Serie
Abbildung 2.22: Verbindungsstruktur der Xilinx XC4000 Serie
der Spannungsversorgung erhalten. Bei SRAM-basierten Schaltern wird die Verbindung
durch das Schreiben einer SRAM-Speicherzelle bestimmt. Diese Programmierung geht
bei einem Abschalten der Spannungsversorgung verloren, dafür ist das FPGA beliebig
oft re-programmierbar.
Die Entwicklung von FPGA-Designs ist sehr ähnlich der ASIC-Entwicklung und da-
2.2. IMPLEMENTIERUNGSARTEN
31
switch type
reprogrammable
volatile
antifuse
EPROM
EEPROM
SRAM
no
yes (out of circuit)
yes (in circuit)
yes (in circuit)
no
no
no
yes
Tabelle 2.4: Switch-Technologien für field-programmable devices
mit dem Hardware-Entwurf. Mit herkömmlichen Entwurfswerkzeugen (Synthesewerkzeuge, Schematic Entry) werden Netzlisten von generischen Elementen (Gates, FlipFlops) erzeugt. Diese Netzlisten werden von den Tools der FPGA-Hersteller in Konfigurationsdaten für ein FPGA umgewandelt. Diese Konfigurationsdaten werden zu dem
FPGA übertragen (das FPGA wird konfiguriert), und danach steht die entwickelte Schaltung auf dem FPGA zur Verfügung. Die herstellerspezifischen FPGA-Tools müssen folgende Teilprobleme erledigen: Als erstes werden die generischen Elemente der Netzliste auf die tatsächlich vorhandenen Elemente im Ziel-FPGA transformiert (technology
mapping). Danach werden die Elemente in dem array des FPGAs plaziert (placement)
und die Verbindungen berechnet (routing). Placement und routing sind eng verwobene
Probleme (schlechtes placement kann das routing beträchtlich erschweren) und werden
iterativ durchgeführt. Diese zwei Phasen lösen ein schwieriges Optimierungsproblem,
was die Ursache für die relativ langen Laufzeiten der FPGA-Tools ist (abhängig von der
Grösse des FPGAs und des Designs im Minuten- bis Stundenbereich).
FPGAs sind das am schnellsten wachsende Segment der Halbleiterbranche. Die Zunahme der Dichte von FPGAs (gates per chip) in den letzten Jahren ist enorm. Ein
aktuelles Beispiel ist die Xilinx Virtex Familie, die in einem 0.22 µ CMOS-Prozess mit
5 Metallebenen gefertigt wird. Der grösste angekündigte FPGA-Baustein dieser Familie
beinhaltet 27648 CLBs. Mit diesem FPGA lassen sich Entwürfe in der Grössenordnung
von bis zu 1M Gattern implementieren. Die Anwendungsgebiete von FPGAs sind:
• Glue Logic
FPGAs wurden für den Zweck entwickelt, digitale Schaltungen, die bei einem System für Interfaces, Ansteuerungen, Codierungen, etc., nötig sind, zu implementieren. In diesem Segment sind sie Erweiterungen von PLAs (programmable logic
arrays) und CPLDs (complex programmable logic devices). Glue logic ist nach wie
vor der Haupteinsatzbereich für FPGAs.
• Rapid Prototyping, Emulation
Mit der Verfügbarkeit von FPGAs mit sehr vielen Gattern wurden Emulationssysteme möglich. Im Entwurf von digitalen Schaltungen ist es sehr wichtig, die
funktionale Korrektheit der Schaltungen zu überprüfen. Dazu können Simulatoren verwendet werden, die aber für umfassende Simulationsläufe zu langsam sind.
Emulation bedeutet, dass die entworfene Schaltung auf einer anderen Hardware
(FPGAs) ausgeführt wird. Dies kann wesentlich schneller geschehen als eine Simulation. Es gibt kommerzielle Emulationssysteme (Quickturn Design Systems,
IKOS Systems), die aus Dutzenden von FPGAs bestehen. Bei der Entwicklung von
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
32
GP-Prozessoren werden heute solche Emulationssysteme zur Verifikation eingesetzt.
• Eingebettete Systeme
FPGAs werden auch in eingebetteten Systemen eingesetzt. FPGA-Lösungen sind
i.allg. schneller als programmierbare Lösungen und flexibler als ASICs. Werden
späte Änderungen oder Änderungen im Betrieb erwartet, sind FPGAs die einzig
mögliche Lösung, falls Prozessoren eine zu geringe Performance aufweisen. Für
kleine Stückzahlen kann eine FPGA-Implementierung auch kostengünstiger sein
als ASICs.
• Configurable Computing
Dies ist ein relativ neues Forschungsgebiet, in dem rekonfigurierbare Bausteine
(wie FPGAs) für allgemeine Berechnungen verwendet werden. Die Idee ist, die
Performance von ASICs mit der Flexibilität von Software zu kombinieren. Ein
typischer Ansatz ist das Erkennen von laufzeitintensiven Programmteilen (meist
innere Schleifen) und das Auslagern dieser Funktionalität in programmierbare
Hardware. Die Bandbreite rekonfigurierbarer Bausteine geht von herkömmlichen
FPGAs über integrierte Kombinationen von Prozessorkernen und FPGA-Blöcken
bis hin zu gänzlich neuen Ansätzen für die Architektur von Prozessoren.
2.3
Systemaufbau
Eingebettete Systeme können als system-on-a-chip (SoC) oder als board-level system, bestehend aus einem board (Einplatinensystem) oder aus mehreren boards (multi-board
system) implementiert werden. Tabelle 2.5 zeigt Vor- und Nachteile dieser Varianten.
parameter
weight,
size,
power consumption
reliability
cost - high volume
system-on-a-chip
board system
multi-board system
low
high
very high
very high
low
medium
high
low
high
Tabelle 2.5: Vergleich der Möglichkeiten für den Systemaufbau
2.3.1
Systems-on-a-Chip
Wenn die geplante Stückzahl eines eingebetteten Systems ausreichend gross ist, stellen
SoC eine sehr attraktive Realisierungsform dar. SoC besitzen die Vorteile eines geringen Gewichts, kleinen Volumens und geringer Leistungsaufnahme. Das ist vor allem im
Bereich mobiler Geräte (Laptops, Mobiltelefone, etc.) äusserst wichtig. Auch die Zuverlässigkeit (reliability) eines SoC ist sehr hoch. Dadurch, dass es keine schnell getakteten
externen Busse (Speicherbusse) gibt, sind wenig Emissionen zu erwarten. Weiters bieten
SoC die Möglichkeit, analoge Systemteile (Leistungstreiber, Sensoren) zu integrieren.
2.3. SYSTEMAUFBAU
33
Durch Fortschritte in der Mikroelektronik ist es heute möglich, mehrere Millionen
Transistoren auf einem Chip zu integrieren. Um diese komplexen Entwürfe durchführen
zu können, muss sich die Art, wie ICs entworfen werden, ändern. Der neue Entwurfsstil
besteht darin, bereits vorhandene Blöcke auf Systemebene (processor cores, memories,
etc.) mit selbst entworfenen Blöcken zu einem neuen Gesamtsystem zu kombinieren.
Die Änderungen der Entwurfsmethodik werden in Abb. 2.23 gezeigt.
Design efficiency
gates per person-day
1000
System on a chip
System-design tools
Block design
Library-based chips
100
Cell array
Chip-design tools
Transistor
10
Handcrafted custom chips
Transistor models
1970
1980
1990
2000
Abbildung 2.23: Änderung im Entwurf von ICs [33]
In der block-based Entwurfsmethode wird die hohe Produktivität durch Wiederverwendung bereits entworfener Blöcke erreicht. Diese Blöcke stellen geistiges Eigentum,
intellectual property (IP), des Designers dar. Bei der Integration von IP unterscheidet man
verschiedene Typen von Blöcken. Soft blocks sind Schaltungen, die in einer Hardwarebeschreibungssprache (HDL) auf RTL-Ebene beschrieben sind, und eventuell Netzlisten
von generischen Bibliothekselementen. Das Kennzeichen von soft blocks ist ihre volle
Synthetisierbarkeit. Firm blocks sind zusätzlich optimiert, z.B. durch Festlegung der Umrisse der Schaltungsteile und deren relative Positionierung (floorplanning). Firm blocks
besitzen jedoch noch kein Layout. Hard blocks hingegen sind bis zum Layout implementiert und dadurch auf eine spezielle Technologie festgelegt. Tabelle 2.6 vergleicht diese
Blocktypen. Soft blocks sind am flexibelsten, unabhängig von einer speziellen Technologie, portabel, aber dafür sind die Flächenbedürfnisse und die Performance nicht
vorhersagbar. Bei hard blocks verhält es sich genau umgekehrt. Dadurch, dass der Anbieter von IP in Form von hard blocks keine synthetisierbare Schaltungsbeschreibung
zur Verfügung stellen muss, besteht hier der beste Schutz von IP.
Damit sich Blöcke verschiedener Hersteller und verschiedener Typen (soft, firm,
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
34
hard) auch integrieren lassen, muss es Standards geben, die festlegen, wie diese Blöcke
zu entwerfen sind und wie sie kombiniert werden können. Zum Zweck der Festlegung
von Standards oder zumindest Empfehlungen wurde ein Forum, die VSI Alliance (Virtual Socket Interface Alliance) [2], bestehend aus IP Anbietern, Herstellern von Design
Tools, Systemhäusern und Halbleiterherstellern, gegründet. Die Integration von IP und
der Entwurf von komplexen SoC sind gegenwärtig stark diskutierte Themen. Neben
wirtschaftlichen und rechtlichen Problemstellungen sind auch eine Reihe technischer
Fragen zu lösen: Wie kann man evaluieren, ob ein IP block für das geplante System
geeignet ist, ohne ihn gleich kaufen zu müssen? Wie kann man einen SoC bestehend
aus mehreren unterschiedlichen Blöcken (soft, firm, hard) simulieren oder das gemischte Design verifizieren? Besonders die Simulation von mehreren Blöcken, manche davon
Hardware, manche programmierbar, ist ein wichtiges HW/SW Codesign Thema (Cosimulation).
block type
flexibility vs. predictability
portability
IP protection
soft
firm
hard
very flexible, unpredictable
flexible, unpredictable
inflexible, very predictable
unlimited
library mapping
process mapping
none
none
good
Tabelle 2.6: Vergleich der IP Typen[33]
Beispiel 2.8 Abbildung 2.24 zeigt das Layout eines konfigurierbaren Ein-Chip-Systems von Texas Instruments (TI-cDSP). Neben einem Prozessorkern, einem digitalen Signalprozessor, enthält der Chip konfigurierbare RAM- und ROM-Bereiche sowie ein Gatearray (links) zum Entwurf anwendungsspezifischer
Hardware. Häufig kommen zu diesen Komponenten auch periphere Komponenten dazu, z.B. eine A/DWandler-Zelle oder Schnittstellenzellen (ser./par.).
Beispiel 2.9 Neben der Entscheidung Hardware oder Software betrifft ein weiterer Heterogenitätsaspekt
eingebetteter Systeme die Frage analog oder digital. Abbildung 2.25 zeigt ein Ein-Chip-System, das einen
SMARTPOWER Mikrocontroller mit einer 3 A DC-Motorbrücke koppelt. Die vier in der rechten Hälfte
dargestellten regelmässigen Zellen des Layouts stellen DMOS-Leistungstransistoren dar. Neben dem Mikrocontroller zur Steuerung der Motorbrücke existiert ein EPROM zur Programmierung von Funktionen
wie beispielsweise die Einstellung von Spannungsreferenzen und Verstärkereinstellungen.
Beispiel 2.10 Tabelle 2.7 zeigt unterschiedliche, vom Halbleiterhersteller Philips stammende, Konfigurationen von Ein-Chip-Systemen, die den Prozessor 8051 verwenden. Eine typische Konfiguration eines
Ein-Chip-Systems mit einem 8051-Prozessor ist in Abb. 2.26 dargestellt.
2.3.2
Board-level Systeme
Es gibt eine Reihe von Gründen, die für einen Platinenentwurf sprechen können:
• Erfüllbarkeit: Das System passt nicht auf einen einzelnen Chip.
2.3. SYSTEMAUFBAU
35
Abbildung 2.24: Layout eines Ein-Chip-Systems von Texas Instruments (cDSP)
Abbildung 2.25: Layout des Ein-Chip-Systems SMARTPOWER
• Kosten: In der geplanten Stückzahl ist die Benutzung von Standardchips kostengünstiger.
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
36
processor
80C51
8K8 ROM
(87C552 8K8
EPROM)
15-vector
interrupt
256x8 RAM
timer0 (16 bit)
timer1 (16 bit)
A/DC
10-bit
PWM
timer2 (16 bit)
UART
watchdog (T3)
I2C
parallel ports 1 through 5
Abbildung 2.26: Philips 83 C552: 8-Bit basierter Mikrocontroller
Eigenschaft
Rom Eprom
Ram [Bytes]
EEPROM [Bytes]
I/O parallel
I/O UART
I/O I2 C
Timer 0,1
Timer 2,3
ADC 8-Eing.
PWM 2-Ausg.
Int.vektoren
Gehäuse
Dil40
PLCC-44
QFP-44
PLCC-68
83/87
C552
83/87
C562
83/87
C652
83/87
C662
83/87
C654
83/87
C528
83
C851
83/87
C592
8K8
256
8K8
256
8K8
256
8K8
256
16K8
256
32K8
256
8K8
256
6x8
x
x
x
x
10 Bit
x
15
6x8
x
4x8
x
x
x
4x8
x
4x8
x
x
4x8
x
x
x
4K8
128
256
4x8
x
x
x
6
5
6
6
5
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
8 Bit
x
14
x
6x8
x
CAN
x
x
10 Bit
x
15
x
Tabelle 2.7: Konfigurationen des 8051 (Quelle: Philips Halbleiter)
• Entwurfszeit: Der Entwurf mit Standardkomponenten (1 Tag...1 Woche) geht häufig
schneller als der Entwurf eines SoC.
• Flexibilität: Spätere Änderungen sind leichter möglich.
• Fehlertoleranz: Fehlertolerante Systeme sind typischerweise verteilt.
Für Multi-Chip-Module sprechen Argumente von sowohl Platinen- als auch EinChip Systemen. Schliesslich kann eine Realisierung auch aus mehreren Platinen bestehen, wenn das System nicht auf ein Board passt. Multi-board Systeme können auch
2.3. SYSTEMAUFBAU
37
skalierbar sein, d.h., durch Hinzufügen von weiteren Boards kann die Systemleistung
gesteigert werden. Diese Systeme können auch leichter gewartet werden (durch Austausch defekter Platinen). Ein wesentlicher Nachteil ist die komplexere Kommunikationsstruktur der Teilsysteme untereinander, die z.B. durch backplanes verbunden sind.
Diese backplanes erlauben schon allein wegen der Länge der Verbindungen keine hohen
Kommunikationsraten zwischen den Teilsystemen.
38
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
Kapitel 3
Systementwurf – Methoden und
Modelle
3.1
3.1.1
Entwurfsmethoden
Erfassen und Simulieren
Diese traditionelle und zum Teil immer noch verwendete Entwurfsmethodik für
HW/SW-Systeme setzt sich aus den zwei Hauptkomponenten Erfassen und Simulieren
zusammen. Man startet mit einer informalen, umgangssprachlichen Spezifikation des
Produktes, die noch keine Informationen über die konkrete Implementierung enthält.
Es wird nur die Funktionalität bestimmt, aber nicht die Art und Weise ihrer Realisierung. Für Funktionen, die in Hardware realisiert werden sollen, wird anschliessend
eine grobe Blockstruktur der Architektur entworfen, die eine verfeinerte, aber immer
noch unvollständige Spezifikation darstellt. In weiteren Verfeinerungsschritten werden
die einzelnen Blöcke dann in Logik- oder sogar Transistor-Diagramme umgesetzt. Auf
dieser Basis lassen sich dann umfangreiche Simulationen der Funktionalität und des
Zeitverhaltens durchführen. Für Softwarefunktionen wird die informale Spezifikation
in Blockstrukturen und anschliessend in Assembler-Programme verfeinert. Es folgen
Simulationen und Emulationen zur Validierung von Funktionalität und Zeitverhalten,
bevor das endgültige Programm in Maschinensprache generiert wird.
3.1.2
Beschreiben und Synthetisieren
In den letzten Jahren hat sich eine Entwurfsmethodik durchgesetzt, die man mit Beschreiben und Synthetisieren charakterisieren kann. Ein System wird durch eine ausführbare Verhaltensbeschreibung spezifiziert, und danach wird die Struktur der Implementierung automatisch durch entsprechende Syntheseverfahren generiert. Das ist im Vergleich zum Entwurf per Hand eine sehr viel schnellere und vor allem sicherere Entwurfsart.
Auf der Logik-Ebene werden funktionale Einheiten (z.Bsp. ALUs, Komparatoren,
Multiplizierer) und Steuerungseinheiten (z.Bsp. Zustandsmaschinen) durch die Logiksynthese automatisch generiert. Dafür braucht man Verfahren zur Minimierung Boolescher Ausdrücke, Zustandsminimierungen und die Technologie-Abbildung, d.h., die
39
KAPITEL 3. SYSTEMENTWURF – METHODEN UND MODELLE
40
Implementierung der minimierten Funktionen mit Gattern und Registern aus einer speziellen Bibliothek. Ein weiteres Beispiel für ein Syntheseverfahren ist die ArchitekturSynthese. Hier werden integrierte Schaltungen synthetisiert, die aus Speicherbausteinen,
Steuerungslogik und funktionalen Bausteinen bestehen. Das Verhalten dieser Schaltungen kann durch Algorithmen, Zustandsmaschinen, Datenflussgraphen, Instruktionssätze, etc., beschrieben werden, bei denen mit jedem Zustand eine beliebig komplexe Berechnung verbunden sein kann. Die Transformation in eine strukturelle Beschreibung
erfolgt anschliessend durch die drei Syntheseaufgaben Allokation, Ablaufplanung und
Bindung.
• Aufgabe der Allokation ist es, die Zahl und Art der Komponenten zu bestimmen,
die in der Implementierung verwendet werden sollen (z.Bsp. die Zahl der Register
und Speicherbänke, die Zahl und Arten der internen Busse sowie die verwendeten funktionalen Einheiten). Die Allokation ist ein wesentlicher Schritt, der die
Balancierung von Kosten und Leistungsfähigkeit bestimmt.
• Die Ablaufplanung teilt das spezifizierte Verhalten in Zeitintervalle ein, so dass
anschliessend für jeden Zeitschritt bekannt ist, welche Daten von einem Register
zu einem anderen transportiert und wie sie dabei von den funktionalen Einheiten
transformiert werden.
• Die Bindung ordnet jeder Variablen eine entsprechende Speicherzelle, jeder Operation eine funktionale Einheit und jeder Datenkommunikation einen Bus oder eine
Verbindungsleitung zu.
Auch im Bereich des Software-Entwurfs wird das Paradigma des „Erfassens und
Simulierens“ angewendet. Die Funktionalität des Systems wird durch eine ablauffähige Hochsprache spezifiziert, z.Bsp. C, C++, Oberon, Java. Anschliessend ist es die Aufgabe des Synthesewerkzeuges (Übersetzers), automatisch ein Maschinenprogramm für
einen bestimmten Prozessor zu generieren. Grundsätzlich sind wieder die drei wesentlichen Aufgaben der Allokation, Ablaufplanung und Bindung durchzuführen. Auch
wenn beim Software-Entwurf die Zielarchitektur im allgemeinen gegeben ist und damit
der Allokationsschritt weitgehend entfällt, sind Ablaufplanungs- und Bindungsprobleme zu lösen. So sind bei der Softwaresynthese die Operationen und Datentransporte
in Zeitschritte einzuteilen, wobei die zur Verfügung stehenden Ressourcen (Busbandbreite, Zahl der Busse, Zahl der internen Register und Zahl und Art der funktionalen
Einheiten) berücksichtigt werden müssen.
3.1.3
Spezifizieren, Explorieren und Verfeinern
Auf der Ebene komplexer Systeme ist die Entwurfsmethodik bei weitem noch nicht
so ausgereift wie in den bisher beschriebenen Bereichen. Dennoch scheint sich hier ein
Paradigma durchzusetzen, das durch die Stichworte „spezifizieren, explorieren und verfeinern“ beschrieben werden kann.
In der Spezifikationsphase wird in einem sehr frühen Stadium des Entwurfsprozesses
eine ausführbare Spezifikation des Gesamtsystems erstellt. Sie ist Ausgangspunkt und
Grundlage für
3.2. ABSTRAKTION UND ENTWURFSREPRÄSENTATIONEN
41
• die Beschreibung der Funktionalität eines Systems (z.Bsp. um die Wettbewerbsfähigkeit eines Produktes abzuschätzen),
• die Dokumentation des Entwurfsprozesses in allen Schritten,
• die automatische Verifikation kritischer Systemeigenschaften,
• die Untersuchung und Exploration verschiedener Realisierungsalternativen,
• die Synthese der Teilsysteme und
• die Veränderung und Nutzung bereits bestehender Entwürfe.
Die Explorationsphase dient dazu, verschiedene Realisierungsalternativen bezüglich
ihrer Kosten und Leistungsfähigkeit zu vergleichen. Die wesentliche Aufgabe ist hier,
die Systemfunktionen auf mögliche Komponenten eines heterogenen Systems zu verteilen. Für die Realisierung der Teilsysteme gibt es eine Fülle von Alternativen, von
programmierbaren Mikroprozessoren bis hin zu anwendungsspezifischen integrierten
Schaltungen. Um die untersuchten Realisierungsalternativen bewerten zu können, muss
eine Schätzung der wesentlichen Eigenschaften, wie Verarbeitungsleistung, Kosten, Leistungsverbrauch und Testbarkeit, durchgeführt werden.
In der anschliessenden Verfeinerungsphase wird die Spezifikation entsprechend der
Partitionierung und Allokation auf die verschiedenen Hardware- und Softwarekomponenten verteilt und die korrekte Kommunikation zwischen diesen Einheiten sichergestellt. Die Ausgangslage ist also vergleichbar mit der Situation nach der Bestimmung
eines Block-Diagramms auf der Grundlage einer informalen Spezifikation, siehe Abschnitt 3.1.1. Im Unterschied dazu wird hier die Aufteilung nach der Exploration eines
grossen Entwurfsraumes erhalten und die Verfeinerung steht auf „sicheren Füssen“, da
sie formal aus der gegebenen Spezifikation abgeleitet wurde.
In weiteren Verfeinerungsschritten kann dann der gesamte Prozess der Exploration
und Verfeinerung wiederholt werden, bis eine vollständige strukturelle Beschreibung
zur Implementierung des Systems vorliegt. Durch ein solches Vorgehen werden nicht
nur frühzeitig mögliche Entwurfsalternativen (z.Bsp. Software statt Hardware, anwendungsspezifische Schaltungen statt Standardkomponenten) geprüft, sondern es wird
auch die Anzahl von teuren und zeitraubenden Entwurfsiterationen reduziert.
3.2
Abstraktion und Entwurfsrepräsentationen
Die folgenden Abschnitte enthalten eine kurze Darstellung der verschiedenen Abstraktionsebenen und Sichten eines Systems, sowie eine Aufzählung von Synthese- und Optimierungsaufgaben beim Systementwurf.
3.2.1
Modelle
Unter einem Modell versteht man die formale Beschreibung eines Systems oder Teilsystems. Dabei werden von einem zu modellierenden Objekt nur ganz bestimmte Eigenschaften gezeigt und andere Details weggelassen. Diesen Vorgang nennt man Abstraktion. Wie in den vorangegangenen Abschnitten erläutert, beruht der Entwurf eines
KAPITEL 3. SYSTEMENTWURF – METHODEN UND MODELLE
42
Systems auf dem Prinzip der Verfeinerung, d.h., der Grad an Detailliertheit wird beim
Entwurf schrittweise erhöht. Man kann Modelle anhand des Grades Ihrer Verfeinerung
in Abstraktionsebenen einteilen. Unabhängig davon gibt es auch unterschiedliche Sichten
eines Objektes. So kann man eine Schaltung als Verbindung von Einzelkomponenten
betrachten oder auch als eine Einheit mit bestimmtem Verhalten. Abstraktionsebenen
und Sichten sind in gewisser Weise orthogonal zueinander. Obwohl man fast beliebig
viele Schichten für Sichten und Abstraktionsebenen einführen kann, werden im Rahmen dieses Skriptums vor allem die Abstraktionsebenen Architektur und Logik beim
Hardware-Entwurf, Modul und Block beim Software-Entwurf und System beim Entwurf
heterogener Systeme, sowie die Sichten Verhalten und Struktur unterschieden. Die Darstellung in Abb. 3.1 zeigt diese Schichten.
Software
Hardware
System
Modul
Architektur
Verhalten
...
Block
Logik
Struktur
Abbildung 3.1: Wichtige Abstraktionsebenen und Sichten beim Systementwurf
Die dargestellten Abstraktionsebenen sind:
System: Die Modelle der System-Ebene beschreiben das zu entwerfende Gesamtsystem
als Netzwerk, das aus komplexen, miteinander kommunizierenden Teilsystemen
besteht.
Architektur: Die Architektur-Ebene gehört zum Bereich des Hardware-Entwurfs. Modelle auf dieser Ebene beschreiben kommunizierende funktionale Blöcke, die komplexe Operationen ausführen.
Logik: Die Logik-Ebene gehört ebenfalls zum Hardware-Bereich. Die Modelle dieser
Ebene beschreiben verbundene Gatter und Register, die Boolesche Funktionen berechnen.
Modul: Die Modul-Ebene gehört zum Software-Bereich. Die entsprechenden Modelle
beschreiben Funktion und Interaktion komplexer Module.
Block: Die Block-Ebene gehört ebenfalls zum Software-Bereich. Die entsprechenden
3.2. ABSTRAKTION UND ENTWURFSREPRÄSENTATIONEN
43
Modelle beschreiben Programme bis hin zu Instruktionen, die auf der zugrundeliegenden Rechnerarchitektur elementare Operationen ausführen.
Die betrachteten Sichten sind:
Verhalten: In der Verhaltens-Sicht werden Funktionen unabhängig von ihrer konkreten
Implementierung beschrieben.
Struktur: In der strukturellen Sicht werden kommunizierende Komponenten beschrieben. Die Aufteilung und Kommunikation entsprechen der tatsächlichen Implementierung.
Anhand dieser Klassifizierung lässt sich der Entwurf eines komplexen Systems als
Abfolge von Verfeinerungsschritten verstehen, bei denen einer Verhaltensbeschreibung
strukturelle Informationen über die Implementierung hinzugefügt werden. Die entstehenden Teilmodule sind dann wieder Ausgangspunkte von Verfeinerungen auf der
nächst niedrigeren Abstraktionsebene sind (siehe Abb. 3.1). Bei dieser Darstellung wird
allerdings stark vereinfachend ausser Acht gelassen, dass bei einem konkreten Entwurf
viele Iterationen zwischen den Abstraktionsebenen notwendig werden, also nicht nur
top-down, sondern auch bottom-up vorgegangen wird. Einige Systemteile werden zudem direkt auf unteren Abstraktionsebenen entworfen, so dass zu einem bestimmten
Zeitpunkt im Entwurfsprozess nicht alle Systemteile den gleichen Abstraktions- oder
Verfeinerungsgrad aufweisen.
Aufgabe der Synthese ist die (teil-)automatische Transformation zwischen den verschiedenen Abstraktionsebenen und Sichten. Um die Zusammenhänge zu verdeutlichen, werden anhand von Synthesebeispielen einige der besprochenen Ebenen und Sichten näher erläutert.
3.2.2
Synthese
Architektur-Synthese
Aufgabe der Architektur-Synthese ist die Generierung einer strukturellen Sicht auf Architekturebene. Wesentliche Aufgaben dabei sind:
• Identifikation von Hardware-Elementen, die die spezifizierten Operationen ausführen können (Allokation)
• Ablaufplanung zur Bestimmung der Zeitpunkte, an denen die Operationen ausgeführt werden
• Zuordnung von Variablen zu Speichern, Operationen zu funktionalen Einheiten
und Kommunikationskanälen zu Bussen (Bindung)
Die makroskopischen Eigenschaften, wie Schaltungsfläche und Verarbeitungsleistung, hängen wesentlich von Optimierungen auf dieser Abstraktionsebene ab.
KAPITEL 3. SYSTEMENTWURF – METHODEN UND MODELLE
44
Logik-Synthese
Aufgabe der Logik-Synthese ist die Generierung einer strukturellen Sicht auf LogikEbene. Ausgangspunkte der Logik-Synthese können zum Beispiel Boolesche Gleichungen oder Zustandsautomaten sein, die entweder durch graphische Methoden oder mit
Hilfe eines Programms in einer Hardware-Beschreibungssprache spezifiziert wurden.
Bei der Logik-Synthese werden unter anderem folgende Teilprobleme gelöst:
• Optimierung Boolescher Ausdrücke
• Zustandsminimierung und Zustandscodierung
• Bindung an eine Bibliothek von Zellen, d.h., ein logisches Modell wird in eine
Verbindung von Instanzen der Bibliothekszellen transformiert
Optimierungsverfahren spielen auch hier eine zentrale Rolle, da die mikroskopischen Eigenschaften einer Implementierung festgelegt werden. Ergebnis der LogikSynthese ist eine strukturelle Repräsentation, die Gatter, Register sowie ihre Verbindungen charakterisiert (Netzliste).
Operationswerk
Steuerwerk
Datenverteilung
Speicher
ALU
*
Steuerungseinheit
Abbildung 3.2: Beispiel einer strukturellen Sicht auf Architektur-Ebene
__
in / 0
in / 0
x0
in / 1
x1
in
clk
&
out
1D Q
C1
__
in / 0
Abbildung 3.3: Beispiel einer Verhaltenssicht (Zustandsdiagramm) und einer strukturellen Sicht auf Logik-Ebene
Beispiel 3.1 Abb. 3.2 zeigt ein Beispiel für eine strukturelle Sicht auf Architekturebene. Das Steuerwerk hat die Aufgabe, durch entsprechende Steuersignale die Operationen im Operationswerk sequentiell
ablaufen zu lassen. Dies ist ein typisches Beispiel, bei dem eine Verhaltensbeschreibung in Form eines
Zustandsdiagramms angebracht ist. Aufgabe der Logiksynthese ist es, eine Schaltung zu generieren, die
diese Spezifikation implementiert. Abb. 3.3 zeigt Beispiele für ein Verhaltens- und ein Strukturmodell auf
der Logik-Ebene. Das Verhalten wird durch einen endlichen Zustandsautomaten modelliert, der zwei oder
3.2. ABSTRAKTION UND ENTWURFSREPRÄSENTATIONEN
45
mehr aufeinanderfolgende ’1’ im Eingangsstrom erkennt. Diese Sichten lassen sich in einer HardwareBeschreibungssprache formulieren. In VHDL lautet das Zustandsdiagramm:
ENTITY rec IS
PORT (in, clk: IN BIT; out: OUT BIT);
END rec;
ARCHITECTURE behavior OF rec IS
TYPE state_type IS (zero, one);
SIGNAL state: state_type := zero;
PROCESS
BEGIN
WAIT UNTIL clk’EVENT AND clk = ’1’;
IF (in = ’1’) THEN
CASE state IS
WHEN => zero
state <= one;
out <= ’0’;
WHEN => one
state <= one;
out <= ’1’;
END CASE;
ELSE
state <= zero;
out <= ’0’;
END IF;
END PROCESS;
END behavior;
In diesem Modell ist das Signal state vom Aufzählungstyp und speichert den Zustand des endlichen
Automaten. Der Prozess wird jedesmal ausgeführt, wenn sich clk auf den Wert ’1’ ändert. Die WAITAnweisung synchronisiert das Modell auf den Takt clk.
Das strukturelle Modell in VHDL lautet:
ARCHITECTURE structure OF rec IS
COMPONENT and PORT (i1, i2: IN BIT; o1: OUT BIT); END COMPONENT;
COMPONENT dff PORT (d1, cl1: IN BIT; q1: OUT BIT); END COMPONENT;
SIGNAL int: BIT;
BEGIN
g1: and PORT MAP (in, int, out);
g2: dff PORT MAP (in, clk, int);
END structure;
Bild 3.4 zeigt eine Schaltungsrepräsentation, die direkt dem VHDL-Modell entspricht.
Modul- und Blocksynthese
Im Bild 3.1 haben wir auf der Seite der Software die Abstraktionsebenen Modul und
Block unterschieden. Auf der Modulebene könnte eine Verhaltensbeschreibung zum Beispiel in Form einer algebraischen Spezifikation vorliegen, die die Eigenschaften des zu
entwickelnden Software-Systems in Form mathematischer Sätze und Axiome beschreibt.
KAPITEL 3. SYSTEMENTWURF – METHODEN UND MODELLE
46
rec (structure)
and
in
dff
clk
d1
q1
cl1
g2:
i1
i2
o1
out
g1:
Abbildung 3.4: Schaltungsdiagramm zu einem strukturellen VHDL-Modell auf LogikEbene
Aufgabe der Synthese ist es, eine äquivalente strukturelle Darstellung zu erzeugen, zum
Beispiel formuliert in einer höheren Programmiersprache (C, C++, Oberon, Java, etc.)
oder einer echtzeitfähigen Sprache (Esterel, Pearl, Ada 9X, etc.).
In der nächst tieferen Blockebene wird nun hieraus durch einen Übersetzungsvorgang ein Assembler- oder Maschinenprogramm erzeugt. Die folgende Aufzählung enthält einige der wesentlichen Transformations- und Optimierungsvorgänge:
• Programmtransformationen zur optimalen Ausnutzung von Instruktionspipelining
• Optimierung des Speicherplatzbedarfs
• Parallelisierung auf Instruktionsebene, um parallele funktionale Einheiten im Zielprozessor ausnutzen zu können
• Ablaufplanung der verschiedenen Software-Prozesse bei Echtzeitsystemen
• Einbindung von Betriebssystemroutinen, z.Bsp. zur Interrupt-Steuerung und zur
Ein- und Ausgabe von Daten.
Im Gegensatz zu einem Programm in einer höheren Programmiersprache ist ein
Assemblerprogramm im allgemeinen nicht nur erheblich länger, sondern auch abhängig von der jeweiligen Zielarchitektur. Ausserdem fehlen Möglichkeiten zur TypÜberprüfung und zum Strukturieren des Kontrollflusses.
Beispiel 3.2 Als Beispiel für die beiden Sichten auf der Blockebene betrachten wir als Verhaltensbe100 2
schreibung ein C-Programm, das ∑ii=
=0 i berechnet:
#include <stdio.h>
int main (int argc, char *argv[])
{
int i;
int sum = 0;
for (i = 0; i <= 100; i = i + 1) sum = sum + i * i;
printf ("The sum of i*i from 0 ... 100 is %d\n", sum);
}
3.2. ABSTRAKTION UND ENTWURFSREPRÄSENTATIONEN
47
Nach der Übersetzung für den RISC-Prozessor MIPS R2000 entsteht das folgende AssemblerProgramm:
...
main:
subu
sw
sd
sw
sw
loop:
lw
mul
lw
addu
...
$29, $sp, 32
$31, 20($29)
$4, 32($29)
$0, 24($29)
$0, 28($29)
$14,
$15,
$24,
$25,
28($29)
$14, $14
24($29)
24($29)
Dieses Assembler-Programm muss anschliessend noch in ein binäres Maschinenprogramm umgesetzt
werden.
System-Synthese
Das Interesse an automatisierten Syntheseverfahren auf der Systemebene lässt sich vor
allem auf die folgenden Gründe zurückführen:
Kurze Entwurfszyklen: Ein automatisiertes Entwurfssystem ist in der Lage, einen Entwurf schneller durchzuführen als dies ein Entwickler ohne Unterstützung von
CAD-Werkzeugen könnte. Ein vergleichbares Beispiel ist die grosse Zeitersparnis
durch Werkzeuge zum automatisierten Plazieren und Verdrahten von Leiterplatten und integrierten Schaltungen. In fast allen Bereichen der Technik ist in den
vergangenen Jahren eine enorme Reduktion der Produktlebensdauern und somit
der time-to-market festzustellen. Mit dieser Entwicklung kann man nur durch den
Einsatz geeigneter CAD-Verfahren schritthalten.
Reduzierte Entwurfsfehler: Um kostspielige Iterationen aufgrund von Fehlern im Entwurf zu vermeiden, wird auf die Entwicklung von Synthesewerkzeugen Wert gelegt, die beweisbar korrekte Entwürfe liefern. Dies gelingt einerseits dadurch, dass
der Verfeinerungsvorgang von einer Verhaltensbeschreibung hin zu einer strukturellen Beschreibung als eine Sequenz von Programmtransformationen verstanden
wird, und andererseits dadurch, dass formale Verifikationsverfahren eingesetzt
werden.
Exploration des Entwurfsraumes: Gerade auf den obersten Entwurfsebenen werden
grundlegende Entwurfsentscheidungen getroffen, die die Leistungsfähigkeit und
Kosten des implementierten Systems bestimmen. Die Konsequenzen von Fehlentscheidungen werden somit in einem frühen Entwurfsstadium deutlich, zum
Beispiel die Verletzung von Zeitbeschränkungen. Mit einem Entwurfswerkzeug
auf Systemebene können unterschiedliche Realisierungsarten einer Spezifikation
schnell unter verschiedenen Optimierungsgesichtspunkten bewertet werden. Man
nennt dies eine Exploration des Entwurfsraumes.
KAPITEL 3. SYSTEMENTWURF – METHODEN UND MODELLE
48
Wie auch in den anderen Abstraktionsebenen ist die Systemebene durch charakteristische Beschreibungsformen bezüglich des Verhaltens und der Struktur gekennzeichnet. Das Verhalten wird durch die funktionale Spezifikation, Leistungsanforderungen
(z.Bsp. Zeitverhalten) und nicht-funktionale Eigenschaften beschrieben. Die strukturelle Beschreibung zeigt das System als Netzwerk aus Prozessoren, Standardkomponenten, anwendungsspezifischen integrierten Schaltungen, Verbindungsstrukturen und
Speicherbausteinen. Entscheidende Entwurfsaufgabe ist auf dieser Ebene die Partitionierung der Verhaltensbeschreibung in Teilsysteme. Hierbei spielen sehr unterschiedliche
Optimierungskriterien eine Rolle, wie Kosten, Verarbeitungsleistung und Leistungsverbrauch, aber auch nicht-funktionale Kriterien wie Wiederverwendbarkeit des Entwurfs
in zukünftigen Produktlinien, time-to-market und Flexibilität gegenüber Produktänderungen.
Beispiel 3.3 Das folgende Beispiel zeigt einige typische Probleme, die bei einem Entwurf auf Systemebene entstehen. In digitalen Video-Anwendungen ist es oft erforderlich, die Übertragungsbandbreiten
durch eine geeignete Datenkompression zu reduzieren. Das folgende Beispiel ist ein Hybrid-Kodierer, der
Transformationskodierung und prädiktive Kodierung kombiniert. Der Kompressionsfaktor einer reinen
Bildkodierung wird durch ein prädiktives Schema für Bildfolgen verbessert. Ein Block innerhalb eines
Bildes wird aus einem Block innerhalb des vorangegangenen Bildes geschätzt. Abbildung 3.5 zeigt eine
Darstellung eines solchen Hybrid-Kodierers auf der Verhaltensebene.
current frame
a[i]
prediction error
b[i]
DCT
Q
Q
DCT
predicted frame
k[i]
c[i]
1
motion
loop h[i]
compensation
filter
motion vector g[i]
motion
estimation
RLC
1
d[i]
+
frame memory
e[i]
previous frame
f[i]
Abbildung 3.5: Darstellung eines Hybrid-Kodiereres für Bildsequenzen
Aus der nun folgenden Beschreibung wird deutlich, dass die einzelnen Blöcke der Darstellung komplexe Teiloperationen beschreiben und die Kommunikation mittels komplexer Datentypen erfolgt (hier
Bildsequenzen, wobei die Bilder ihrerseits wieder aus Blöcken, Makroblöcken und einzelnen Pixeln zusammengesetzt sind). Die zweidimensionale diskrete Kosinus-Transformation (DCT) wird auf nicht überlappende Blöcke des Prädiktionsfehler-Bildes b[i ] angewendet. Die transformierten Blöcke repräsentieren den
räumlichen Frequenzinhalt des entsprechenden Blocks. Die nachfolgende Quantisierung (Q) benutzt die
räumliche Irrelevanz innerhalb eines Bildes und die abschliessende Kodierung (RLC) die Dynamik der zu
übertragenden Werte, um die Übertragungsrate zu reduzieren. Die Bewegungsschätzung und Bewegungskompensation werden für die Kodierung zwischen aufeinanderfolgenden Bildern einer Sequenz benutzt.
Ein Block im Bild a[i ] wird mit Nachbarblöcken des vorangegangenen Bildes f [i ] verglichen und hieraus
ein Bewegungsvektor g[i ] bestimmt. Als Resultat der Bewegungskompensation erhält man ein geschätztes
Bild k [i ]. In der Darstellung nach Bild 3.5 werden Teilalgorithmen als Blöcke dargestellt. Hier gibt es noch
keine Spezifikation des zeitlichen Ablaufs, der Abbildung auf eine Zielarchitektur, der Speichergrössen, der
Partitionierung von Bildern in Blöcke oder Makroblöcke.
Bild 3.6 zeigt eine mögliche Systemarchitektur, die aus den Komponenten BUS, CM (Steuerungsprozessor für die Speicherzugriffe und Bus-Arbitrierung), FC (Bildspeicher), BM (Spezialmodul für die
3.2. ABSTRAKTION UND ENTWURFSREPRÄSENTATIONEN
49
Bewegungsschätzung), BC (lokaler Speicher für das BM), PM (Prozessormodul) und GC (lokaler Speicher
für die PM-Module) besteht.
FC
CM
BUS
spezielle
HardwareModule
Speicherbausteine
BC
BM
GC
PM
PM
Prozessoren
Abbildung 3.6: Strukturelle Sicht einer möglichen Implementierung eines Video-Codec
Neben den Beschreibungsformen unterscheiden sich die verschiedenen Abstraktionsebenen vor allem in den Freiheitsgraden, die bei der Verfeinerung von der Verhaltenssicht auf eine strukturelle Sicht bestehen. Die Entwurfsfreiheit nimmt von den
oberen Abstraktionsebenen zu den niedrigeren immer weiter ab. Es ist ineffizient, sich
Gedanken über Details der Implementierung zu machen, bevor nicht grundlegende Entscheidungen bezüglich der Systemarchitektur getroffen worden sind. Insbesondere können die folgenden Entscheidungen getroffen und die daraus resultierenden Kosten- und
Leistungsfaktoren gegeneinander abgewogen werden:
• Festlegung der Art und Anzahl von Komponententypen, die in der Implementierung verwendet werden (Allokation), wie z.Bsp. Mikroprozessoren, ASICs,
Speicherbausteine. Dazu gehören auch die Verbindungsstrukturen.
• Zuordnung der Variablen zu Speicherbausteinen, Operationen zu Funktionsbausteinen und Kommunikationen zu Bussen. Dieses Bindungsproblem wird im Bereich des HW/SW-Codesign oft als Partitionierung bezeichnet, da es sich dabei um
eine Aufteilung in Hardware und Software handelt. Hierbei sind auch Realisierungen in Hardware und in Software gegeneinander abzuwägen.
Beispiel 3.4 In Zusammenhang mit dem vorangegangenen Beispiel gibt es nun verschiedene Zielarchitekturen, auf die das gesamte System abgebildet werden kann, z.Bsp.:
• ein einziger Mikroprozessor, Signal- oder Bildprozessor
• mehrere parallel arbeitende programmierbare Prozessoren
• eine Erweiterung der oben genannten Architekturen mit spezialisierten funktionalen Einheiten,
z.Bsp. für die diskrete Kosinus-Transformation oder die Bewegungsschätzung
• eine reine spezialisierte Hardware-Lösung, die an den Algorithmus genau angepasst ist
KAPITEL 3. SYSTEMENTWURF – METHODEN UND MODELLE
50
FC
CM
BUS
spezielle
HardwareModule
BM
DM
PC
BC
DC
PM
Speicherbausteine
Prozessor
Abbildung 3.7: Strukturelle Sicht auf eine leicht veränderte Implementierung eines
Video-Codec
Zu jeder dieser Implementierungen existieren wiederum verschiedene Möglichkeiten der Zuordnung
von Daten zu Speichern und Teilalgorithmen zu Modulen, der Wahl von Kommunikationsstruktur und
Busbandbreiten sowie der Ablaufplanung der einzelnen Teilalgorithmen. Als Beispiel einer nur graduellen
Änderung könnte man die Kommunikation zu den lokalen Cache-Speichern verändern und einen der
allgemein programmierbaren Prozessoren durch ein Spezialbaustein DM mit privatem Cache-Speicher
DC ersetzen, der effizient die diskrete Kosinus-Transformation berechnen kann (siehe Bild 3.7).
3.2.3
Optimierung
Optimierung ist ein entscheidender Gesichtspunkt von Entwurfsverfahren auf allen Abstraktionsebenen. Die unterschiedlichen strukturellen Implementierungen eines Systems
definieren seinen Entwurfsraum. Der Entwurfsraum ist somit eine endliche Menge von
Entwurfspunkten. Mit jedem dieser Entwurfspunkte sind Werte der Zielfunktionen verbunden, z.Bsp. Kosten und Verarbeitungsleistung. Aufgabe der Optimierung ist es, den
besten Entwurf zu finden, d.h. diejenige Implementierung, die alle Zielfunktionen optimiert. Da ein Optimierungsproblem auf Systemebene aber verschiedene Kriterien beinhaltet, die oft dazu noch gegenläufig sind (einen trade-off bilden), muss der Begriff
der Optimalität näher betrachtet werden. Beim Systementwurf gibte es eine Menge von
sinnvollen Entwurfspunkten, die man Pareto-Punkte nennt. Ein Entwurfspunkt ist genau
dann ein Pareto-Punkt, falls er von keinem anderen Entwurfspunkt des Entwurfsraumes in allen Eigenschaften übertroffen wird. Im Gegensatz zum Begriff des globalen
Optimums gibt es beim Systementwurf hier i.allg. mehrere Pareto-Punkte.
Beispiel 3.5 Das Beispiel der Implementierung eines Video-Codec wird hier fortgesetzt. Als mögliche
Kriterien (unter vielen anderen) für eine Exploration des Entwurfraumes werden die Chipfläche bei einer Implementierung auf einer einzigen integrierten Schaltung und die Verarbeitungsrate, ausgedrückt
durch die erreichbare Bildperiode, betrachtet. In die Chipfläche gehen die Zahl und Art aller Teilsysteme (Prozessoren, Coprozessoren, Bildspeicher, schnelle Cache-Speicher, Busse) mit ein. Die Komplexität
der Syntheseaufgabe wird deutlich, wenn man bedenkt, dass für jede betrachtete Architektur jeweils eine
möglichst optimale Partitionierung und Ablaufplanung bestimmt werden muss.
In Bild 3.8 wird ein kleiner Ausschnitt des Entwurfsraumes gezeigt, der durch die Parameter Flächenbedarf und Bildperiode in zwei Dimensionen aufgespannt wird. Nur die Pareto-Punkte führen zu sinnvollen Implementierungen. Aus der Menge der Pareto-Punkte wird schlussendlich ein einzelner Punkt für
die Implementierung gewählt. Dies geschieht durch Gewichtung der Parameter. Die Gewichtung kann in
3.3. GRAPHENMODELLE FÜR KONTROLL- UND DATENFLUSS
Flächenbedarf
51
suboptimale
Entwurfspunkte
100
1
80
2
3
60
Pareto-Punkte
40
20
0
0
10
20
30
40
50
60
Bildperiode
Abbildung 3.8: Beispiel eines Entwurfsraumes mit drei Pareto-Punkten
Form einer Zielfunktion, die alle Parameter einschliesst, gegeben sein oder durch zusätzliche Rahmenbedingungen, wie z.Bsp. die Bedingung, dass das Produkt einen gewissen Kostenwert (Flächenbedarf) nicht
überschreiten darf.
3.3
Graphenmodelle für Kontroll- und Datenfluss
Kontroll- und Datenflussgraphen sind häufig verwendete Modellierungsformen im Entwurf von HW/SW-Systemen. Bei diesen Modellen stellen die Knoten der Graphen Aktivitäten (Operationen, Tasks) und die Kanten die Abhängigkeiten zwischen den Operationen dar. Abhängigkeiten zwischen Operationen treten aus mehreren Gründen auf:
• Verfügbarkeit von Daten
Wenn eine Operation für ihre Ausführung Daten braucht, die von einer anderen
Operation erzeugt werden, besteht eine Datenabhängigkeit (data dependency) zwischen den Operationen.
• Kontrollfluss
Bei Kontrollflussanweisungen (z.Bsp. Verzweigungen) muss zuerst eine Bedingung ausgewertet werden, bevor weitere Operationen gestartet werden können.
Das erzeugt eine Kontrollflussabhängigkeit (control dependency). Weiters werden
durch die Spezifikation, z.Bsp. in einer sequentiellen Programmiersprache, Abhängigkeiten zwischen den Anweisungen definiert. Diese Abhängigkeiten sind in
einem gewissen Sinn künstlich, da sie durch den Programmierer (notwendigerweise in einer sequentiellen Programmiersprache) eingeführt werden und nicht
auf Datenabhängigkeiten beruhen.
• Ressourcenbeschränkungen
Wenn mehrere Operationen auf eine gemeinsame Ressource zugreifen (z.Bsp. zwei
Instruktionen auf eine ALU) entstehen Abhängigkeiten. Diese Form der Abhängigkeit ist im Gegensatz zu den vorher genannten Abhängigkeiten nicht durch die
Spezifikation gegeben, sondern entsteht erst durch die Implementierung.
52
KAPITEL 3. SYSTEMENTWURF – METHODEN UND MODELLE
Beispiel 3.6 Das folgende Programmstück modelliert eine Schaltung, die eine Differentialgleichung der
Form y00 + 3xy0 + 3y = 0 im Intervall [ x, a] mit der Schrittweite dx und den Anfangswerten y(0) = y,
y0 (0) = u mit Hilfe der Euler-Methode numerisch löst.
diffequ {
read (x, y, u, dx, a);
repeat {
x1 = x + dx;
u1 = u - (3 * x * u * dx) - (3 * y * dx);
y1 = y + u * dx;
c = x1 < a;
x = x1;
y = y1;
u = u1;
} until not(c);
write(y);
}
3.3.1
---------
1
2
3
4
5
6
7
8
Datenflussgraphen (DFGs)
Datenflussgraphen (data flow graphs, DFGs) sind gerichtete Graphen, deren Knoten Operationen bzw. Tasks darstellen und deren Kanten einen gerichteten Datenfluss repräsentieren. DFGs können Operationen und ihre Datenabhängigkeiten, aber nicht andere Arten von Abhängigkeiten modellieren. Beim Datenflussmodell wird angenommen, dass
für die Daten Variablen (Speicherplätze) existieren, die die Daten für die Dauer ihrer
Lebenszeit (Erzeugung bis letzte Verwendung) halten.
Beispiel 3.7 Abb. 3.9 zeigt den Datenflussgraphen der inneren Schleife (Anweisungen 1, 2, 3, 4) der
in Beispiel 3.6 beschriebenen Schaltung. Es gibt keine explizite Ausführungsreihenfolge der Operationen.
Durch Kommutativität und Assoziativität in den Ausdrücken kann man i.allg. unterschiedliche Datenflussgraphen für einen Ausdruck konstruieren. Die zyklischen Abhängigkeiten, die durch die Anweisungen 5, 6, 7 entstehen, sind hier nicht dargestellt. Es gibt Erweiterungen von Datenflussmodellen, die auch
die Modellierung zyklischer Abhängigkeiten erlauben.
3.3.2
Kontrollflussgraphen (CFGs)
Ein Datenflussgraph kann keine Kontrollstrukturen, wie z.Bsp. Verzweigungen und
Iterationen, modellieren. Dazu benutzt man Kontrollflussgraphen (control flow graphs,
CFGs). Ein Kontrollflussgraph ist ein gerichteter Graph, bei dem die Knoten den Anweisungen entsprechen und die Kanten die Nachfolgerelationen im (sequentiellen) Programmfluss ausdrücken, nicht aber Datenabhängigkeiten. Besitzt ein Knoten mehrere Nachfolger, so handelt es sich um einen Verzweigungsknoten. Der von einem Verzweigungsknoten ausgehende Programmfluss ist alternativ, d.h., es wird nur genau
ein Nachfolgeast durchlaufen. Die Auswahl eines Astes ist abhängig von Bedingungen
(Booleschen Ausdrücken), die man üblicherweise an die Ausgangskanten eines Verzweigungsknotens schreibt.
3.3. GRAPHENMODELLE FÜR KONTROLL- UND DATENFLUSS
3
x
u
dx
3
y
u
dx
y
dx
53
x
dx
x1
a
u
y1
c
u1
Abbildung 3.9: Datenflussgraph der Schleife in Beispiel 3.6 (Anweisungen 1 bis 4)
Beispiel 3.8 Abb. 3.10 zeigt den Kontrollflussgraphen der Schleife von Beispiel 3.6. Eine Kontrollflussabhängigkeit entsteht beispielsweise aus der sequentiellen Abarbeitungsreihenfolge der Anweisungen 6
und 7, obwohl keine Datenabhängigkeit zwischen den beiden Zuweisungsoperationen besteht.
Ein Nachteil von Kontrollflussgraphen ist, dass die Möglichkeit der parallelen Ausführung von Anweisungen von vorneherein nicht betrachtet wird. Folglich ist dieses
Modell vor allem im Bereich von steuerungsorientierten Anwendungen relevant. Ein
Vorteil von Kontrollflussgraphen ist ihre leichte Generierbarkeit aus einer Spezifikation
(z.Bsp. in Form eines C-Programms oder einer prozessorientierten Verhaltensbeschreibung), da eine Datenflussanalyse zur Bestimmung von Datenabhängigkeiten entfallen
kann.
3.3.3
Kontroll/Datenflussgraphen (CDFGs)
Kontroll/Datenflussgraphen (control/data flow graphs, CDFGs) sind heterogene Modelle, die eine Aufteilung einer Systemspezifikation in steuerungsorientierte Komponenten
und datenflussorientierte Komponenten erlauben. Eine einfache Weise, die Möglichkeiten von Kontrollfluss- und Datenflussgraphen zu vereinigen, ist die Erweiterung von
Datenflussgraphen durch sogenannte Verzweigungsknoten. Ein Verzweigungsknoten
stellt den Ursprung einer Menge alternativer Pfade dar, die den einzelnen Verzweigungsalternativen entsprechen. Verzweigungsknoten berechnen Verzweigungsentscheidungen. Wie bei Kontrollflussgraphen kennzeichnet man dann die Verzweigungsäste
mit den Ausdrücken, die die Ausführung des entsprechenden Teilzweigs bedingen. Ein
Schleifenkonstrukt lässt sich dann einfach durch eine Verzweigung mit Test auf die
Abbruchbedingung der Iteration modellieren. Oft ist es nützlich, eine gesamtheitliche
54
KAPITEL 3. SYSTEMENTWURF – METHODEN UND MODELLE
1
2
3
4
5
6
7
8
x1 < a
x1 >= a
Abbildung 3.10: Kontrollflussgraph der Schleife in Beispiel 3.6. Die Nummern der Knoten im CFG entsprechen den Anweisungsnummern im Beispiel 3.6
Betrachtung von Kontroll- und Datenfluss zu besitzen, aber den Kontrollfluss vom Datenfluss durch Einführung eines hierarchischen Graphenmodells zu separieren. Dies
leisten hierarchische CDFGs, sogenannte Sequenzgraphen.
Hierarchische Sequenzgraphen
Definition 3.1 Ein Sequenzgraph ist eine Hierarchie von gerichteten Graphen. Ein Element des Graphen heisst Einheit des Sequenzgraphen und ist ein erweiterter Datenflussgraph
GS (V, A) mit Knotenmenge V und Kantenmenge A sowie folgenden Eigenschaften:
• Eine Einheit besitzt zwei Arten von Knoten: a) Operationen bzw. Tasks und b) Hierarchieknoten. Hierarchieknoten dienen der Verbindung von Einheiten in der Hierarchie.
• Eine Einheit stellt einen azyklischen und polaren Graphen dar, d.h., es gibt zwei ausgezeichnete Knoten, den Startknoten und den Endknoten. Beide Knoten sind reine Hierarchieknoten und stellen keine Operation dar (NOP, no operation). Neben Start- und Endknoten können drei weitere Hierarchieknoten vorkommen: der Modulaufruf (CALL), die
Verzweigung (BR) und die Iteration (LOOP).
3.3. GRAPHENMODELLE FÜR KONTROLL- UND DATENFLUSS
55
• Einheiten des Sequenzgraphen, die Blätter sind, besitzen ausser den Start- und Endknoten
keine weiteren Hierarchieknoten.
NOP 0
1
2
3
6
8
10
7
9
11
4
5
n
NOP
Abbildung 3.11: Sequenzgraph für das Beispiel 3.6
Beispiel 3.9 Abb. 3.11 stellt einen Sequenzgraphen dar, der äquivalent zu dem in Abb. 3.9 gezeigten
Datenflussgraphen ist. Dieser Sequenzgraph besteht nur aus einer Einheit. Die Knoten 0 und n (n = 12)
sind Hierarchieknoten, wobei der Knoten 0 der Startknoten und Knoten n der Endknoten ist. Alle anderen
Knoten sind von 1 . . . 11 durchnummeriert.
Aus einem gegebenen Datenflussgraphen erhält man den zugehörigen Sequenzgraphen durch folgende Schritte:
1. Entfernen aller Eingangskanten, die zu Knoten ohne Vorgängerknoten führen.
2. Einfügen des Startknotens mit je einer Kante zu allen Knoten, die keine Eingangskanten besitzen.
3. Entfernen aller Ausgangskanten, die von Knoten ohne Nachfolgerknoten wegführen.
4. Einfügen eines Endknotens mit je einer Kante von jedem Knoten ohne Nachfolger
zu diesem Endknoten.
KAPITEL 3. SYSTEMENTWURF – METHODEN UND MODELLE
56
Ein Modulaufrufknoten (siehe Abb. 3.12) ist ein Zeiger auf eine Einheit des Sequenzgraphen auf niederer Hierarchiestufe. Er modelliert a) die Abhängigkeiten der unmittelbaren Vorgängerknoten zum Startknoten des aufgerufenen Moduls und b) die Abhängigkeiten vom Endknoten des aufgerufenen Moduls zu seinen unmittelbaren Nachfolgeknoten.
Verzweigung wird bei Sequenzgraphen durch einen Verzweigungsknoten (BR) und
eine Menge von Verzweigungsgraphen modelliert (siehe Abb. 3.13). Ein Verzweigungsgraph ist wiederum ein Sequenzgraph. Der Verzweigungsknoten berechnet einen Verzweigungsausdruck und wählt je nach dessen Wert einen der Verzweigungsgraphen zur
Ausführung aus. Die Anzahl unterschiedlicher Verzweigungsgraphen entspricht dem
Wertebereich des Verzweigungsausdrucks. Die Ausführung eines Verzweigungsgraphen
schliesst die Ausführung jedes anderen Verzweigungsgraphen aus.
Iterationen werden ähnlich dem Modulaufruf über Hierarchiebildung modelliert
(siehe Abb. 3.14). Der Hierarchieknoten LOOP wertet einen Ausdruck aus, der den Iterationsabbruch bestimmt. Jede Iteration entspricht dem Aufruf des Iterationsrumpfes,
der durch einen Sequenzgraphen repräsentiert wird.
NOP
CALL
NOP
NOP
NOP
Abbildung 3.12: Modellierung von Modulaufrufen in Sequenzgraphen
Beispiel 3.10 Abb. 3.12 zeigt die Modellierung von Modulaufrufen in Sequenzgraphen. Der dargestellte Sequenzgraph entspricht dem folgenden Programmstück:
x := a * b;
y := x * c;
z := a + b;
submodul(a,z);
mit
3.3. GRAPHENMODELLE FÜR KONTROLL- UND DATENFLUSS
57
PROCEDURE submodul (m,n) IS
p := m + n;
q := m * n;
END submodul;
NOP
BR
NOP
NOP
NOP
NOP
NOP
NOP
Abbildung 3.13: Modellierung von Verzweigung im Modell des Sequenzgraphen
In 3.13 ist die Modellierung von Verzweigungen dargestellt. Der dargestellte Sequenzgraph entspricht
den Anweisungen:
x := a *
y := x *
z := a +
IF z > 0
p := m
q := m
END IF;
b;
c;
b;
THEN
+ n;
* n;
Beispiel 3.11 Abb. 3.14 zeigt den Sequenzgraphen für das Programm zur Lösung einer Differentialgleichung nach der Euler-Methode (siehe Abb. 3.6). Das System führt drei Aufgaben durch: a) das Einlesen
der Eingangsdaten, b) die Iteration, die den Ausgabewert berechnet, und c) die Ausgabe des Ergebnisses.
Die Steuerung der Iteration erfolgt im Knoten LOOP, der die Variable c auswertet. Der Schleifenrumpf
ist der in Abb. 3.11 dargestellte Graph.
Den Knoten und Kanten von Sequenzgraphen kann man beliebige Attribute zuweisen. Häufig sind dies Messwerte oder Abschätzungen der Implementierungsparameter (Flächenbedarf- und/oder Berechnungszeiten). Die Berechnungszeiten können
dabei datenunabhängig oder datenabhängig sein. Nur datenunabhängige Berechnungszeiten können vor der Synthese genau abgeschätzt werden. Datenabhängige Operationen
(z.Bsp. Iterationen und Verzweigungen) können beschränkt oder unbeschränkt sein. Bei
KAPITEL 3. SYSTEMENTWURF – METHODEN UND MODELLE
58
NOP 0
NOP
1
READ
2
3
LOOP
6
8
10
7
9
11
4
WRITE
5
NOP
NOP
Abbildung 3.14: Sequenzgraphen für das Beispiel 3.6
beschränkten datenabhängigen Operationen können untere und obere Schranken berechnet werden. Eine unbeschränkte datenabhängige Operation ist z.Bsp. das Warten
auf externe Ereignisse.
Kapitel 4
Compiler und Codegenerierung
4.1
4.1.1
Compiler – Aufbau
Aufgaben eines Compilers
Ein Compiler ist ein Programm, das ein in einer bestimmten Sprache (der Quell-Sprache)
geschriebenes Programm in ein äquivalentes Programm einer anderen Sprache (der ZielSprache) übersetzt (siehe Abb. 4.1).
Quellprogramm
Zielprogramm
COMPILER
Fehlermeldungen
Abbildung 4.1: Ein- und Ausgabe eines Compilers
Im Rahmen dieses Skriptums wird die Quellsprache eine höhere Programmiersprache (high-level language, HLL) und die Zielsprache eine Assemblersprache sein. Um
von einem Programm in einer HLL zu einem Programm zu kommen, das auf einem
bestimmten Prozessor ausgeführt wird, sind i.allg. mehrere Schritte bzw. Werkzeuge
notwendig. Abb. 4.2 zeigt einen typischen Ablauf von Werkzeugen beim Übersetzungsprozess. Dem Compiler vorgeschaltet ist ein Preprocessor, zu dessen Aufgaben das Einlesen aller zum Quellprogramm gehörenden Dateien und das Expandieren von Makros
gehören. Nach dem Compiler erzeugt ein Assembler den Maschinencode. Dieser Code
ist relocatable, d.h., er besitzt keine absoluten Adressen und ist somit nicht an einen
bestimmten Adressbereich gebunden. Der Linker bindet diesen Maschinencode mit anderen Maschinencodes, die die verwendeten Bibliotheksfunktionen darstellen. Zur Laufzeit erzeugt der Loader schliesslich den absoluten Maschinencode und lädt den Code zur
Ausführung in den Speicher.
59
KAPITEL 4. COMPILER UND CODEGENERIERUNG
60
skeletal source program
preprocessor
source program
compiler
target assembly program
assembler
relocatable machine code
linker / loader
library (relocatable object files)
absolute machine code
Abbildung 4.2: Werkzeuge beim Übersetzungsprozess
4.1.2
Phasen eines Compilers
Ein Compiler besteht, wie in Abb. 4.3 dargestellt, aus mehreren Phasen. Jede Phase
transformiert das Quellprogramm in eine neue Repräsentationsform. Die ersten drei
Phasen werden auch als Analyse bezeichnet, die letzten drei Phasen als Synthese. Alle
Phasen haben Zugriff auf zwei weitere Einheiten, die Verwaltung der Symboltabelle
und die Fehlerbehandlung.
Analyse Die Analyse teilt sich in die drei Phasen lexikalische Analyse, syntaktische
Analyse und semantische Analyse auf.
• Lexikalische Analyse: Bei der lexikalischen Analyse, auch als lineare Analyse bezeichnet, wird das Quellprogramm von oben nach unten und links nach rechts
gelesen (scanning) und als Strom von Zeichen betrachtet. Der Zeichenstrom wird
in Symbole (tokens) zerlegt. Ein Symbol stellt eine Folge von Zeichen dar, die
zusammen eine bestimmte Bedeutung haben. Beispiele für solche Symbole sind
Bezeichner, Zahl oder Operator.
• Syntaktische Analyse: Bei der syntaktischen Analyse, auch als hierarchische Analyse bezeichnet, werden die Symbole zu Sätzen zusammengefasst (parsing). Diese
4.1. COMPILER – AUFBAU
61
Quellprogramm
Analyse
Lexikalische Analyse
Syntaxanalyse
Semantikanalyse
Symboltabellenverwaltung
Fehlerbehandlung
Zwischencodegenerierung
Codeoptimierung
Codegenerierung
Synthese
Zielprogramm
Abbildung 4.3: Die Phasen eines Compilers
Sätze werden durch die Grammatik der Quellsprache definiert. Die Grammatik
wird durch rekursive Regeln ausgedrückt. Die Regeln
Z → Bezeichner := A
A → A + A| A ∗ A| Bezeichner | Zahl
definieren zum Beispiel, wie ein Ausdruck A und eine Zuweisung Z aufgebaut
sind. Die erzeugten grammatikalischen Sätze werden durch einen Parse-Baum
oder einen Syntax-Baum dargestellt.
• Semantische Analyse: Bei der semantische Analyse werden die Sätze des Quellprogramms geprüft, um sicherzustellen, dass die Bestandteile des Programms
sinnvoll zusammenpassen.
Beispiel 4.1 Die Anweisung
position := initial + rate * 60
wird analysiert. Die lexikalische Analyse erzeugt die Symbolsequenz:
Bezeichner (position), Zuweisungssymbol (:=), Bezeichner (initial), Operator (+),
Bezeichner (rate), Operator (*), Zahl (60)
Die syntaktische Analyse bildet einen grammatikalischen Satz, der in Abb. 4.4 graphisch als Parsebaum
und als Syntaxbaum dargestellt ist. Die semantische Analyse überprüft den grammatikalischen Satz, stellt
fest, dass alle Bezeichner vom Typ real sind, und fügt eine Funktion zur Typumwandlung der Konstanten
von 60 in 60.0 ein.
KAPITEL 4. COMPILER UND CODEGENERIERUNG
62
Bezeichner
position
Parsebaum
Syntaxbaum
Zuweisung
:=
:=
Ausdruck
+
Ausdruck
Bezeichner
Ausdruck
*
initial
Bezeichner
Zahl
rate
60
Ausdruck
+
position
*
initial
Ausdruck
rate
60
Abbildung 4.4: Parse- und Syntaxbaum
Synthese Die Synthese teilt sich in die Phasen Zwischencodegenerierung, Codeoptimierung und Codegenerierung auf.
• Zwischencodegenerierung: Manche Compiler erzeugen nach der Analysephase
eine explizite Zwischendarstellung. Diesen Zwischencode kann man als Assemblerprogramm für eine abstrakte Maschine sehen, der folgende Eigenschaften aufweisen sollte:
– Der Zwischencode sollte leicht aus den Repräsentationsformen der Analysephasen erzeugbar sein.
– Der Zwischencode sollte einfach ins Zielprogramm übersetzbar sein.
Die Verwendung eines Zwischencodes entkoppelt die Analyse- und Synthesephasen eines Compilers. Dadurch wird die Analysephase maschinenunabhängig, und
es ist einfacher, einen Compiler an neue Zielsprachen anzupassen (retargeting).
Ausserdem können auf dem Zwischencode maschinenunabhängige Codeoptimierungen durchgeführt werden.
• Codeoptimierung: Codeoptimierung wird an mehreren Stellen der Synthesephase
durchgeführt. Einerseits kann der Zwischencode optimiert werden, andererseits
können viele Optimierungen erst bei bzw. nach der Codegenerierung gemacht
werden. Bezüglich der optimierten Parameter werden je nach Anwendungsgebiet
verschiedene Anforderungen gestellt:
– Bei general-purpose Prozessoren, die meist in PCs und Workstations eingesetzt werden, muss das Zielprogramm mit hoher Geschwindigkeit laufen,
und die Zeit für die Übersetzung soll gering sein.
4.1. COMPILER – AUFBAU
63
– Bei eingebetteten Prozessoren wird auf die Codequalität Wert gelegt, d.h.,
die Programmgrösse, der Speicheraufwand und die Ausführungszeit sollten
minimal sein. Die Übersetzungszeit ist hier von untergeordneter Bedeutung.
• Codegenerierung: Die Codegenerierung weist jeder im Programm benutzten Variablen einen Speicherplatz zu und übersetzt jede Instruktion des Zwischencodes
in eine Folge von Assemblerbefehlen der Zielmaschine.
Symboltabelle, Fehlerbehandlung Die Symboltabelle ist eine zentrale Datenstruktur
für alle Phasen eines Compilers. Diese Tabelle enthält für jeden im Quellprogramm
benutzten Bezeichner einen record. Diese records werden in der Analysephase durch
Eintragen verschiedener Attribute (z.Bsp. der Speicherbedarf, der Typ und der Gültigkeitsbereich bei Bezeichnern für Variablen, die Anzahl und die Typen der Argumente
bei Bezeichnern für Prozeduren, etc.) aufgebaut.
Beispiel 4.2 Abb. 4.5 zeigt anhand der Anweisung position := initial + rate * 60, wie die
Repräsentationen des Quellprogrammes nach den einzelnen Compilerphasen aussehen und wie die Symboltabelle aufgebaut ist.
Die Fehlerbehandlung ist eine wichtige Funktion eines Compilers, die seine Verwendbarkeit bestimmt. Fehler können in den verschiedensten Phasen auftreten. Die Reaktionen des Compilers auf Fehler können unterschiedlich sein: Abbruch mit verschiedenen Fehlermeldungen über den aufgetretenen Fehler, Tolerieren einer bestimmten
Anzahl von Fehlern, automatische Korrektur von Fehlern, etc.
4.1.3
Zwischencode
Zwischencodes sind eine maschinenunabhängige Repräsentation eines Programmes. Es
gibt einer Reihe von verschiedenen Darstellungen von Zwischencode, wobei die graphischen Darstellungen in Form von syntax trees (Syntaxbäume) und DAGs, directed acyclic
graphs (azyklische gerichtete Graphen) sowie der 3-Adress Code besondere Bedeutung haben.
Syntaxbäume, DAGs
Syntaxbäume sind die Darstellung, die bei der syntaktischen Analyse erzeugt werden
(siehe Abb. 4.4). Jeder Knoten des Baumes stellt eine subexpression (Teilausdruck) des
Ausdrucks dar, wobei die inneren Knoten die Operatoren, und die Kinder dieser inneren Knoten die Operanden darstellen. DAGs sind den Syntaxbäumen ähnlich. Sie
identifizieren aber zusätzlich gemeinsame subexpressions. Abb. 4.6 zeigt den Syntaxbaum und den dazugehörigen DAG für den Ausdruck a:= b*(-c)+b*(-c). DAGs stellen Ausdrücke kompakter dar als Syntaxbäume und sind sehr leicht aus Syntaxbäumen
zu erzeugen.
KAPITEL 4. COMPILER UND CODEGENERIERUNG
64
position := initial + rate * 60
lexikalische Analyse
id1 := id2 + id3*60
syntaktische Analyse
:=
id1
+
id2
*
id3
60
semantische Analyse
:=
Symboltabelle
1
position
...
2
initial
...
3
rate
...
...
...
...
id1
+
id2
*
id3
intToReal
60
Zwischencode-Erzeugung
temp1 := intToReal (60)
temp2 := id3 * temp1
temp3 := id2 + temp2
id1 := temp3
Code-Optimierung
temp1 := id3 * 60.0
id1 := id2 + temp1
Codegenerierung
MOVF
MULF
MOVF
ADDF
MOVF
id3, R2
#60.0, R2
id2, R1
R2, R1
R1, id1
Abbildung 4.5: Repräsentation des Quellprogrammes nach den verschiedenen Compilerphasen
3-Adress Code
Der 3-Adress Code ist eine Zwischensprache, die ein Programm durch eine Liste von
Anweisungen bzw. Instruktionen der Form
x := y op z
4.1. COMPILER – AUFBAU
65
Syntaxbaum
DAG
:=
:=
+
a
a
*
*
-
b
+
b
c
*
b
-
c
c
Abbildung 4.6: Die graphische Zwischendarstellungen Syntaxbaum und DAG
darstellt. Dabei sind x, y und z Namen von Variablen oder Konstanten des Programmes oder Namen von vom Compiler generierten temporären Variablen, und op ist ein
binärer arithmetischer oder logischer Operator. Der Name 3-Adress Code kommt daher,
dass jede Anweisung maximal drei Adressen und zwei Operatoren hat. Eine Adresse gehört zum Ergebnis der Anweisung, die anderen zwei Adressen gehören zu den
Operanden. Von den Operatoren ist einer der Zuweisungsoperator. Der obige 3-Adress
Befehl definiert die Variable x und verwendet (referenziert) y und z.
Beispiel 4.3 Für den Ausdruck
x + y * z
werden folgende 3-Adress Anweisungen generiert:
t1 := y * z
t2 := x + t1
Dabei sind t1 und t2 temporäre Zwischenvariablen.
Es gibt folgende Arten von 3-Adress Anweisungen:
• Zuweisungen
– x := y op z
op ist ein logischer oder arithmetischer Operator.
– x := op y
op ist ein logischer Operator oder ein Schiebeoperator.
– x := y
Das ist eine reine Kopieroperation.
• Kontrollflussanweisungen
KAPITEL 4. COMPILER UND CODEGENERIERUNG
66
– goto L
Diese Anweisung ist ein unbedingter Sprung zum Label L, der Adresse einer
weiteren 3-Adress Anweisung.
– if x relop y goto L
Diese Anweisung ist ein bedingter Sprung. Falls die Bedingung relop (=, ≤
, ≥, . . .) erfüllt ist, wird zum Label L gesprungen, anderenfalls wird mit der
folgenden 3-Adress Anweisung fortgefahren.
• Unterprogrammaufruf
Für den Aufruf von Unterprogrammen und die Parameterübergabe gibt es die
folgenden Anweisungen:
– param x
Diese Anweisung definiert einen Parameter x.
– call p, n
Das Unterprogramm p wird mit n Parametern, die vorher definiert werden
müssen, aufgerufen.
– return y
Mit dieser Anweisung können Unterprogramme einen Wert y an das aufrufende Programm zurückgeben.
• Indizierte Adressierung
– x := y[i]
– x[i] := y
• Pointeranweisungen
x ist ein Pointer und y eine Variable.
– x := &y
Zuweisung der Adresse der Variablen y an den Pointer x.
– y := *x
Zuweisung des Wertes, auf dessen Adresse der Pointer x zeigt, an die Variable
y.
– *x := y
Zuweisung des Wertes y an die Variable, auf deren Adresse der Pointer x
zeigt.
Beispiel 4.4 Die folgenden beiden 3-Adress Codesequenzen ergeben sich durch die Zwischencodegenerierung aus dem Syntaxbaum und dem DAG in Abb. 4.6:
/* Syntaxbaum */
t1 := -c
t2 := b * t1
t3 := -c
t4 := b * t3
t5 := t2 + t4
a := t5
/* DAG */
t1 := -c
t2 := b * t1
t5 := t2 + t2
a := t5
4.1. COMPILER – AUFBAU
67
Die Zwischencodedarstellung mittels 3-Adress Code bietet folgende Vorteile:
• Lange arithmetische Ausdrücke und geschachtelte Kontrollflussanweisungen werden in Operationen mit zwei Operanden aufgelöst. Dies ist für die spätere Zielcodegenerierung vorteilhaft, da Prozessoren i.allg. Instruktionen von ähnlicher
Mächtigkeit besitzen.
• Die Vergabe von Zwischennamen (temporäre Variablen) erlaubt eine leichtere Umordnung von Anweisungen in der Codeoptimierungsphase.
• Der 3-Adress Code ist eine linearisierte Darstellung eines Syntaxbaums bzw. DAGs,
in der die Zwischennamen den inneren Knoten der Graphen entsprechen. Eine
Liste von 3-Adress Anweisungen stellt bereits einen gültigen Ablaufplan dar.
4.1.4
Grundblöcke und Kontrollflussgraphen
Definition 4.1 Ein Grundblock (basic block) ist eine Folge fortlaufender Anweisungen, in
die der Kontrollfluss am Anfang eintritt und die er am Ende verlässt, ohne dass er dazwischen
anhält oder – ausser am Ende – verzweigt.
Die Einteilung einer Sequenz von 3-Adress Befehlen in Grundblöcke kann mit folgendem Algorithmus durchgeführt werden, dessen Ausgabe eine Liste von Grundblöcken ist, wobei jeder 3-Adress Befehl in genau einem Grundblock enthalten ist:
1. Bestimmung der Menge von Blockanfängen:
(a) Der erste Befehl der Eingangssequenz ist ein Blockanfang.
(b) Jeder Befehl, der Ziel eines bedingten oder unbedingten Sprungs ist, ist ein
Blockanfang.
(c) Jeder Befehl, der direkt auf einen bedingten oder unbedingten Sprung folgt,
ist ein Blockanfang.
2. Bestimmung der Grundblöcke:
Zu jedem Blockanfang gehört ein Grundblock. Ein Grundblock besteht aus dem
Blockanfang selbst und aus allen folgenden 3-Adress Befehlen bis zum nächsten
Blockanfang (exklusive diesem) oder bis zum Ende des Programms.
So wie ein einzelner Ausdruck lässt sich auch ein ganzer Grundblock durch einen
DAG darstellen. DAGs von Grundblöcken zeigen, wie die von den Befehlen im Grundblock berechneten Werte in den nachfolgenden Befehlen benutzt werden. DAGs modellieren gemeinsame Teilausdrücke und werden gerne als Darstellungsform für die
Implementierung von Transformationen auf Grundblöcken eingesetzt (Optimierung).
Definition 4.2 (DAG eines Grundblocks) Ein DAG eines Grundblocks ist ein gerichteter
azyklischer Graph mit folgender Knotenmarkierung:
• Die Blätter werden durch eindeutige Bezeichner markiert, die Variablennamen oder Konstanten darstellen. Stellen die Blätter Initialwerte für Variablen dar, werden die Namen
mit dem Index 0 versehen.
KAPITEL 4. COMPILER UND CODEGENERIERUNG
68
• Die inneren Knoten sind mit einem Operatorsymbol markiert. Aus dem Operator, der auf
eine Variable angewandt wird, kann man bestimmen, ob die Adresse oder der Wert der
Variablen benötigt wird.
• Optional kann einem Knoten auch eine Sequenz von Bezeichnern zugewiesen werden. Das
bedeutet, dass alle Bezeichner den berechneten Wert erhalten.
Beispiel 4.5 Folgendes Programm berechnet das Skalarprodukt zweier Vektoren a und b mit je 20 Elementen:
int i, prod, a[20], b[20];
...
prod = 0; i = 0;
do{
prod = prod + a[i] * b[i];
i++;
} while(i<=19);
Der 3-Adress Code für dieses Programm sieht folgendermassen aus:
(1)
(2)
(3)
(4)
(5)
(6)
(7)
(8)
(9)
(10)
(11)
(12)
/* Grundblock 1 */
prod := 0
i := 0
/* Grundblock 2 */
t1 := 4 * i
t2 := a[t1]
t3 := 4 * i
t4 := b[t3]
t5 := t2 * t4
t6 := prod + t5
prod := t6
t7 := i + 1
i := t7
if i<=19 goto (3)
Dieser 3-Adress Code setzt eine Zielarchitektur voraus, die Byte-adressierbar ist und bei der 4 Bytes ein
Maschinenwort (Integer) bilden. Deshalb wird in den Anweisungen (3) und (5) die (Wort-)Adresse mit 4
multipliziert. In diesem 3-Adress Code ist nach Regel 1(a) die Anweisung (1) ein Blockanfang. Das gleiche
gilt nach Regel 1(b) für die Anweisung (3). Nach Regel 1(c) ist die dem Befehl (12) folgende Anweisung
ebenfalls ein Blockanfang. Deshalb bilden die Anweisungen (1) und (2) den ersten Grundblock und die
Anweisungen (3)-(12) den zweiten Grundblock. Die Abb. 4.7 zeigt den DAG für den Grundblock 2.
Durch Hinzufügen von Kontrollflussinformation zur Menge der Grundblöcke eines Programms erhält man einen gerichteten Graphen, der ein Kontrollflussgraph ist.
Manchmal wird dieser Graph auch als entarteter Kontrollflussgraph bezeichnet, da im
Gegensatz zu einem reinen Kontrollflussgraphen ein ganzer Grundblock zu einem Knoten im Graph reduziert wird. Es exisitiert eine gerichtete Kante vom Grundblock (Knoten) B1 zum Grundblock (Knoten) B2, falls B2 in einer der möglichen Ausführungssequenzen direkt nach B1 folgen kann. Dies ist der Fall, wenn es einen bedingten oder
4.1. COMPILER – AUFBAU
69
t6, prod
+
prod
t5
*
0
[]
a
t4
t2
[]
<=
t1, t3
b
*
+
1
i0
4
19
t7, i
Abbildung 4.7: DAG zur Berechnung des Skalaproduktes
unbedingten Sprung von der letzten Anweisung in B1 zur ersten Anweisung in B2 gibt,
oder B2 im Programm direkt nach B1 folgt und B1 am Ende keinen Sprung enthält. In
diesem Fall ist B1 Vorgänger von B2, und B2 ist der Nachfolger von B1.
B1
prod := 0
i :=0
L:
t1 := 4 * i
B2
t2 := a[t1]
t3 := 4 * i
t4 := b[t3]
t5 := t2 * t4
t6 := prod + t5
prod := t6i
t7 := i + 1
i := t7
if i <= 19 goto L
Abbildung 4.8: Kontrollflussgraph mit Grundblöcken als Knoten
Beispiel 4.6 Abb. 4.8 zeigt den Kontrollflussgraphen zum Beispiel der Berechnung des Skalarprodukts,
in dem Knotenmengen, die Grundblöcke darstellen, zu jeweils einem Knoten zusammengefasst sind. Die
KAPITEL 4. COMPILER UND CODEGENERIERUNG
70
Bedingungen für die verschienden Kontrollflusspfade sind nicht dargestellt.
4.2
Codegenerierung
In Abb. 4.3 ist die Codegenerierung als letzte Phase eines Compilers dargestellt. Der Codegenerator liest die (eventuell optimierte) Zwischendarstellung des Quellprogramms
und gibt das Zielprogramm aus. Codeoptimierungen werden am Zwischencode und
auch nach dem Codegenerator vorgenommen. Die im folgenden vorgestellten Techniken zur Codegenerierung sind unabhängig davon, ob zuerst eine Optimierungsphase
durchgeführt wurde oder nicht.
Anforderungen an die Codegenerierung sind:
• Erzeugung von korrektem Code. Dies ist die wichtigste Anforderung.
• Erzeugung von effizientem Code, d.h., die Ressourcen der Zielmaschine sollen möglichst gut ausgenutzt werden.
• Eine effiziente Codegenerierung, d.h., der Übersetzungsvorgang soll rasch ablaufen.
Die Codegenerierung ist ein Syntheseproblem, das wie alle Syntheseaufgaben in die
drei Teilaufgaben Allokation, Bindung und Ablaufplanung unterteilt werden kann.
• Die Allokation ist in der Softwaresynthese ein eher untergeordnetes Problem, da
die Komponenten, wie Prozessor, Anzahl der Register, Anzahl und Grösse der
Speicher, etc., in den meisten Fällen vorgegeben sind.
• Die Bindung in der Softwaresynthese bezeichnet die Abbildung von Namen auf
Register und Speicheradressen. Zwei wichtige Teilprobleme dabei sind die Registervergabe und die Registerzuweisung. Die Registervergabe wählt die Variablen
aus, die an Register gebunden werden sollen. Die Registerzuweisung bestimmt
aus dieser Auswahl diejenigen Variablen, die an ein bestimmtes Register gebunden werden. Ein weiteres Bindungsproblem ist die Befehlsauswahl. Oft kann eine Anweisung des Zwischencodes durch mehrere verschiedene Instruktionen der
Zielarchitektur implementiert werden.
• Die Ablaufplanung ist die Berechnung einer Instruktionsreihenfolge.
Im folgenden werden die Problemstellungen der Bindung und Ablaufplanung anhand von Beispielen erläutert.
Registerbindung Befehle, die Registeroperanden enthalten, sind i.allg. kürzer und
schneller ausführbar als Befehle mit Speicheroperanden. Eine effiziente Ausnutzung der
vorhandenen Register ist ein wichtiger Faktor für die Codequalität. Die Verwendung
von Registern wird oft in zwei Teilprobleme zerlegt:
• Registervergabe: Für jeden Punkt im Programm wird die Menge der Variablen
bestimmt, die in Registern gehalten werden sollen.
4.2. CODEGENERIERUNG
71
• Registerzuweisung: Jeder dieser Variablen wird ein bestimmtes Register zugeteilt.
Das Problem der Registerzuweisung ist NP-vollständig. In der Praxis werden die
Registervergabe und -zuweisung noch durch bestimmte Vorgaben der Prozessorarchitektur (bestimmte Adressierungsarten benötigen bestimme Register) und der Laufzeitumgebung (Konventionen zur Parameterübergabe in Registern) erschwert.
Befehlauswahl Für jede 3-Adress Anweisung wird ein Codemuster angegeben, das
zeigt, aus welchen Instruktionen der Zielcode für diese 3-Adress Anweisung aufgebaut
ist.
Beispiel 4.7 Für die 3-Adress Anweisung
x := y + z
könnte der generierte Code wie folgt aussehen:
MOV y,R0 /* lade y in Reg. R0 */
ADD z,R0 /* addiere z zu R0 */
MOV R0,x /* speichere R0 nach x */
Diese Art der Befehlsauswahl, die für jeden 3-Adress Befehl ein in einer Bibliothek gespeichertes Codemuster einsetzt, führt oft zu ineffizientem Code, wie folgendes Beispiel zeigt:
a := b + c
d := a + e
MOV
ADD
MOV
MOV
ADD
MOV
b,R0
c,R0
R0,a
a,R0
e,R0
R0,d
Die vierte Instruktion ist überflüssig, da der Wert von a bereits im Register R0 steht. Das gleiche gilt für
den dritten Befehl, falls a nicht noch später verwendet wird.
Bei Prozessoren mit einem reichhaltigen Instruktionssatz gibt es meist viele Möglichkeiten, wie man eine 3-Adress Anweisung implementieren kann. Die Möglichkeiten unterscheiden sich i.allg. in der Anzahl von Instruktionszyklen und im Speicheraufwand.
Oft sind diese Parameter vom Kontext, in dem ein Befehl verwendet wird, abhängig.
Ablaufplanung Bestimmte Ausführungsreihenfolgen benötigen weniger Register zur
Aufnahme von Zwischenergebnissen als andere. Die Bestimmung der Befehlsreihenfolge mit der kürzesten Gesamtausführungszeit (oder alternativ mit der geringsten Codelänge) ist für den allgemeinen Fall auch ein NP-vollständiges Problem.
Beispiel 4.8 Dieses Beispiel zeigt, wie die Reihenfolge, in der die Berechnungen ausgeführt werden, den
resultierenden Zielcode beeinflusst. Gegeben ist der DAG in Abb. 4.9, der die Anweisung
(a + b) - (e - (c + d))
KAPITEL 4. COMPILER UND CODEGENERIERUNG
72
t4
t1
t3
-
+
t2
a
b
e
+
c
d
Abbildung 4.9: DAG für den Grundblock in Beispiel 4.8
modelliert. Die folgenden zwei 3-Adress Codesequenzen stellen unterschiedliche Ablaufpläne für diesen
DAG dar:
/*
t2
t3
t1
t4
Version a */
:= c + d
:= e - t2
:= a + b
:= t1 - t3
/*
t1
t2
t3
t4
Version b */
:= a + b
:= c + d
:= e - t2
:= t1 - t3
Unter der Annahme, dass es nur zwei Register R0 und R1 gibt und dass nach dem Codestück die Variable
t4 noch aktiv sein muss, ergeben sich folgende Zielcodes:
/* Version a */
MOV c,R0
ADD d,R0
MOV e,R1
SUB R0,R1
MOV a,R0
ADD b,R0
SUB R1,R0
MOV R0,t4
4.2.1
/* Version b */
MOV a,R0
ADD b,R0
MOV c,R1
ADD d,R1
MOV R0,t1
MOV e,R0
SUB R1,R0
MOV t1,R1
SUB R0,R1
MOV R1,t4
Modellmaschine
Für die Codegenerierung muss man die Zielarchitektur kennen. Die folgenden Abschnitte stellen ein Modell einer Zielarchitektur vor, das eine relativ grosse Klasse von
Prozessoren abdeckt. Die Zielarchitektur ist eine Byte-adressierbare Maschine, wobei 4
Bytes ein Maschinenwort bilden, mit n general-purpose Registern R0, . . . , Rn-1. Die
Zielarchitektur hat 2-Adress Instruktionen der Form
op
source, destination
wobei op einen Operationscode, und source und destination Operandenfelder darstellen. Bei binären Operationen stehen die Operanden in source und destination, das
4.2. CODEGENERIERUNG
73
Ergebnis wird nach destination geschrieben. Jede ausgeführte Instruktion verursacht
Kosten von einer Kosteneinheit. Falls Instruktionen Adressen von Speicherstellen benötigen, befinden sich diese Adressen in den folgenden Instruktionswörtern. Tabelle 4.1
listet die möglichen Adressierungsarten auf. Dabei gibt die contents(x) den Inhalt des
Registers oder der Speicherstelle x an.
mode
form
address
added cost
absolute
register
indexed
indirect register
indirect indexed
immediate
M
R
c(R)
*R
*c(R)
#c
M
R
c + contents(R)
contents(R)
contents(c + contents(R))
c
1
0
1
0
1
1
Tabelle 4.1: Adressierungsarten der Modellmaschine
Die Tabelle gibt auch die Zusatzkosten für jede Adressierungsart an. Diese Kosten
sind die Anzahl der Worte, die zusätzlich zur Instruktion noch gelesen werden müssen,
um die Adresse zu berechnen. Adressierungsarten, die auf Werte in Registern zugreifen,
haben keine zusätzliche Kosten (register, indirect register). Beispiele für Instruktionen
der Zielmaschine sind:
MOV
RO, a
ADD
R2, R1
SUB
*R2, *R1
MOV
*4(R3), b
Beispiele für Codesequenzen mit indizierten Anweisungen sind in Tabelle 4.2, für
Pointer-Anweisungen in Tabelle 4.3 dargestellt. Diese Anweisungen werden wie binäre
Operationen behandelt. Die Codesequenz wird dabei durch den Index i oder einen
Pointer bestimmt. In diesen Tabellen werden verschiedene Fälle unterschieden, je nachdem ob sich i oder der Pointer in einem Register Ri, in einem Speicherplatz Mi oder
im Stack mit der relativen Adresse Si befindet. Ein spezielles Register A zeigt auf den
Beginn des Stackbereichs einer Prozedur.
Für bedingte Sprünge wird ein Bedingungscode verwendet. Nach jedem berechneten oder in ein Register geladenen Wert wird der Bedingungscode auf negativ, null oder
positiv gesetzt. Die Instruktion CMP x, y setzt den Bedingungscode, ohne das Ergebnis in ein Register oder eine Speicherstelle abzulegen. Das folgende Beispiel zeigt die
Codesequenz für die 3-Adress Abweisung if x<y goto z:
CMP
JLE
x,y
z
KAPITEL 4. COMPILER UND CODEGENERIERUNG
74
statement
i in register Ri
code
cost
a := b[i]
MOV b(Ri),R
2
a[i] := b
MOV b,a(Ri)
3
i in memory Mi
code
cost
MOV
MOV
MOV
MOV
Mi,R
b(R),R
Mi,R
b,a(R)
4
5
i in stack
code
MOV
MOV
MOV
MOV
cost
Si(A),R
b(R),R
Si(A),R
b,a(R)
4
5
Tabelle 4.2: Beispiele für Codesequenzen mit indizierten Anweisungen
statement
p in register Rp
code
cost
a := *p
MOV *Rp,a
2
*p := a
MOV a,*Rp
2
p in memory Mp
code
cost
MOV
MOV
MOV
MOV
Mp,R
*R,R
Mp,R
a,*R
3
4
p in stack
code
MOV
MOV
MOV
MOV
cost
SP(A),R
*R,R
a,R
R,*SP(A)
3
4
Tabelle 4.3: Beispiele für Codesequenzen mit Pointeranweisungen
4.2.2
Einfacher Codegenerator
Definition 4.3 Ein Name (Variable) in einem Grundblock ist an einem gegebenen Punkt aktiv,
falls sein Wert nach diesem Punkt im Programm verwendet wird (möglicherweise auch in einem
anderen Grundblock). Dagegen ist ein Name an einem gegebenen Punkt passiv, falls sein Wert
nach diesem Punkt nicht mehr verwendet wird.
Ein Name muss nur in einem Register gehalten werden, falls er aktiv ist. Andernfalls
kann das Register einem anderen Namen zugewiesen werden.
Definition 4.4 (Verwendung von Variablen) Der Befehl I (Befehl mit dem Label I) weise
der Variablen x einen Wert zu. Wenn x ein Operand des Befehls J (Befehl mit dem Label J) ist
und es einen Pfad im Kontrollflussgraphen von I nach J gibt, auf dem x keine neue Zuweisung
erhält, dann verwendet J den bei I berechneten Wert von x.
Die Lebenszeiten der Variablen in einem Grundblock lassen sich mit folgendem Algorithmus bestimmen:
1. Gehe bis zum Ende des Grundblocks und notiere für jeden Namen x in der Symboltabelle, ob x beim Verlassen des Grundblocks aktiv sein muss (dies ist aus einer
globalen Datenflussanalyse bekannt).
2. Gehe Befehl für Befehl zum Anfang des Grundblocks zurück. Für jeden Befehl
I: x := y op z werden die folgenden Aktionen durchgeführt:
(a) An den Befehl I werden die Informationen gebunden, die momentan in der
Symboltabelle über die nächste Verwendung von x, y und z stehen. Ist x nicht
aktiv, so kann dieser Befehl entfernt werden.
(b) x wird in der Symboltabelle auf nicht aktiv und keine nächste Verwendung
gesetzt.
4.2. CODEGENERIERUNG
75
(c) y und z werden auf aktiv und deren nächste Verwendung auf I gesetzt.
Im folgenden wird ein einfacher Codegenerator vorgestellt, der Zielcode erzeugt,
indem jeder 3-Adress Befehl der Reihenfolge nach behandelt wird. Bei jedem neuen Befehl wird berücksichtigt, ob die Operanden schon in Registern stehen. Die berechneten
Ausdrücke werden so lange wie möglich in den Registern gehalten. Nur in zwei Fällen
müssen Werte wieder in den Speicher transferiert werden: i) wenn das Register für eine
andere Berechnung gebraucht wird, oder ii) vor einem Unterprogrammaufruf, einem
Sprung oder einer Anweisung mit einer Sprungmarke. Im zweiten Fall wird ein neuer
Grundblock begonnen. In dieser einfachen Codegenerierungsstrategie wird angenommen, dass beim Betreten eines Grundblocks alle Variablen im Speicher stehen und beim
Verlassen des Grundblocks wieder alle Variablen in den Speicher geschrieben werden
müssen.
Der Codegenerator benutzt zwei Deskriptoren (descriptors), einen RegisterDeskriptor und einen Adress-Deskriptor. Der Register-Deskriptor ist eine Datenstruktur, die eine Liste aller Namen, die sich zu einem bestimmten Zeitpunkt in Registern
befinden, verwaltet. Am Anfang sind alle Register ohne Namen. Im Laufe der Codegenerierung können auch mehrere Namen einem Register zugeordnet sein. Der AdressDeskriptor ist eine Datenstruktur, die eine Liste aller Stellen verwaltet, an denen sich
die aktuellen Werte der Variablen befinden. Diese Stellen können Register, Positionen
im Stack, Speicheradressen, oder eine Kombination dieser Stellen sein.
Für eine Sequenz von 3-Adress Befehlen, die einen Grundblock bilden, wird Code
generiert, indem für jeden 3-Adress Befehl x := y op z folgende Schritte durchlaufen
werden:
1. Bestimme durch Aufruf der Funktion getreg() die Stelle L, in der das Ergebnis der
Operation y op z abgelegt werden soll. L wird meist ein Register, kann aber auch
eine Speicherstelle sein. Die Funktion getreg() wird im Anschluss erläutert.
2. Bestimme über den Adressdeskriptor y’ die aktuelle Stelle von y. Falls sich y
gleichzeitig in einem Register und im Speicher befindet, bevorzuge das Register.
Falls der Wert von y nicht schon an der Stelle L steht, generiere die Instruktion
<MOV y’, L>, um eine Kopie von y in L zu erzeugen.
3. Generiere die Instruktion <op z’, L>, wobei z’ die aktuelle Stelle von z ist. Falls
z gleichzeitig in einem Register und im Speicher steht, bevorzuge das Register.
Aktualisiere den Adressdeskriptor von x mit der Information, dass der aktuelle
Wert von x nun an der Stelle L steht. Wenn L ein Register ist, aktualisiere auch den
Registerdeskriptor.
4. Wenn die aktuellen Werte von y und/oder z keine nächste Verwendung haben, am
Ende des Grundblocks nicht aktiv sein müssen und sich in Registern befinden, ändere die Deskriptoren, um anzuzeigen, dass nach der Ausführung der Instruktion
diese Register nicht mehr die Namen y bzw. z halten.
Falls der betrachtete 3-Adress Befehl einen unären Operator verwendet, werden obige Schritte in analoger Weise durchlaufen. Ein Spezialfall ist der Kopierbefehl x :=
y. Wenn y in einem Register steht, wird keine Instruktion generiert, sondern nur die
KAPITEL 4. COMPILER UND CODEGENERIERUNG
76
Register- und Adressdeskriptoren geändert, die nun anzeigen, dass der aktuelle Wert
von x im Register (das schon y hält) zu finden ist. Wenn y keine nächste Verwendung
hat und am Ende des Grundblocks nicht aktiv sein muss, ändere die Deskriptoren, so
dass y nicht länger im Register ist. Falls y nicht in einem Register steht, wird durch
Aufruf der Funktion getreg() ein Register ausgesucht, in das y geladen wird und das
nun auch zur aktuellen Stelle für x wird.
Nachdem alle 3-Adress Befehle bearbeitet wurden, werden <MOV R, M> Instruktionen generiert, um alle Variablen, die am Ende des Grundblocks aktiv sein müssen und
deren aktuelle Werte sich in Registern befinden, in den Speicher zu transferieren.
Die Funktion getreg() gibt für eine Instruktion der Form x := y op z ein Register
zurück, in das der Wert x abgelegt werden soll. Für die Implementierung dieser zentralen Funktion wird eine einfache Möglichkeit vorgestellt:
1. Wenn y in einem Register steht, das sonst keine Variablen hält (keine Kopien von
y), und nicht aktiv ist, d.h. keine nächste Verwendung hat, dann wähle das Register
als Ziel.
2. Sonst: Wähle ein leeres Register als Ziel, falls eines existiert.
3. Sonst: Falls x eine nächste Verwendung im Grundblock hat oder op ein Operator
ist, der ein Register benötigt (z.Bsp. Indizierung), finde ein belegtes Register R.
Speichere den Wert von R in eine Speicherstelle (<MOV R, M>), falls dieser Wert
nicht schon in einer Speicherstelle ist, ändere den Adressdeskriptor für M und gib
das Register R als Ziel zurück. Falls R die Werte mehrerer Variablen hält, wird
eine MOV Instruktion für jede Variable generiert, die in den Speicher geschrieben
werden muss. Ein oft benutzte Strategie zur Wahl eines Registers R ist, ein Register
zu wählen, dessen Name erst wieder sehr weit in der Zukunft benutzt wird.
4. Sonst: Wähle die Speicherstelle von x als Ziel.
4.2.3
Registerbindung
Anweisungen, die nur Registeroperanden benötigen, sind kürzer und schneller als solche mit Speicheroperanden. Die im vorigen Abschnitt verwendete Funktion getreg() hat
versucht, wenn immer möglich, Register für die Operanden zu wählen. Für die Registervergabe und Registerzuweisung gibt es darüber hinaus eine Reihe von Strategien und
Algorithmen.
Registerzuweisung durch Graphfärbung
Wenn alle Register belegt sind und ein Register benötigt wird, muss der Inhalt eines belegten Registers in den Speicher transferiert werden (register spill). Die Graphfärbung ist
eine einfache und systematische Technik zur Registerzuweisung und zur Minimierung
von register spills. Diese Technik erfordert zwei Durchläufe. In einem ersten Durchlauf
wird eine Instruktionsfolge des Zielcodes unter der Annahme generiert, dass es beliebig
viele symbolische Register gibt. Das bedeutet, dass jede Variable und Zwischenvariable
ein Register zugewiesen bekommt. Das Problem der Registerbindung besteht nun darin,
4.2. CODEGENERIERUNG
77
den Variablen physikalische Register zuzuweisen und dabei die Anzahl von Registerabwürfen (register spills) zu minimieren. Dieses Problem lässt sich als Knotenfärbungsproblem eines Graphen, des sogenannten Registerkonfliktgraphen, formulieren.
Definition 4.5 (Registerkonfliktgraph) Ein Registerkonfliktgraph Gk (Vk , Ek ) ist ein ungerichteter Graph, in dem die Knotenmenge Vk symbolische Register darstellt. Die Kantenmenge Ek = {(vi , v j ) : vi 6∼ v j ; vi , v j ∈ Vk } drückt die Konfliktrelationen der Register
aus. vi 6∼ v j bedeutet, dass vi an einem Punkt aktiv ist, an dem v j definiert wird.
Beispiel 4.9 Gegeben sei folgendes Programm, das den Rumpf einer Schleife darstellt. Am Ausgang der
Schleife sollen t3 und t4 aktiv sein.
t1
t2
t3
t4
:=
:=
:=
:=
t3
t4
t1
t2
*
*
+
+
10
20
5
t3
Die Aktivitätsintervalle (Lebensdauern) der Variablen definieren die Konflikte und sind in Abb. 4.10
dargestellt. Der daraus resultierende Konfliktgraph ist in Abb. 4.11 dargestellt. In dem Graph existiert je
eine Kante zwischen zwei Knoten (Variablen), deren Aktivitätsintervalle sich schneiden.
1 Periode
t1
t2
t3
t4
i=1 i=2 i=3 i=4
Abbildung 4.10: Definitionszeitpunkte und Aktivitätsintervalle der Variablen in Beispiel
4.9
t1
t2
t1
t4
t2
t4
t3
t3
a) Konfliktgraph
..
b) Farbung
mit zwei Farben
Abbildung 4.11: Registerkonfliktgraph zu Beispiel 4.9
Bei der Graphfärbung wird versucht, die Knoten des Registerkonfliktgraphen mit
l Farben derart einzufärben, dass Knoten, die mit einer Kante verbunden sind, nicht
KAPITEL 4. COMPILER UND CODEGENERIERUNG
78
die gleiche Farbe bekommen. Der Parameter l entspricht dabei der Anzahl der verfügbaren Register. Das Entscheidungsproblem, ob ein gegebener Graph l-färbbar ist, ist
NP-vollständig.
Für den einfachen Fall l = 2 entspricht das Knotenfärbungsproblem einer Überprüfung, ob der Graph bipartit ist. Dieses einfache Problem lässt sich in linearer Zeit lösen.
Für Fälle mit l > 2 wird häufig folgende Heuristik verwendet:
1. Finde einen Knoten vi in Gk mit deg(vi ) < l, d.h., einen Knoten mit weniger als l
Nachbarknoten.
2. Entferne vi und alle Kanten, die von vi ausgehen. Dadurch wird ein neuer Graph
0
Gk erzeugt. (Wenn es möglich ist, den neuen Graph mit l Farben zu färben, kann
auch Gk gefärbt werden, indem man vi mit der Farbe färbt, die keinem seiner
Nachbarn zugewiesen wurde.)
3.
0
(a) Falls der Graph Gk leer ist, ist eine l-Färbung möglich. Die Farben für die
Knoten erhält man, indem man die erzeugten Graphen Schritt für Schritt
bis zum Ausgangsgraphen zurückgeht und jedem neuen Knoten eine Farbe
zuordnet, die keiner seiner Nachbarn bereits hat .
(b) Falls es nur noch Knoten mit ≥ l Nachbarknoten gibt, ist eine l-Färbung
nicht möglich. In diesem Fall werden Instruktionen zum Speichern und Zurückladen eines Registers generiert, der Registerkonfliktgraph geändert und
die Heuristik erneut angewendet.
0
(c) Andernfalls setze Gk = Gk und gehe zu 1.
Beispiel 4.10 Der Konfliktgraph in Abb. 4.11 kann mit der vorgestellten Heuristik nicht mit k = 2
Farben gefärbt werden (obwohl es, wie Abb. 4.11b zeigt, möglich ist). In der dargestellten Lösung können
sich die Variablen t1 und t2 das Register R0 teilen, weil sich ihre Lebenszeiten nicht überlappen. Das
gleiche gilt für die Variablen t2 und t4, die sich das Register R1 teilen.
Globale Registerzuweisung
Der Codegenerierungsalgorithmus im vorherigen Abschnitt hat am Ende eines Grundblocks alle aktiven Variablen in den Speicher geschrieben. Eine globale Registerzuweisungstrategie hält häufig verwendete Variablen über Grundblockgrenzen hinweg in Registern. Nachdem Programme die meiste Zeit in inneren Schleifen verbringen, kann
man einige der verfügbaren Register für Variablen der inneren Schleifen, eine andere
Menge von Registern für global gehaltene Variablen und den Rest für die Zuordnung
weiterer lokaler Variablen reservieren. Ein Nachteil dieser Strategie ist, dass die feste
Reservierung einer Anzahl von Registern nicht in allen Fällen passend ist. In manchen
Programmiersprachen kann der Programmierer auch definieren, welche Variablen nach
Möglichkeit in Registern zu halten sind. In C geschieht dies z.Bsp. durch Programmkonstrukte wie
register int i;
4.2. CODEGENERIERUNG
79
Verwendungszähler
Definition 4.6 (Schleife) Eine Schleife L ist ein gerichteter Zyklus im Kontrollflussgraphen
mit folgenden Eigenschaften:
1. Alle in der Schleife enthaltenen Knoten sind streng verbunden; d.h., von jedem Knoten
innerhalb der Schleife gibt es zu jedem anderen Knoten der Schleife einen Pfad, der ganz
in der Schleife liegt.
2. Es gibt einen eindeutigen Eingang in die Schleife. Dies bedeutet, dass jeder Weg zu einem
Knoten der Schleife von genau einem Eingang her führt.
Nach dem Maschinenmodell vom vorherigen Abschnitt ergibt sich für jede Verwendung einer Variablen x eine Kosteneinsparung von 1, falls sich x bereits in einem Register befindet. Eine weitere Kosteneinsparung von 2 ergibt sich, wenn man x am Ende
eines Blocks nicht speichern muss. Beim Eingangsblock einer Schleife fallen Kosten in
der Höhe von 2 an, falls x am Anfang der Schleife aktiv ist. Weitere Kosten von 2 fallen
für jeden Ausgangsblock der Schleife an, wenn x nach der Schleife aktiv sein muss. Diese Kosten fallen aber nur jeweils einmal an, während die Kostenersparnisse für jeden
Schleifendurchlauf erhalten werden.
Damit lässt sich folgende Näherungsformel für die Kosteneinsparung bei Zuweisung
eines Registers an x innerhalb einer Schleife L formulieren:
∑
(verwendet( x, B) + 2 · aktiv( x, B))
Bl öcke B∈ L
Dabei bezeichnet verwendet( x, B) die Anzahl der Verwendungen von x im Block B,
die vor einer möglichen Zuweisung an x auftreten. Wenn x im Block eine Zuweisung
erhält, zählt man nur die Verwendungen vor dieser Zuweisung, da man annimmt, dass
nach der Zuweisung x ohnehin weiter in einem Register gehalten wird, was dann keine
weitere Einsparungen liefert. Diese Annahme gilt nur für die Art der Codegenerierung,
wie sie im vorherigen Abschnitt vorgestellt wurde. Die Funktion aktiv( x, B) ist 1, falls
x am Ausgang von B aktiv ist und in B einen Wert zugewiesen bekommen hat, sonst
ist aktiv( x, B) = 0. Diese Formel ist eine Näherung, da nicht alle Blöcke innerhalb einer
Schleife mit der gleichen Häufigkeit ausgeführt werden und man annimmt, dass die
Schleife oft iteriert.
Beispiel 4.11 Abb. 4.12 zeigt einen Kontrollflussgraphen für eine Schleife, die aus vier Grundblöcken
besteht. Die Sprünge wurden der Einfachheit halber entfernt. In die Abbildung sind die Mengen der
aktiven Namen zu Beginn und zum Ende jedes Blockes eingezeichnet. Es stehen die Register R0, R1 und
R2 für die Aufnahme von Werten innerhalb der Schleife zur Verfügung.
Die Variable a ist am Ausgang von B1 aktiv und hat in B1 einen Wert erhalten. Am Ausgang von B2, B3
und B4 ist a nicht aktiv.
∑ 2 · aktiv(a, B) = 2
B∈ L
Die Verwendungszähler sind
verwendet( a, B1) = 0, verwendet( a, B2) = verwendet( a, B3) = 1, verwendet( a, B4) = 0
KAPITEL 4. COMPILER UND CODEGENERIERUNG
80
bcdf
a := b+c
d := d-b
e := a+f
B1
acdef
acde
acdf
f := a-d
B2
b := d+f
e := a-c
cdef
cdef
bcdef
b := d+c
bcdef
B3
B4
bdef
bdef
Abbildung 4.12: Kontrollflussgraph zu Beispiel 4.11
Damit ergibt sich:
∑ verwendet(a, B) = 2
B∈ L
Es würden vier Kosteneinheiten eingespart, wenn man für a ein globales Register auswählt. Die Kosteneinsparungswerte für die restlichen Variablen b, c, d, e, f sind 6, 3, 6, 4, 4. Als Konsequenz daraus
wird man die drei Variablen a, b, d für eine Registerzuweisung auswählen. In der abschliessenden Registerbindung wird z.Bsp. die Variable a dem Register R0, die Variable b dem Register R1 und die Variable
d dem Register R2 zugewiesen. Der dann generierte Code ist:
/* B1 */
MOV R1,R0
ADD c,R0
SUB R1,R2
MOV R0,R3
ADD f,R3
MOV R3,e
4.2.4
/* B2 */
MOV R0,R3
SUB R2,R3
MOV R3,f
/* B3 */
MOV R2,R1
ADD f,R1
MOV R0,R3
SUB c,R3
MOV R3,e
/* B4 */
MOV R2,R1
ADD c,R1
Codegenerierung für DAGs
Der Vorteil der Codegenerierung ausgehend von einer DAG-Repräsentation eines
Grundblocks ist, dass ein DAG im Gegensatz zum 3-Adress Code nicht schon eine bestimmte Ausführungsreihenfolge darstellt. Somit ist es einfacher, Berechnungen von Teilausdrücken umzuordnen. In Beispiel 4.8 wurde gezeigt, dass er mehrere Möglichkeiten
gibt, Code von einem DAG zu generieren, abhängig davon, in welcher Reihenfolge man
die Knoten des DAGs betrachtet. Diese Beispiel zeigte, dass es vorteilhaft ist, einen Knoten eines DAGs unmittelbar nach der Betrachtung seines linken Kindes zu bearbeiten.
Aufbauend auf diese Beobachtung kann man folgende Heuristik für die Bestimmung
der umgedrehten Berechnungsreihenfolge der Knoten eines DAG angeben:
4.2. CODEGENERIERUNG
81
1. reihe die Wurzel des DAG
2. wähle einen Knoten n, dessen Eltern bereits gereiht wurden und reihe n
3. solange das am weitesten links liegende Kind m von n keine ungereihten Eltern
hat und kein Blatt ist:
(a) reihe m
(b) n ← m
(c) gehe zu 3.
4. gehe zu 2.
Beispiel 4.12 Für den DAG in Abb. 4.13 erzeugt diese Heuristik die Knotenreihung 1, 2, 3, 4, 5, 7, 6.
Der Codegenerator sollte also die Knoten dieses DAGs in der umgekehrten Reihenfolge 6, 7, 5, 4, 3, 2, 1
besuchen und folgenden Code generieren:
t8
t6
t5
t4
t3
t2
t1
:=
:=
:=
:=
:=
:=
:=
d + e
a + b
t6 - c
t5 * t8
t4 + e
t6 - t4
t2 * t3
1
*
2
3
−
+
4
*
6
5
−
+
8
7
c
9
d
10
e
+
11
a
12
b
Abbildung 4.13: DAG für Beispiel 4.12
Ist der DAG ein Baum, kann man für das vorgestellte Maschinenmodell einen in der
Anzahl der Knoten des DAGs linearen Algorithmus angeben, der optimalen Code generiert. Optimal bedeutet hier, dass der Code eine minimale Anzahl von Instruktionen
hat. Sobald ein DAG gemeinsame Teilausdrücke darstellt, ist er kein Baum mehr, und
KAPITEL 4. COMPILER UND CODEGENERIERUNG
82
das Codegenerierungsproblem wird ungleich schwieriger. Es wurde gezeigt, dass optimale Codegenerierung für allgemeine DAGs NP-vollständig ist. Eine in der Praxis gut
funktionierende Methode ist es, einen DAG so in mehrere Teile aufzuspalten, dass die
Teil-DAGs keine gemeinsamen Teilausdrücke mehr modellieren, und für jeden dieser
Teil-DAGs optimalen Code zu generieren.
4.2.5
Codegenerierung mit Dynamische Programmierung
Aufbauend auf den optimalen Codegenerierungsalgorithmus für baumartige DAGs
kann man ein Verfahren, das auf dem Prinzip der dynamischen Programmierung beruht, angeben, welches für eine erweiterte Klasse von Maschinenmodellen optimalen
Code für Bäume generiert.
Bei dem bisher betrachteten Maschinenmodell wurden die Ergebnisse der Berechnungen in Registern abgelegt, und alle Operatoren befanden sich in zwei Registern
oder in einem Register und einem Speicherplatz. Dieses Modell wird nun auf eine Klasse erweitert, die Maschinen mit komplexeren Instruktionen einschliesst:
Definition 4.7 (Zielmaschine) Gegeben sei ein Maschinenmodell mit r frei verfügbaren Registern R0, R1, . . ., Rr-1. Die Befehle sind von der Art Ri := E, wobei E ein beliebiger aus
Operatoren, Registern und Speicherplätzen bestehender Ausdruck sein kann. Falls E eines oder
mehrere Register enthält, so muss Ri eines dieser Register sein. Die Zielmaschine besitze auch
einen Load-Befehl Ri := M, ein Store-Befehl M := Ri und einen Registerkopierbefehl Ri := Rj.
Der Einfachheit halber wird angenommen, dass jeder Befehl die gleichen Kosten hat.
Beispiel 4.13 Dieses erweiterte Maschinenmodell schliesst auch das alte Modell mit ein. Die Befehle
des alten Maschinenmodells können einfach in Befehle des erweiterten Modells umgewandelt werden.
/* altes Modell */
ADD R0,R1
ADD *R0,R1
/* erweitertes Modell */
R1 := R1 + R0
R1 := R1 + ind R0
Dabei stellt ind R0 die Anwendung eines Operators (für indirekte Adressierung) auf R0 dar.
Nach dem Prinzip der dynamischen Programmierung wird ein Ausdruck E in zwei
Teilausdrücke aufgespalten:
E = E1 op E2
Der optimale Code für E wird generiert, in dem der optimalen Code für die Teilausdrücke E1 und E2 geeignet kombiniert (zuerst E1, dann E2 oder umgekehrt) und dann
der Code für den Operator op generiert wird. Der optimale Code für die Teilausdrücke
wird wiederum durch eine Aufspaltung in Teilausdrücke erzeugt.
Abb. 4.14 zeigt den Syntaxbaum T für den Ausdruck E. Code, der durch das beschriebene Verfahren generiert wird, hat die Eigenschaft, dass der Ausdruck E = E1 op
E2 benachbart (contiguously) ausgewertet wird.
4.2. CODEGENERIERUNG
83
op
T1
T2
Abbildung 4.14: Syntaxbaum T für E
Definition 4.8 Ein Programm (der generierte Code) wertet einen Baum T benachbart aus,
wenn es zuerst diejenigen Teilbäume von T berechnet, deren Werte im Speicher abgelegt werden
müssen. Anschliessend wird der Rest von T berechnet (in der Reihenfolge T1, T2 oder T2, T1)
und dann die Wurzel von T. Dabei werden die vorher in den Speicher abgelegten Werte der
Teilbäume verwendet.
Für das beschriebene Maschinenmodell kann man beweisen, dass es zu jeder Codesequenz P, die den Baum T auswertet, eine äquivalente Codesequenz P0 gibt, für die
gilt:
• P0 besitzt keine höheren Kosten als P
• P0 benutzt nicht mehr Register als P
• P0 wertet den Baum benachbart aus
Das bedeutet, wenn man den Baum T benachbart auswertet, erhält man ein optimales Programm (minimale Anzahl von Instruktionen und benötigen Registern). Das
auf dynamischer Programmierung beruhende Verfahren zur Codegenerierung für einen
Ausdrück T, der ein Baum ist, besitzt drei Phasen.
1. Phase: Kostenberechnung Gehe in T von unten nach oben (bottom-up) und berechne
für jeden Knoten n einen Kostenvektor C. Der Teilbaum von T, dessen Wurzel n
ist, wird dabei mit S bezeichnet. Die Elemente von C sind:
• C [i ]: optimale Kosten der Berechnung des Teilbaumes S in ein Register. Hierbei wird angenommen, dass für diese Berechnung i Register, 1 ≤ i ≤ r zur
Verfügung stehen. In den Kosten sind alle eventuell benötigten Load/StoreBefehle eingeschlossen.
• C [0]: optimale Kosten zur Berechnung des Teilbaumes S, wenn das Resultat
im Speicher abgelegt wird. Man beachte, dass C [0] nicht die Kosten darstellt,
um S ohne Register zu berechnen. Vielmehr wird S hier mit i Registern berechnet und danach im Speicher abgelegt. Daraus ergibt sich, dass C [0] für
Blätter des Baums gleich 0 ist, und für alle anderen Knoten gleich dem Minimum aller C [i ]; i > 0 plus 1.
Zur Bestimmung von C [i ] muss jeder Maschinenbefehl einzeln betrachtet werden,
der R := E abbilden kann, wobei E der in Knoten n zu berechnende Teilausdruck
KAPITEL 4. COMPILER UND CODEGENERIERUNG
84
ist. Die Kosten für die Berechnung der Operanden von E sind durch die Kostenvektoren der Kinder von n bestimmt. Für die Registeroperanden von E müssen
alle möglichen Reihenfolgen der in Registern ausgewerteten Teilbäume von S betrachtet werden. Für jede Reihenfolge kann der erste Teilausdruck, dessen Resultat
in ein Register abgelegt wird, i Register zur Berechnung verwenden, der zweite
Teilausdruck kann noch i − 1 Register verwenden, usw. Zu den Kosten für die Berechnung der Operanden von E müssen noch die Kosten für den Befehl R := E
addiert werden. Der Wert von C [i ] ist dann das Kostenminimum über alle Auswertungsreihenfolgen und geeignete Maschinenbefehle. Zu jedem Knoten n wird
der so bestimmte Maschinenbefehl notiert.
2. Phase: Bestimmung der Reihenfolge Der Baum T wird von oben nach unten durchlaufen, wobei bei den Knoten aufgrund der Kostenvektoren bestimmt wird, welche
Teilbäume in den Speicher berechnet werden müssen.
3. Phase: Codegenerierung Der Baum wird von unten nach oben durchlaufen, wobei
für jeden Knoten die Kostenvektoren und die damit verbundenen Maschinenbefehle benutzt werden, um den Zielcode zu erzeugen. Dabei wird zuerst der Code
für diejenigen Teilbäume erzeugt, deren Ergebnisse im Speicher abgelegt werden.
Alle drei Phasen dieses Algorithmus benötigen O(v) Zeit, wobei v die Anzahl der
Knoten des Baumes ist.
+
-
*
a
b
c
/
d
e
Abbildung 4.15: Syntaxbaum zu Beispiel 4.14
Beispiel 4.14 Die Abb. 4.15 zeigt einen Syntaxbaum, für den optimaler Code generiert werden soll. Die
Zielmaschine besitzt 2 Register, R0 und R1, und folgende Befehle, die alle Kosten von 1 haben:
Ri
Ri
Ri
Ri
Mj
:=
:=
:=
:=
:=
Mj
Ri op Rj
Ri op Mj
Rj
Ri
In der ersten Phase müssen die Kostenvektoren für die Knoten berechnet werden. Für das Blatt a des
Baums ist C [0], der Kostenwert zur Berechnung von a in den Speicher, gleich 0, da sich der Wert von
4.3. CODEOPTIMIERUNG
85
a bereits dort befindet. C [1], die Kosten zur Berechnung von a in ein Register, sind 1, da man a mit
dem Befehl R0 := a in ein Register laden kann. C [2], die Kosten zur Berechnung von a in eines von
zwei verfügbaren Registern, sind gleich C [1]. Das ergibt den Kostenvektor (0,1,1) für Blatt a. Dieselben
Kostenvektoren ergeben sich für die Blätter b, c, d, und e.
Für den Knoten, der a und b als Teilbäume hat, gibt es unter Verwendung von einem Register nur den
Befehl Ri := Ri - Mj. Es addieren sich die Kosten, um i) den linken Operanden a in ein Register zu
berechnen (Kosten 1), ii) den rechten Operanden b in den Speicher zu berechnen (Kosten 0), und iii) den
Befehl Ri := Ri - Mj auszuführen (Kosten 1). Daher ergibt sich: C [1] = 2. Falls man zwei Register zur
Verfügung hat, lassen sich zwei Befehle verwenden, Ri := Ri - Mj und Ri := Ri - Rj. Im ersten Fall
muss man den linken Operanden unter Verwendung von zwei Registern in ein Register berechnen (Kosten
1), und den rechten Operanden in den Speicher (Kosten 0). In Summe kostet dieser Befehl 2 Einheiten.
Für den Befehl Ri := Ri - Rj muss man zwei Reihenfolgen untersuchen. Die erste Möglichkeit ist,
zuerst den linken Teilbaum mit zwei Registern (Kosten 1), und dann den rechten Teilbaum mit einem
Register (Kosten 1) zu berechnen. Das ergibt Gesamtkosten von 3. Die zweite Möglichkeit ist, zuerst den
rechten Teilbaum mit zwei Registern, und dann den linken Teilbaum mit einem Register zu berechnen,
was wiederum Kosten von 3 ergibt. Daher ist der Wert C [2] = 2. C [0] = 3, da man zu den optimalen
Kosten, den Ausdruck in ein Register zu berechnen, noch Kosten von 1 für den Store-Befehl Mj := Ri
addieren muss. Der resultierende Kostenvektor ist (3, 2, 2).
Für die Wurzel des Baumes kann man unter Verwendung von einem Register nur den Befehl Ri := Ri
+ M verwenden. C [1] = 8, was sich aus den optimalen Kosten, den linken Teilbaum in ein Register zu
berechnen (Kosten 2), den rechten Teilbaum in den Speicher zu berechnen (Kosten 5) und den Kosten
für den Befehl (Kosten 1) ergibt. Für die Berechnung mit zwei Registern gibt es wieder zwei mögliche
Befehle, Ri := Ri + Mj und Ri := Ri + Rj. Für den ersten Befehl ergeben sich Kosten von 8. Für den
zweiten Befehl muss man zwei mögliche Reihenfolgen betrachten. In der ersten Reihenfolge berechnet man
den zuerst den linken Teilbaum mit zwei Registern und dann den rechten Teilbaum mit einem Register.
Dies ergibt Gesamtkosten von 8. In der zweiten Reihenfolge berechnet man zuerst den rechten Teilbaum
mit 2 Registern und dann den linken Teilbaum mit einem Register. Dies ergibt Gesamtkosten von 7. Die
Kosten für die Berechnung der Wurzel in den Speicher sind die optimalen Kosten für die Berechnung in
ein Register plus 1. Für die Wurzel ist demnach der Kostenvektor (8, 8, 7).
Die Ergebnisse der Phasen 1 bis 3 sind in Abb. 4.16 dargestellt. Die Codegenerierung ergibt folgende
optimale Codesequenz:
R0
R0
R1
R1
R0
R0
R0
:=
:=
:=
:=
:=
:=
:=
d
R0
c
R1
a
R0
R0
/ e
* R0
- b
+ R1
Diese Methode, dynamische Programmierung zur Codegenerierung für Ausdrucksbäume einzusetzen, stammt von Aho und Johnson und wurde 1976 entwickelt. Modifizierte Varianten davon werden heute in einer Vielzahl von Compilern eingesetzt.
4.3
Codeoptimierung
Codeoptimierung ist die Anwendung von Transformationen auf einen Code, um die
Qualität des Codes zu erhöhen. Das Wort Optimierung ist etwas irreführend, da es
KAPITEL 4. COMPILER UND CODEGENERIERUNG
86
R0:=R0+R1
erst rechts
+ (8,8,7)
R1:=R1*R0
R0:=R0 b
(3,2,2)
(0,1,1)
erst rechts
R1:=c
R0:=a
a
* (5,5,4)
b
(0,1,1)
R0:=R0/e
/ (3,2,2)
c
(0,1,1)
R0:=d
d
(0,1,1)
e
(0,1,1)
Abbildung 4.16: Dynamische Programmierung zur optimalen Befehlsauswahl, Ablaufplanung und Registervergabe
i.allg. keine Garantie dafür gibt, dass der verbesserte Code tatsächlich optimal hinsichtlich eines Qualitätsparameters ist. Es handelt sich bei der Codeoptimierung um Codeverbesserungstechniken. Je nach dem, welches Stück eines Programmcodes betrachtet
und verbessert wird, unterscheidet man in verschiedene Arten von Optimierungen. Bei
der sogenannten peephole Optimierung wird ein kurzer Ausschnitt des gesamten Programmes betrachtet, unabhängig von Grundblöcken. Transformationen, die auf einzelne Grundblöcke angewendet werden, bezeichnet man als lokale Optimierungen. Bei den
globalen Optimierungen wird dann das ganze Programm mit allen Grundblöcken betrachtet. Üblicherweise werden lokale Transformationen zuerst durchgeführt, und dann
werden globale Optimierungstechniken verwendet. Einzelne Transformationen können
oft für alle diese Optimierungsarten angewendet werden. So kommen z.Bsp. algebraische Transformationen sowohl in der peephole, als auch in der lokalen und globalen
Optimierung vor. In den folgenden Abschnitten werden für jede Optimierungsart typische Transformationen dargestellt.
4.3.1
Peephole Optimierung
Codegeneratoren, die einen Zwischencodebefehl nach dem anderen abarbeiten, erzeugen oft Zielcode mit suboptimalen Konstrukten und überflüssigen Anweisungen. Eine
einfache, aber effektive Technik für lokale Verbesserungen im Zielcode ist die sogenannte peephole optimization. Die peephole optimization ist eine Methode, bei der ein kurzer
Ausschnitt des Zielcodes betrachtet wird und Transformationen angewendet werden,
um in dem betrachteten Ausschnitt Codeverbesserungen zu erzielen. Das peephole ist
ein kleines, sich über dem Zielprogramm bewegendes Fenster. Charakteristisch für die
peephole optimization ist, dass jede Verbesserung neue Verbesserungsmöglichkeiten
schaffen kann. Deshalb wird bei der peephole optimization das peephole in mehreren Durchläufen über das Progamm gezogen. Peephole optimization kann sowohl zur
Verbesserung der Codequalität des Zielcodes als auch des Zwischencodes eingesetzt
werden. Im folgenden werden einige typischen Transformationen, die während einer
4.3. CODEOPTIMIERUNG
87
peephole optimization durchgeführt werden, aufgezählt:
• Entfernung überflüssiger Anweisungen
Beispiel 4.15 Durch die Codegenerierung werden häufig überflüssige Load- und Store-Befehle
erzeugt. In der Befehlsfolge
(1) MOV R0,a
(2) MOV a, R0
kann die Anweisung (2) entfernt werden, da jedesmal, wenn Anweisung (2) ausgeführt wird, durch
Anweisung (1) sichergestellt ist, dass sich der Wert von a bereits im Register R0 befindet. Dies gilt
jedoch nur, wenn Anweisung (2) kein Sprungziel ist. Anders formuliert: Die Anweisungen (1) und
(2) müssen dem gleichen Grundblock angehören, damit die Transformation korrekt ist.
• Kontrollflussoptimierungen
Beispiel 4.16 Ein Beispiel für eine Kontrollflussoptimierung ist die Elimination von Sprüngen
auf Sprünge. In der Befehlssequenz:
L1:
goto L1
...
goto L2
kann der Sprung auf L1 durch einen Sprung auf L2 ersetzt werden:
L1:
goto L2
...
goto L2
Falls es jetzt überhaupt keine Sprünge nach L1 mehr gibt, kann auch die Anweisung L1: goto L2
entfernt werden kann, falls dieser Anweisung ein unbedingter Sprung vorangeht. Diese Situation
tritt häufig bei Verzweigungen auf. Man nennt dieses Entfernen von Anweisungen, die nie mehr
erreicht (ausgeführt) werden können, dead code elimination.
• Algebraische Vereinfachungen
Beispiel 4.17 Von der grossen Anzahl möglicher algebraischer Vereinfachungen sind typische
und oft auftretende Fälle folgende Anweisungen:
x := x + 0
oder
x := x * 1
Diese Anweisungen können eliminiert werden.
• Operatorreduktionen (strength reduction)
Beispiel 4.18 Hier wird ein Operator durch einen anderen Operator ersetzt, der geringere Kosten
hat, d.h., der auf der Zielmaschine schneller ausgeführt werden kann. Ein Beispiel ist die Operation
KAPITEL 4. COMPILER UND CODEGENERIERUNG
88
x := y**2
die als
x := y*y
schneller ausgeführt werden kann. Weitere Beispiele sind das Ersetzen von Multiplikationen mit
Potenzen von 2 durch Schiebeoperationen oder von Divisionen durch floating-point Konstanten
durch Multiplikationen mit floating-point Konstanten. Im letzten Fall handelt es sich allerdings
meist um eine Approximation.
• Ausnutzung von Maschineneigenheiten
Beispiel 4.19 Besitzt die Zielmaschine z.Bsp. autoinkrement/autodekrement Adressierungsmodi,
können Anweisungen, die Adresszähler erhöhen bzw. erniedrigen, oft eliminiert werden.
4.3.2
Lokale Optimierung
Definition 4.9 In einem Grundblock werden eine Menge von Ausdrücken berechnet. Die Ergebnisse dieser Berechnungen erscheinen am Ausgang des Grundblocks als Werte aktiver Namen.
Zwei Grundblöcke sind äquivalent, wenn sie die gleiche Menge von Ausdrücken berechnen.
Bei der lokalen Optimierung werden Transformationen auf Grundblöcken durchgeführt. Ein Grundblock darf nur in einen äquivalenten Grundblock transformiert werden,
d.h., die Menge der von dem ursprünglichen Grundblock berechneten Ausdrücke darf
nicht verändert werden. Man unterscheidet zwei Klassen von lokalen Transformationen:
die strukturerhaltenden Transformationen und die algebraischen Transformationen. Die algebraischen Transformation sind identisch zu den algebraischen Transformationen bei
der peephole Optimierung. Im folgenden werden einige typische strukturerhaltende
Transformationen vorgestellt:
• Eliminierung gemeinsamer Teilausdrücke (common subexpression elimination)
Beispiel 4.20 Im Grundblock:
(1)
(2)
(3)
(4)
a
b
c
d
:=
:=
:=
:=
b
a
b
a
+
+
-
c
d
c
d
verwenden die Anweisungen (2) und (4) die gleichen Teilausdrücke. Die Anweisungen (1) und (3)
verwenden zwar auch die gleichen Variablen, allerdings nicht die gleichen Teilausdrücke, da die
Variable b in Anweisung (2) neu geschrieben wird. Dieser Grundblock kann wie folgt vereinfacht
werden:
(1)
(2)
(3)
(4)
a
b
c
d
:=
:=
:=
:=
b + c
a - d
b + c
b
4.3. CODEOPTIMIERUNG
89
• Umbenennung von Zwischenvariablen (variable renaming)
Beispiel 4.21 Bei der Anweisung
t := b + c
sei t eine Zwischenvariable. Man kann nun die Variable t auf den noch nicht benutzten Namen u
umbenennen und in allen folgenden Anweisungen, in denen t auftritt, t durch u ersetzen.
Der so erzeugte Grundblock ist äquivalent zum ursprünglichen Grundblock. Führt
man diese Umbenennung für alle Zwischenvariablen eine Grundblockes durch,
erhält man einen Grundblock, bei dem jede Zwischenvariable nur einmal definiert
wird. Man bezeichnet dies als Normalform eines Grundblocks. Variable renaming
alleine führt noch zu keiner Optimierung, ist aber eine wichtige Voraussetzung für
weitere Optimierungstechniken (z.Bsp. instruction interchange).
• Vertauschung von Anweisungen (instruction interchange)
Beispiel 4.22 Die beiden folgenden Anweisungen
t1 := b + c
t2 := x + y
kann man vertauschen, falls weder x noch y gleich t1 und weder b noch c gleich t2 sind. Wenn
ein Grundblock in Normalform vorliegt, sind alle Vertauschungen möglich, die durch die Datenabhängigkeiten erlaubt sind.
4.3.3
Globale Optimierung
Bei der globalen Optimierung werden alle Grundblöcke, d.h. der ganze Kontrollflussgraph des Programmes, betrachtet. Viele der bereits genannten Transformationen sind
auch hier anwendbar, z.Bsp. common subexpression elimination und verschiedene algebraische Transformationen. Eine globale Transformation muss nicht mehr strukturerhaltend sein, sonder funktionserhaltend. Das optimierte Programm muss die gleiche
Funktion wie das ursprüngliche Programm ausführen. Um eine globale Optimierung
durchführen zu können, benötigt man eine globale Datenflussanalyse. Diese zeigt für
das gesamte Programm, wo welche Ausdrücke erzeugt werden und wo sie wiederverwendet werden. Typische globale Transformationen sind:
• Entfernung passiven Codes (passive code elimination)
Eine Anweisung, die einen Wert für den Namen x erzeugt, kann dann entfernt
werden, wenn x nach der Erzeugung nicht mehr verwendet wird.
• Ersetzen kopierter Variablen (copy propagation)
Beispiel 4.23 In der folgenden Anweisungssequenz
KAPITEL 4. COMPILER UND CODEGENERIERUNG
90
(1)
(2)
(3)
(4)
x := t1
a[t2] := t3
a[t4] := x
goto L
ist Anweisung (1) eine Kopieranweisung. Bei der copy propagation wird - wo möglich - in allen folgenden Anweisungen statt der kopierten Variablen x direkt die ursprüngliche Variable t1
verwendet. Der Vorteil dieser Transformation liegt darin, dass es dann oft möglich ist, die Kopieranweisung zu eliminieren. Wenn im gegeben Beispiel die Variable x nach Anweisung (1) in
keinem Grundblock mehr definiert wird, kann die Anweisung (1) entfernt werden (passive code
elimination).
Eine weitere wichtige Klasse von Optimierungen betreffen Schleifenkonstrukte. Viele
Programme verbringen den grössten Teil ihrer Laufzeit in inneren Schleifen. Ziel der
Schleifenoptimierung ist es, diese inneren Schleifen möglichst schnell zu machen. Das
kann durchaus auf Kosten der Laufzeit der übrigen Programmteile geschehen. Typische
Schleifenoptimierungen sind:
• Codeverschiebung (code motion)
Beispiel 4.24 Wenn bei der Schleife
while (i <= limit*4-2) {
....
}
die Variable limit im Schleifenrumpf nicht verändert wird, kann die Schleife in
t = limit*4-2;
while (i <= t) {
....
}
transformiert werden.
• Induzierte Variablen und Operatorreduktion
Beispiel 4.25 In der Schleife
(1)
(2)
(3)
(4)
....
j := n
j := j - 1
t4 := 4 * j
t5 := a[t4]
if t5 > v goto (1)
....
gibt es neben dem Schleifenzähler j die Variable t4, die vom Schleifenzähler abhängt. Man kann
diese Schleife transformieren zu
4.4. CODEGENERIERUNG FÜR SPEZIALPROZESSOREN
(1)
(2)
(3)
(4)
91
....
j := n
t4 := 4 * j
j := j - 1
t4 := t4 - 4
t5 := a[t4]
if t5 > v goto (1)
....
und reduziert dadurch den Operator * von Anweisung (2) zu einem -.
4.4
Codegenerierung für Spezialprozessoren
Spezialprozessoren werden hauptsächlich in eingebetteten Systemen verwendet. Im Gegensatz zu general-purpose Systemen werden eingebettete Systeme tradionellerweise
in Assembler programmiert. In den letzten Jahren ist hier - mit Ausnahme von zeitkritischen Programmteilen - ein starker Trend zu erkennen, Assembler durch high-level
languages (HLLs) zu ersetzen. Die Motivation dafür sind geringere Entwicklungskosten
(schnellere Entwicklung) und geringere Wartungskosten. Diesen Vorteilen steht jedoch
der Nachteil eines grösseren Codes bei HLLs gegenüber, was zu grösseren Speicherbausteinen und damit zu höheren Kosten führt.
Die Ursache für die grössere Codelänge von compilierten Programmen liegt darin, dass bisher in der Codeoptimierungsphase fast ausschliesslich auf eine minimale
Ausführungszeit optimiert wurde und nicht auf eine minimale Codelänge. Zwischen
diesen zwei Parametern gibt es oft einen trade-off (z.Bsp. Code-Inlining vs. Unterprogramme). Ausserdem blieben Optimierungstechiken beschränkt auf Methoden, die eine
Zeitkomplexität kleiner als O(n2 ) besitzen, da eine schnelle Übersetzung wichtig war.
Die Hauptanforderungen an Compiler für Spezialprozessoren sind:
• korrekter Code
• sehr schneller Code Für eingebettete Systeme ist ein schneller Code (kurze Ausführungszeit) wesentlich wichtiger als eine schnelle Übersetzung.
• sehr kompakter Code Die Grösse der benötigten Programm- und Datenspeicher
muss minimiert werden. Dies ist besonders bei kostensensitiven Anwendungen
wichtig.
Die für die Programmierung von Spezialprozessoren am meisten verwendeten HLLs
sind C/C++. In manchen Gebieten (z.Bsp. Bildverarbeitung, Telekommunikationstechnik) werden auch bereichsspezifische Sprachen verwendet. Manche Entwicklungswerkzeuge erlauben die visuelle Spezifikation (Programmierung), unterstützen Simulation
und haben integrierte Codegeneratoren für C/C++ oder direkt für bestimmte Zielprozessoren (z.Bsp. COSSAP von Synopsys). Weitere Anforderungen an HLL und Compiler
für eingebettete Systeme sind:
• hohe Sicherheit Eingebettete Anwendungen sind manchmal sicherheitskritische
Anwendungen. Deshalb sollten die Programme in einer HLL, die möglichst gut
92
KAPITEL 4. COMPILER UND CODEGENERIERUNG
(formal) definiert ist, geschrieben werden. Das Ziel ist, die Abwesenheit von Fehlern in einem Programm mathematisch (automatisiert) zu beweisen.
• Echtzeitbedingungen Viele eingebettete Systeme müssen Zeitbedingungen einhalten. Dazu würde man HLLs mit Konstrukten benötigen, welche es erlauben,
zeitliche Bedingungen zu formulieren. Der Compiler könnte dann den generierten
Code auf die Einhaltung dieser Bedingungen überprüfen, bzw. der Code könnte
auf diesen Parameter optimiert werden.
• Unterstützung für DSP-Algorithmen und -Architekturen DSP-Anwendungen
haben in den letzten Jahren enorm an Bedeutung gewonnen. Um HLLs effizient zur Softwareentwicklung verwenden zu können, müssen die HLLs Konstrukte
zur Unterstützung von DSP-typischen Operationen, wie delayed signals, saturated
arithmetic, etc., aufweisen. Ein Beispiel ist die Sprache DFL (Data Flow Language)
[55]. Ausserdem müssen die Codegeneratoren die Architektureigenschaften von
DSPs unterstützen. Dazu zählen die spezialisierten, nicht homogenen Registersätze, die verschiedene Formen der Parallelität und DSP-spezifische Adressierungsarten.
• Retargetable Compiler Man möchte nicht für jeden neuen eingebetteten Prozessor
einen Compiler von Beginn an neu entwerfen müssen. Die Codegenerierungsteile
der Compiler sollten rasch veränderbar bzw. an die neue Architektur anpassbar
sein.
Bei der Codegenerierung für Spezialprozessoren müssen - wie bei GP-Prozessoren
- prinzipiell die Teilprobleme der Registerbindung, Befehlsauswahl und der Ablaufplanung gelöst werden. Bei Spezialprozessoren sind die Registersätze oft nicht homogen
und die Datenpfade irregulär. Bei einem nicht-homogenen Registersatz sind bestimmte Befehle oder bestimmmte Adressierungarten nur mit bestimmten Registern möglich.
Ein irregulärer Datenpfad bedeutet, dass die verschiedenen Rechenwerke nur bestimmte Quellen und Senken für Operanden und Ergebnisse verwenden können. Dies spiegelt
sich auch im Instruktionssatz wider, der dann Instruktionen mit vielen Einschränkungen und Sonderfällen enthält.
Das Kennzeichen der Codegenerierung für Spezialprozessoren ist, dass die Teilprobleme Registervergabe, Befehlsauswahl und Ablaufplanung sehr eng miteinander gekoppelt sind. Dies wird auch als Phasenkopplung (phase coupling) bezeichnet. Dadurch
kann man die Teilphasen der Codegenerierung nicht getrennt behandeln, und es gibt
auch keine für alle Fälle beste Reihenfolge der Phasen. Compiler verwenden heuristische
Strategien, um die Phasen möglichst clever zu verbinden. Auswirkungen einer noch zu
durchlaufenden Phase können z.Bsp. mit schnellen aber ungenauen Methoden geschätzt
werden, um Informationen für die aktuelle Phase zu erhalten.
Bei Spezialprozessoren mit Parallelbefehlen (DSPs, ASIPs) wird die Befehlsauswahl
und Ablaufplanung oft in drei Phasen durchgeführt:
• In der Phase code selection wird der Zwischencode auf partielle Instruktionen der
Zielmaschine abgebildet. Eine partielle Instruktion muss noch nicht ein Maschinenbefehl der Zielmaschine sein, sondern kann aus einem Teil einer Instrukti-
4.4. CODEGENERIERUNG FÜR SPEZIALPROZESSOREN
93
on bestehen, z.Bsp. nur aus einem Datentransferbefehl oder einer Adressberechnung. Der Hintergrund dieser Art der Befehlsauswahl ist, dass es bei Prozessoren
mit Parallelbefehlen viele Möglichkeiten gibt, mehrere partielle Instruktionen auf
einen Maschinenbefehl abzubilden.
• In der Ablaufplanung (scheduling) wird eine partielle Ordnung zwischen den TeilInstruktionen eingeführt, die einerseits die Semantik des Programmes erhalten
und andererseits möglichst viel Parallelität nutzen muss.
• Die Codekompaktierung (code compaction) generiert aus den partiellen Instruktionen unter Einhaltung des Ablaufplanes die Maschinen-Instruktionen.
In den folgenden Abschnitten werden ausgesuchte, wichtige Aufgabenstellungen
bei der Codegenerierung für Spezialprozessoren behandelt. Dabei werden zuerst zwei
Themen betrachtet, die vor allem bei DSPs und ASIPs auftreten: Nicht-homogene Registersätze bzw. irreguläre Datenpfade und die Zuweisung von Adressen und Adressregistern. Danach werden Codekompressionstechniken betrachtet; ein Thema, das besonders bei kostensensitiven Anwendungen von Interesse ist.
4.4.1
Nicht-homogene Registersätze, irreguläre Datenpfade
Bei der Codegenerierung für baumartige DAGs wird üblicherweise eine Strategie verwendet, die auf dynamischer Programmierung beruht und zuerst den kompletten linken
Teilbaum eines Ausdrucks, dann den rechten Teilbaum (oder umgekehrt) und anschliessend die Wurzel berechnet (siehe Abschnitt 4.2.5). Diese Strategie, die auch als normal
form scheduling (NFS) bezeichnet wird, liefert jedoch nur unter der Voraussetzung, dass
der Zielprozessor einen homogenen Registersatz besitzt, optimalen Code.
Beispiel 4.26 Abb. 4.17 zeigt das Blockschaltbild des Festkomma-DSPs TMS320C25 (Texas Instruments). Dieser Prozessor besitzt keine homogenen general-purpose Register. Es gibt einen Akkumulator
ACC (im folgenden mit a bezeichnet), der sich am Ausgang der ALU befindet und die zwei Spezialregister
TR und PR (im folgenden mit t bzw. p bezeichnet), die einen Eingangsoperanden bzw. das Ergebnis des
Multiplizierers halten.
Tabelle 4.4 zeigt einen Teil der Instruktionen des Prozessors. In Abb. 4.18 sind diese Instruktionen
als Muster, wie sie in einem DAG auftreten (tree pattern), dargestellt. Je nach Instruktion können die
Operanden und das Ergebnis im Speicher (im folgenden mit m bezeichnet), im Akkumulator oder in einem
der Spezialregister des Multiplizierers stehen, oder Konstanten sein (CONST). Die mpy Instruktion zum
Beispiel erwartet einen Operanden im t-Register, den anderen im Speicher und legt das Ergebnis im
p-Register ab.
Zum Transfer von Daten zwischen den Registern und dem Speicher gibt es die Befehle lac (load
memory into accumulator), lack (load constant into accumulator), lt (load memory into t register), pac
(load p register into accumulator) und sacl (store accumulator into memory).
94
KAPITEL 4. COMPILER UND CODEGENERIERUNG
Abbildung 4.17: Blockschaltbild des DSPs TMS320C25
Mit diesen Instruktionen soll für den DAG in Abb. 4.19 Code generiert werden. (In diesem Beispiel
gehen die Kanten des DAG von den Blättern zur Wurzel. Dies steht im Gegensatz zu der bisher in diesem
Kapitel verwendeten Kantenrichtung. Es sind beide Kantenrichtungen eindeutig und daher korrekt.)
In Abb. 4.19 sind die Knoten des DAG mit den jeweils passenden Instruktionen des TMS320C25
beschriftet. Die Befehlsauswahl und in diesem Fall auch die Registerbindung sind bereits durchgeführt. In
4.4. CODEGENERIERUNG FÜR SPEZIALPROZESSOREN
add:
apac:
spac:
a
a
+
a
a
+
m
-
a
mpy:
p
a
mpyk:
p
p
p
lack:
CONST
pac:
p
a
sacl:
a
m
lac:
m
a
lt:
m
t
p
*
*
t
95
m
t
CONST
Abbildung 4.18: Instruktionen des TMS320C25 als tree pattern
number
instruction
1
2
3
4
5
6
7
8
9
10
add
apac
spac
mpy
mpyk
lack
pac
sacl
lac
lt
destination
cost
a
a
a
p
p
a
a
m
a
t
1
1
1
1
1
1
1
1
1
1
Tabelle 4.4: Teil der Instruktionen des TMS320C25
m6
a
p
a
p
*
*
a
m0
m1
m5
t
m4
+
m2
m3
t
a
Abbildung 4.19: DAG für Beispiel 4.26
den folgenden Codestücken stellt die Version a) den generierten Code dar, wenn zuerst der linke Teilbaum
des DAGs ausgewertet wird, die Version b) den Code, wenn zuerst der rechte Teilbaum ausgewertet wird
und schliesslich die Version c) den optimalen Code für dieses Beispiel. Bei den Versionen a) und b) wird
für das Abspeichern von Zwischenergebnissen die Speicherstelle m7 verwendet.
96
/* Version a) */
lt
m1
mpy
m0
pac
sacl m7
lac
m3
add
m2
sacl m5
lt
m4
mpy
m5
lac
m7
spac
sacl m6
KAPITEL 4. COMPILER UND CODEGENERIERUNG
/* Version b) */
lt
m4
lac
m3
add
m2
sacl m5
mpy
m5
pac
sacl m7
lt
m1
mpy
m0
pac
lt
m7
mpyk 1
spac
sacl m6
/* Verision c) optimal */
lac
m3
add
m2
sacl m5
lt
m1
mpy
m0
pac
lt
m4
mpy
m5
spac
sacl m6
Wie das Beispiel zeigt, ist der optimale Ablaufplan keiner der beiden NFSAblaufpläne. Gesucht sind Verfahren zur Codegenerierung, die für Prozessoren mit
solchen nicht-homogenen Registersätzen und irregulären Datenpfaden möglichst guten
oder optimalen Code erzeugen und trotzdem allgemein genug sind, um auf verschiedene Prozessoren angepasst werden zu können.
In [5][4] wird ein Codegenerator vorgestellt, der zwei Phasen durchläuft. In einer
ersten Phase wird mit einem auf dynamischer Programmierung basierenden Verfahren
eine optimale Befehlsauswahl und Registerbindung berechnet. Dies ist eine Erweiterung
des Verfahrens von Abschnitt 4.2.5, wobei jeder Befehl des Prozessors als tree pattern
(wie in Abb. 4.18) dargestellt wird. In jedem Einzelschritt dieser ersten Phase werden
alle passenden Befehle untersucht. Zusätzlich wird berücksichtigt, dass der Transfer
von Daten zwischen Registern und Registern/Speicher oft nicht direkt erfolgen kann.
In einer zweiten Phase wird der optimale Ablaufplan bestimmt.
Es wurde gezeigt, dass für die Klasse von Prozessoren, die das sogenannte RTGKriterium erfüllen, ein optimaler Ablaufplan in O(n) Zeit generiert werden kann, wobei
n die Anzahl der Knoten des DAG ist. Optimal bedeutet hier, dass der erzeugte Ablaufplan keine zusätzlichen register spills einfügt. Für die Bestimmung des RTG-Kriteriums
muss man den Registertransfergraph (RTG) eines Prozessors konstruieren.
Definition 4.10 (Registertransfergraph) Der Registertransfergraph (RTG) eines Prozessors ist ein gerichteter Graph, bei dem jeder Knoten eine Stelle im Datenpfad kennzeichnet, an
der Daten gespeichert werden können. Jede Kante zwischen einem Knoten ri und r j wird mit den
Instruktionen beschriftet, die Operanden aus ri lesen und das Ergebnis nach r j schreiben.
In einem RTG gibt es drei Arten von Knoten: Knoten, die ein einzelnes Register, Registerfiles oder Speicher darstellen. Bei Registerfiles und Speicher bezeichnet ein Knoten
eine Menge von Speicherplätzen. Abb. 4.20 zeigt den RTG für den DSP TMS320C25. Die
Register a, p und t sind Einzelregister. Die Kanten sind mit den Indizes der Instruktionen aus Tabelle 4.4 beschriftet.
Definition 4.11 (RTG Kriterium) Das RTG Kriterium ist erfüllt, falls es für alle Knoten
r1 , r2 und r3 des RTG, für die i) r3 eingehendene Kanten von den Registerknoten r1 und r2 mit
4.4. CODEGENERIERUNG FÜR SPEZIALPROZESSOREN
97
1, 2, 3
2, 3, 7
a
1, 9
8
4, 5
p
t
4
10
m
Abbildung 4.20: Registertransfergraph für den TMS320C25
gleicher Beschriftung hat, und ii) es mindestens einen gerichteten Zyklus zwischen r1 und r2
gibt, gilt: In jedem möglichen Zyklus zwischen r1 und r2 existiert ein Speicherknoten.
Aus Abb. 4.20 sieht man, dass für den TMS320C25 das RTG-Kriterium zutrifft. Der
einzige Knoten, der von zwei Registerknoten eingehende Kanten derselben Beschriftung
hat, ist a. a hat eingehende Kanten von a und p durch die Instruktionen apac und spac.
Jeder Zyklus zwischen a und p führt über den Speicherknoten m.
4.4.2
Zuweisung von Speicheradressen und Adressregistern
Viele Spezialprozessoren, insbesondere DSPs, besitzen eigene Register und Adressrechenwerke für die Adressgenerierung. Damit werden indirekte Adressierungen mit autoinkrement/dekrement um eine Adresse oder um eine konstante Schrittweite, oder
aber auch DSP-typische Adressierungsarten, wie circular- und bitrevers-Adressierung,
realisiert. Durch effiziente Nutzung dieser Adressierungsarten können viele Adressrechenoperationen parallel zur eigentlichen Instruktionsabarbeitung durchgeführt werden, was den Code sowohl kürzer als auch schneller macht. Für die Codegenerierung ergeben sich zwei Problemstellungen: Wie plaziert man die Variablen im Speicher,
so dass man möglichst oft die Spezialadressierungsarten verwenden kann (Speicheradresszuweisung), und wie teilt man die vorhandenen Adressregister den Variablen zu
(Adressregisterzuweisung).
Abb. 4.21 zeigt die Adressrechenwerke der DSPs TMS320C2x (Texas Instruments),
Motorola 56000 und ADSP 210x (Analog Devices). Der TMS320C2x unterstützt direkte
und indirekte Adressierungsmodi. Im direkten Adressierungsmodus wird die effektive Adresse durch ein 9 Bit Register (data page pointer, DP) und die niederwertigen 7
Bits des Instruktionswortes gebildet. Bei den indirekten Adressierungsmodi wird die
effektive Adresse durch eines der acht 16-Bit breiten auxiliary registers AR[0] . . . AR[7]
gebildet. Welches dieser Register verwendet wird, wird durch das 3-Bit breite Register
ARP bestimmt. Das Adressenrechenwerk erlaubt post-modifications, d.h., die AR-Register
können am Ende eines Maschinenzyklus verändert werden. Die Adresse kann entweder um +/− 1 verändert werden oder es kann der Inhalt von AR[0] zu/von AR[ARP]
addiert/subtrahiert werden.
98
KAPITEL 4. COMPILER UND CODEGENERIERUNG
Abbildung 4.21: Adressrechenwerke von DSPs
Der Motorola 56000 bietet auch direkte und indirekte Adressierungmodi an. Bei der
direkten Adressierung wird die effektive Adresse aus dem Instruktionswort extrahiert.
Für indirekte Adressierungarten besitzt der DSP acht Adressregister R0 . . . R7 und acht
Offsetregister N0 . . . N7. Die effektive Adresse wird entweder durch Ri oder durch Ri+Ni
bestimmt. Die Register Ri können um +/− 1 oder +/− Ni post-modifiziert werden. Zusätzlich können die Ri pre-dekrementiert werden. Es können in einem Zyklus maximal
zwei Ri modifiziert werden.
Beim ADSP210x gibt es ebenfalls direkte und indirekte Adressierungsmodi. Die effektive Adresse bei der direkten Adressierung wird aus dem Instruktionswort extrahiert.
Indirekte Adressierung wird durch die vier Indexregister I0 . . . I3 und die vier Modifikationsregister M0 . . . M3 unterstützt. Die effektive Adresse entspricht dem Inhalt eines
4.4. CODEGENERIERUNG FÜR SPEZIALPROZESSOREN
99
Indexregisters, die Modifikationsregister werden für Post-Modifikationen der Indexregister verwendet.
Abbildung 4.22: Generisches Adressrechenwerk
Obwohl es von DSP zu DSP Unterschiede gibt, haben alle Prozessoren gewisse gemeinsame Merkmale:
• Eine Reihe von Adressregistern (AR), die die effektiven Adressen für indirekte
Adressierungsarten beinhalten.
• Eine Reihe von Modifikationsregistern (MR), die für die Änderungen der Adressregister verwendet werden.
• Modifikationsoperationen, die parallel zur Instruktionsabarbeitung durchgeführt
werden. Diese Operationen ändern die Werte der Adressregister, ohne auf den
Speicher oder Register ausserhalb des Adressrechenwerkes zugreifen zu müssen.
Abb. 4.22 zeigt eine generische Adressgenerierungseinheit, die diese Eigenschaften
zusammenfasst und die für die folgenden Betrachtungen verwendet wird. Es gibt eine
Adressregisterfile und ein Modifikationregisterfile. Die effektive Adresse entspricht einem Adressregister. Die Adressregister können um +/− 1 oder um +/− dem Inhalt
eines Modifikationsregisters verändert werden. Die Tabelle 4.5 zeigt die Operationen
dieses generischen Adressrechenwerkes. Operationen, die auf einen Wert im Speicher
zugreifen (value), haben einen Kostenwert von 1, die anderen 0. Die Operationen ARP
load und MRP load haben ebenfalls den Kostenwert 0, da sie nur kurze Operaden benötigen und man annimmt, dass diese direkt im Instruktionswort codiert sind.
KAPITEL 4. COMPILER UND CODEGENERIERUNG
100
operation
functionality
cost
AR load
MR load
ARP load
MRP load
immediate modify
auto-increment
auto-decrement
auto-modify
AR[ARP] = value
MR[MRP] = value
ARP = value
MRP = value
AR[ARP] += value
AR[ARP] ++
AR[ARP] –
AR[ARP] += MR[MRP]
1
1
0
0
1
0
0
0
Tabelle 4.5: Operationen der generischen Adressgenerierungseinheit
Adressierung von Skalarvariablen
In Compilern für general-purpose Prozessoren spielt die Reihenfolge, in der die Variablen den Speicherstellen zugewiesen werden, keine Rolle. Üblicherweise werden die
Variablen in der Reihenfolge ihrer Definition oder ihrer ersten Verwendung den Speicherplätzen zugeordnet. Bei Prozessoren mit Adressgenerierungseinheiten ist jedoch
die Reihenfolge der Variablen im Speicher wichtig. Bei einer guten Reihenfolge können möglichst oft autoinkrement/dekrement Operationen genutzt werden.
0
1
2
3
a
b
c
d
0
1
2
3
c
a
d
b
0
1
2
3
c
a
d
b
LOAD AR, 1
-- b
LOAD AR, 3
-- b
LOAD AR, 3
-- b
AR += 2
-- d
AR --
-- d
AR --
-- d
AR -= 3
-- a
AR --
-- a
AR --
-- a
AR += 2
-- c
AR --
-- c
AR --
-- c
AR ++
-- d
AR +=2
-- d
LOAD MR,2
AR -= 3
-- a
AR --
-- a
AR +=MR
-- d
AR += 2
-- c
AR --
-- c
AR --
-- a
AR --
-- b
AR += 3
-- b
AR --
-- c
AR --
-- a
AR -= 2
-- a
AR += 3
-- b
AR += 3
-- d
AR ++
-- d
AR -=MR
-- a
AR -= 3
-- a
AR --
-- a
AR ++
-- d
AR += 2
-- c
AR --
-- c
AR --
-- a
AR ++
-- d
AR += 2
-- d
AR --
-- c
AR +=MR
-- d
adressing cost = 9
adressing cost = 5
adressing cost = 3
a)
b)
c)
Abbildung 4.23: Adressierungsoperationen für Beispiel 4.27
4.4. CODEGENERIERUNG FÜR SPEZIALPROZESSOREN
101
Beispiel 4.27 In einem Grundblock werden vier Speicherstellen (Variablen) a, b, c, d in der Reihenfolge
(b, d, a, c, d, a, c, b, a, d, a, c, d) referenziert. Abb. 4.23a) zeigt die Adressgenerierung mit einem Adressregister AR, falls die Variablen in der Reihenfolge a, b, c, d im Speicher abgelegt sind. Die gesamten Kosten
für die Adressierung sind gleich 9. Abb. 4.23b) zeigt die Adressierungsoperationen für die Reihenfolge
c, a, d, b der Variablen im Speicher. Durch diese Reihenfolge lassen sich autoinkrement/dekrement Adressierungen, die keine zusätzliche Kosten verursachen, wesentlich öfter nutzen. Die Gesamtkosten betragen
5. Abb. 4.23c) zeigt schliesslich die Adressgenerierung, wenn zusätzlich noch ein modify-Register MR
verwendet wird. Dadurch lassen sich die Gesamtkosten auf 3 reduzieren.
Variablen, zwischen denen die Transitionshäufigkeit gross ist (die oft hintereinander
in der Zugriffsreihenfolge vorkommen), sollten in aufeinanderfolgenden Speicherzellen
stehen. Dann kann man für diese Transitionen durch autoinkrement/dekrement um eins
die Adressen ohne Zusatzkosten generieren. Dieses Problem wird formal mittels eines
access graphs (Zugriffsgraph) modelliert.
Definition 4.12 (Zugriffsgraph) Für eine gegebene Menge von Variablen V und eine Zugriffssequenz S ist der Zugriffsgraph ein gewichteter, ungerichteter Graph Ga = (V, E, ω ).
Dabei sind die Knoten V die Variablen, die Kanten (vi , v j ), vi , v j ∈ V, vi 6= v j verbinden Variablen, die in der Zugriffssequenz aufeinander folgen und die Gewichte der Kanten ω : E → N0
geben die Transitionshäufigkeit zwischen den Variablen an.
Definition 4.13 (Adresszuweisung) Für ein Zugriffssequenz S auf einer Variablenmenge V
ist eine Adresszuweisung eine Abbildung π : V → 0, . . . , |V − 1|, die allen Variablen aus V
eine eindeutige Position in einem durchgehenden Adressraum der Grösse |V | zuordnet.
Die Distanz δπ (vi , v j ) für zwei Variablen vi , v j ∈ V in Bezug auf eine Adresszuweisung π ist |π (vi ) − π (v j )|. Variablen in aufeinanderfolgenden Speicherzellen haben
demnach eine Distanz von 1. Damit lässt sich ein Kostenwert für eine Adresszuweisung
angeben:
cost(π ) = 1 +
∑
ω (e)
e={vi ,v j }∈ Ê
mit
Ê = {{vi , v j } ∈ E | δπ (vi , v j ) > 1}
Das zu lösende Problem ist nun, eine Adresszuweisung mit minimalen Kosten
für den Zugriffsgraph Ga unter Verwendung eines Adressregisters zu finden. Dieses
Problem wird auch als das simple offset assignment (SOA) Problem bezeichnet. Eine
Adresszuweisung mit minimalen Kosten ist ein Hamiltonischer Pfad in Ga mit maximalem Gewicht. Ein Hamiltonischer Pfad ist ein Pfad durch einen Graphen, der alle
Knoten genau einmal besucht. Dieses Problem ist NP-hart. Folgende Heuristik für das
SOA Problem ist von Liao [50] vorgeschlagen worden:
1. Sortiere alle Kanten, die ein Gewicht > 0 besitzen in absteigender Reihenfolge
nach Gewichten. Dies erzeugt die Liste L. Der Anfangspfad ist leer (P = {}).
2. Solange der Pfad noch nicht aus |V − 1| Kanten besteht:
KAPITEL 4. COMPILER UND CODEGENERIERUNG
102
(a) falls es keine Kanten mehr mit einem Gewicht > 0 gibt (L leer ist), wähle
irgendwelche Kanten des Zugriffsgraphen mit Gewicht 0.
(b) sonst nimm die oberste Kante von der Liste L und gib die Kante zu P falls
• sie keinen Zyklus in P erzeugt
• sie keinen Knoten in P mit fanout > 2 erzeugt
1
a
b
a
4
b
0
c
1
a
2
d
3
b
4
3
1
3
1
1
c
2
d
c
Zugriffsgraph
d
Pfad mit maximalem Gewicht
a)
b)
Adresszuweisung
c)
Abbildung 4.24: Berechnung der optimalen Adresszuweisung für Beispiel 4.28
Beispiel 4.28 Der Zugriffsgraph für die Zugriffssequenz in Beispiel 4.27 ist in Abb. 4.24a) dargestellt.
Abb. 4.24b) zeigt den Pfad mit maximalem Gewicht, und Abb. 4.24c) zeigt die optimale Adresszuweisung
mit einem Adressregister.
Das SOA-Problem lässt sich auf das GOA (general offset assignment) Problem verallgemeinern, bei dem mehrere Adressregister betrachtet werden. Realistische Architekturen besitzen 4-8 Adressregister. Bei GOA wird die Variablenmenge V in disjunkte
Teilmengen aufgeteilt und jeder Teilmenge ein eigenes Adressregister zugewiesen.
Definition 4.14 (Subzugriffssequenz) Für einen Zugriffsgraph Ga = (V, E, ω ) einer Zugriffssequenz S und eine Teilmenge V 0 der Variablen, ist die Subzugriffssequenz S(V 0 ) die
Menge aller Zugriffe in S, die ausschliesslich Elemente von V 0 referenzieren.
Für eine Adressgenerierungseinheit mit k Adressregistern ist das GOA-Problem das
Finden einer Partitionierung
P : V → V1 , . . . , Vk
so dass
k
∑ cost(πi ) → minimum
i =1
wobei πi eine optimale Adresszuweisung für die Subzugriffssequenz S(Vi ) ist. Auch
das GOA-Problem ist NP-hart und muss für praktisch interessante Probleme mit Heuristiken gelöst werden. Die SOA- und GOA-Aufgabenstellungen können erweitert werden, um auch Modifikationsregister mit einzuschliessen. Ist ein Modifikationsregister
4.4. CODEGENERIERUNG FÜR SPEZIALPROZESSOREN
103
mit einem Wert m geladen, sind nicht nur Variablen mit der Adressdifferenz von 1 benachbart, sondern auch mit der Adressdifferenz von m.
Adressierung von Arrays
Eine effiziente Adressgenerierung für Arrayzugriffe ist sehr wichtig, da viele DSPAlgorithmen Operationen in Schleifen auf mehrdimensionalen Feldern von Daten
durchführen. Es werden folgende, für DSP-Algorithmen realistische, Annahmen getroffen:
• Die Schleifen sind FOR-Schleifen mit einer festen, zur Übersetzungszeit bekannten, Anzahl von Iterationen.
• Für jede Schachtelungstiefe gibt es einen eindeutigen Schleifenzähler, der vor der
Schleife definiert und am Ende der Schleife um eins erhöht wird.
• Die
Arrayreferenzen für ein n-dimensionales Array sind A[ f 1 (i1 )][ f 2 (i2 )] . . . [ f n (in )]. Dabei
sind die Indizierungsfunktionen von der Art f j (i j ) = {c, i j + c, i j − c, c + i j , c − i j },
wobei c ∈ N0 und i j die Schleifenvariable ist.
Für die Organisation der Arraydaten im Speicher wird folgendes, bei Compilern
übliches, Schema angenommen: In einem n-dimensionales Array A[m1 ][m2 ] . . . [mn ], mit
mi ∈ [0, Ni − 1], hat ein bestimmtes Element die Adresse
n
adr ( A[m1 ][m2 ] . . . [mn ]) = base( A) + ∑ m j .a j
j =1
wobei
(
aj =
∏nk= j+1 Nk , 1 ≤ j < n
1,
j=n
Beispiel 4.29 Ein 3-dimensionales Array ist als A[10][4][5] definiert, d.h., N1 = 10, N2 = 4 und
N3 = 5. Die Arrayreferenz A[5][3][1] führt zu folgender Adresse im Speicher:
adr ( A[5][3][1] = base( A) + m1 .a1 + m2 .a2 + m3 .a3 = base( A) + 5 × 20 + 3 × 5 + 1 × 1 = base( A) + 116.
Der einfachste Fall einer nicht-geschachtelten Schleife hat folgende Struktur:
FOR i=low TO high DO
array_reference_1
array_reference_2
....
array_reference_k
ENDFOR
KAPITEL 4. COMPILER UND CODEGENERIERUNG
104
Jeder Arrayreferenz kann ein sogenannter update value (UV) zugeordnet werden. Der
UV einer Referenz ist die Differenz der Adressen in aufeinanderfolgenden Iterationen
der Schleife.
UV ( A[ f 1 (i )] . . . [ f n (i )]) = adr ( A[ f 1 (i1 ) . . . f n (i1 )]) − adr ( A[ f 1 (i0 ) . . . f n (i0 )])
In dieser Gleichung ist i0 der Index einer Iteration und i1 der Index der nächsten Iteration. Das Ziel der Adressregisterzuweisung bei Arrays ist es, durch Verwendung von
autoinkrement/dekrement Adressierungen die Kosten für die Arrayreferenzen möglichst gering zu halten. Dies gilt einerseits für die Sequenz von Referenzen innerhalb
des Schleifenrumpfes, und andererseits auch für die Referenzen in aufeinanderfolgenden Iterationen. Im letzteren Fall sollten die Adressregister automatisch um den UV
erhöht werden. Insbesondere können sich zwei Arrayreferenzen ein Adressregister teilen, wenn sie den gleichen update value haben.
Beispiel 4.30 In der folgenden nicht-geschachtelten Schleife
(1)
(2)
(3)
(4)
(5)
(6)
(7)
FOR i=2 TO 1024 DO
ref A[i+1]
ref A[i]
ref A[i+2]
ref A[i-1]
ref A[i+1]
ref A[i]
ref A[i-2]
ENDFOR
sind alle Referenzen von der Form A[i + c]. Die update values aller Referenzen sind deshalb 1. Das
bedeutet, dass sich alle Referenzen ein Adressregister teilen können. Eine mögliche Adressgenerierung ist
wie folgt:
AR1 = adr(A[3])
FOR i=2 TO 1024 DO
AR1 -AR1 += 2
AR1 -= 3
AR1 += 2
AR1 -AR1 -= 2
AR1 += 4
ENDFOR
/* AR1 zeigt auf A[i+1] @ i=2 */
Dabei enstehen in der Schleife Zusatzkosten von 5. Eine Alternative wäre die Verwendung von einem
eigenen Adressregister für jede Referenz. Dies würde zwar mehrere Initialisierungsinstruktionen vor der
Schleife benötigen, in der Schleife selbst aber keine Zusatzkosten verursachen.
In der Praxis muss man für eine gegebene Anzahl von Adressregistern die Zusatzkosten durch die Adressierung minimieren. Dieses Problem kann mit folgenden Graphen
modelliert werden:
4.4. CODEGENERIERUNG FÜR SPEZIALPROZESSOREN
105
Definition 4.15 (Intra-Iterationsgraph) Für
eine
Sequenz
von
Arrayreferenzen (r1 , r2 , . . . , rk ) in einer Schleife ist der Intra-Iterationsgraph Gintra = (V, E) ein
gerichteter azyklischer Graph, bei dem die Knoten V die Referenzen (r1 , r2 , . . . , rk ) darstellen
und eine Kante e = (ri , r j ) zwischen zwei Knoten besteht, falls
1. i < j
2. UV (ri ) = UV (r j )
3. | adr (ri ) − adr (r j )| ≤ 1
Eine Kante von ri nach r j gibt an, dass die Adresse von r j ohne Zusatzkosten, d.h. nur
mit autoinkrement/dekrement um eins, aus der Adresse von ri berechnet werden kann.
In diesem Fall können sich ri und r j ein Adressregister teilen. Abb. 4.25 zeigt den IntraIterationsgraph zum Beispiel 4.30.
1
2
3
4
5
6
7
Abbildung 4.25: Intra-Iterationsgraph zu Beispiel 4.30
Als nächstes betrachtet man zwei aufeinander folgende Iterationen der Schleife.
Definition 4.16 (Inter-Iterationsgraph) Für
eine
Sequenz
von
Arrayreferenzen
(r1 , r2 , . . . , rk ) in einer Schleife ist der Inter-Iterationsgraph Ginter = (V, E) ein bipartiter
gerichteter azyklischer Graph, bei dem die Knoten V aus {r1 , r2 , . . . , rk } ∪ {r10 , r20 , . . . , rk0 } sind,
und eine Kante e = (ri , r 0j ) zwischen zwei Knoten besteht, falls
1. i ≥ j
2. UV (ri ) = UV (r j )
3. | adr (r j ) + UV (r j ) − adr (ri )| ≤ 1
KAPITEL 4. COMPILER UND CODEGENERIERUNG
106
1
1’
2
2’
3
3’
4
4’
5
5’
6
6’
7
7’
Abbildung 4.26: Inter-Iterationsgraph zu Beispiel 4.30
In diesem Fall kann die Adresse für r j in der nächsten Iteration (adr (r j ) + UV (r j ))
von der Adresse von ri in der aktuellen Iteration ohne Zusatzkosten berechnet werden.
Abb. 4.26 zeigt den Inter-Iterationsgraphen für das Beispiel 4.30.
Schliesslich erhält man den gesamten Distanzgraphen durch Vereinigung von Intraund Inter-Iterationsgraphen.
Definition 4.17 (Distanzgraph) Für einer Sequenz von Arrayreferenzen (r1 , r2 , . . . , rk ) in
einer Schleife mit dem Intra-Iterationsgraph Gintra (V1 , E1 ) und dem Inter-Iterationsgraph
Ginter (V2 , E2 ) erhält man den Distanzgraphen Gdist (V1 ∪ V2 , E1 ∪ E2 ).
Nun müssen die Referenzen {r1 , . . . , rk } in disjunkte Gruppen aufgespaltet werden,
deren Anzahl nicht grösser als die Anzahl verfügbarer Adressregister sein darf. Für jede Gruppe muss es einen Pfad in Gdist geben, der alle Knoten der Gruppe besucht und,
falls er in ri startet, in ri0 endet. Wenn für eine Gruppe so ein Pfad gefunden werden
kann, können alle Adressierungen in der Gruppe ohne Zusatzkosten durchgeführt werden. Eine optimale Zuweisung zu finden entspricht einem path covering problem. Bereits
für den einfachen Fall einer nicht-geschachtelten Schleife ist dieses Problem NP-hart
und somit für grössere Schleifen nicht in sinnvoller Zeit exakt berechenbar. Es wurden
mehrere Heuristiken vorgeschlagen, siehe [49].
Beispiel 4.31 Abb. 4.27 zeigt den Distanzgraph zu Beispiel 4.30. Die optimale Aufspaltung dieses
Graphen in Pfade ist gegeben durch
P1 = (1, 2, 5, 10 ), P2 = (4, 6, 40 ), P3 = (3, 30 ), P4 = (7, 70 )
Damit ergeben sich folgende vier Gruppen:
4.4. CODEGENERIERUNG FÜR SPEZIALPROZESSOREN
107
1
1’
2
2’
3
3’
4
4’
5
5’
6
6’
7
7’
Abbildung 4.27: Distanzgraph zu Beispiel 4.30
g1 = {1, 2, 5}, g2 = {4, 6}, g3 = {3}, g4 = {7}
Das bedeutet, dass mit vier Adressregistern alle Adressierungsoperationen wie folgt durchgeführt werden
können:
AR1 = adr(A[3])
AR2 = adr(A[1])
AR3 = adr(A[4])
AR4 = adr(A[0])
FOR i=2 TO 1024 DO
AR1 -AR1 ++
AR3 ++
AR2 ++
AR1 ++
AR2
AR4 ++
ENDFOR
/*
/*
/*
/*
/*
/*
/*
/*
/*
/*
/*
AR1
AR2
AR3
AR4
zeigt
zeigt
zeigt
zeigt
bereitet
bereitet
bereitet
bereitet
bereitet
bereitet
bereitet
auf
auf
auf
auf
A[i+1]
A[i-1]
A[i+2]
A[i-2]
@
@
@
@
i=2
i=2
i=2
i=2
*/
*/
*/
*/
(2) vor */
(5) vor */
(3)’ vor */
(6) vor */
(1)’ vor */
(4)’ vor */
(7)’ vor */
In der Schleife selbst werden alle Adressrechnungen durch autoinkrement/dekrement um eins realisiert, was keine Zusatzkosten für Adressierungen verursacht. Vor der Schleife müssen die vier Adressregister auf ihre Initialwerte gesetzt werden.
Diese Graphenmodelle der Adressregisterzuweisungsprobleme lassen sich durch
Hinzunahme von Modifikationsregistern erweitern und sind auch auf geschachtelte
Schleifen anwendbar.
KAPITEL 4. COMPILER UND CODEGENERIERUNG
108
4.4.3
Codekompression
Zielcode ist i.allg. redundant, d.h., es kommen bestimmte Codefragmente öfters vor.
Daher kann man Codekomprimierungstechniken einsetzen, um diese Redundanz zu
entfernen und dadurch die Grösse des Codes zu reduzieren. Im general-purpose Bereich können Programme komprimiert auf dem Sekundärspeicher abgelegt und beim
Laden in das RAM dekomprimiert werden. Diese Technik behandelt die zu komprimierenden Daten (Zielcode) als sequentielle Folge von Bytes bzw. Worten. Der Vorteil dieser
Komprimierung ist ein geringerer Sekundärspeicherbedarf.
Bei eingebetteten Systemen ist die Reduktion der Programm-ROM oder RAM Grösse
wesentlich, und nicht die Ersparnisse beim Sekundärspeicher. Man benötigt hier wahlfreien Zugriff (random access) auf die Worte des Zielcodes (Sprünge, Schleifen). Die
im general-purpose Bereich verwendeten Datenkompressionsverfahren sind nicht direkt anwendbar.
Es wurden Prozessorarchitekturen (RISC) vorgeschlagen, die komprimierten Code
in den Cache lesen und dort dekomprimieren [72] [41]. Obwohl dieses Verfahren den
Einsatz von sehr leistungsfähigen Komprimierungstechniken erlaubt, müsste man das
komplette Cachesystem eines Prozessors ändern. Dies wäre ein enormer Aufwand und
nur für neue Prozessoren machbar. Ein weiterer Nachteil dieser Technik ist, dass die
Programmausführungszeit, die bei Prozessoren mit Cache ohnehin schwer vorherzusagen ist, noch schwerer zu bestimmen wäre. Viele Spezialprozessoren besitzen überdies
keinen Cache.
Ein Codekompressionsverfahren, das auch auf bereits existierenden Spezialprozessoren anwendbar ist, beruht auf dem external pointer macro (EPM) Modell [69]. In diesem
Modell besteht das komprimierte Programm aus zwei Teilen, einem dictionary und einem skeleton. Das dictionary enthält Befehlsreihenfolgen, die öfters im Originalcode auftauchen. Das skeleton besteht aus ursprünglichen Befehlen vermischt mit Pointern auf
Einträge im dictionary. Bei der Komprimierung werden öfters auftretende Befehlsfolgen
im Code identifiziert und im dictionary abgespeichert. Im Originalcode werden diese
Befehle durch einen Pointer auf die entsprechende Position im dictionary ersetzt.
Eine einfache Möglichkeit, das EPM-Modell zu implementieren, ist, die Pointer
durch call Anweisungen zu realisieren. Dadurch wird die Befehlsreihenfolge im dictionary zu einem kleinen Unterprogramm (mini-subroutine). Der letzte Befehl eines Eintrages im dictionary muss ein ret Befehl sein (der bei der Komprimierung eingefügt werden muss). Da die mini-subroutines direkt aus dem Zielcode extrahiert wurden, müssen
beim call keine Register gesichert werden, was den overhead des mini-subroutine Aufrufes reduziert.
Eine alternative Implementierung des EPM-Modells, die noch weniger overhead mit
sich bringt, aber zusätzliche Hardware benötigt, wurde in [51] vorgeschlagen. Ein Pointer besteht hier aus zwei Teilen, einer Adresse (address) und einer Längenangabe (length).
Der Instruktionssatz des Prozessors muss um die Instruktion CALD (call dictionary) erweitert werden. Beim Aufruf einer mini-subroutine mit CALD steht fest, wie viele Instruktionen aus dem dictionary gelesen werden müssen. Die ret Befehle im dictionary
können daher weggelassen werden. Abb. 4.28 zeigt, wie die Hardwareerweiterung beim
DSP TMS320C25 aussieht. Insgesamt wird ein Flip-Flop, ein Zähler, ein AND-, zwei
OR-Gatter und ein Registerstack benötigt. Wenn das Flip-Flop gesetzt ist, befindet sich
4.5. RETARGETABLE COMPILER
109
der Prozessor im dictionary Mode. Der Zähler zählt dann die Anzahl der Instruktionen,
die noch aus dem dictionary gelesen werden müssen. Der Registerstack hält die Rückkehradressen für die CALD und auch CALL Instruktionen. Immer wenn ein CALD oder ein
CALL ausgeführt wird, wird das PUSH Signal aktiv, um die aktuelle Rückkehradresse in
den Registerstack zu schreiben. Wenn der Zähler 0 erreicht oder ein RET im dictionary
erreicht wird, wird das POP Signal aktiv, um die aktuelle Rücksprungadresse aus dem
Registerstack zu lesen.
Abbildung 4.28: Hardwareerweiterung für das EPM-Modell zur Codekomprimierung
Mit dieser Methode benötigt jeder Zugriff auf einen dictionary Eintrag beim
TMS320C25 nur einen zusätzlichen Instruktionszyklus. Experimente mit diesem Modell haben gezeigt, dass die Codegrösse um 8 - 30 % gegenüber einem kommerziellen
Compiler (Texas Instruments) reduziert werden kann.
4.5
Retargetable Compiler
Retargetable Compiler sind Compiler, die sich relativ rasch an neue Zielarchitekturen
anpassen lassen. Man unterscheidet drei Arten von retargetable Compilern:
• Maschinenunabhängige Compiler
Diese Compiler werden auch automatically retargetable genannt. Der Compiler hat
bereits Codegeneratoren für verschiedene Zielarchitekturen eingebaut. Durch Setzen eines Parameters wird eingestellt, für welches Ziel Code generiert werden
soll. Solche Compiler sind typisch für parametrische Architekturen, d.h., Architekturen, die sich in der Bitbreite des Datenpfades, der Anzahl von Registern im
Registerfile, etc., unterscheiden.
• Compiler-Compiler Diese Compiler heissen auch user retargetable. Die Eingabe für
einen Compiler-Compiler ist eine Beschreibung der Zielarchitektur. Daraus wird
ein Compiler für diese Architektur generiert.
KAPITEL 4. COMPILER UND CODEGENERIERUNG
110
• Portable Compiler
Diese Compiler werden auch als developer retargetable bezeichnet und stellen die
Grenze zwischen retargeting und Schreiben eines neuen Compilers dar.
Kommerziell werden zur Zeit maschinenunabhängige und portable Compiler eingesetzt. Der Aufwand für eine Portierung hängt stark von der Heterogenität der Architektur ab, dauert aber i.allg. einige Personenmonate. Für die Erzeugung von Code hoher
Qualität ist die Gruppe der portable Compiler zur Zeit die einzig realistische (verwendet z.Bsp. im Gnu-C Compiler Projekt). Compiler-Compiler sind Gegenstand intensiver
Forschung. Bis jetzt wurde vor allem die Phase der Befehlsauswahl automatisiert. Maschinenabhängigere Optimierungen, die z.Bsp. Spezialregister oder Parallelverarbeitung
betreffen, sind nur für ganz spezielle Anwendungsbereiche verfügbar. Ein wichtiges
Codesign-Forschungsthema ist die gemeinsame Entwicklung von Zielarchitektur und
Compiler. Für Prozessoren mit relativ genau definerten Applikationsbereichen werden
typische Anwendungen in einer HLL geschrieben (Benchmarks). Parallel dazu wird eine
mögliche Zielarchitektur spezifiziert und mittels eines Compiler-Compilers eine Compiler generiert. Die Benchmarks werden dann compiliert und auf einem Simulator, der
die Zielarchitektur abbildet, ausgeführt. Durch Profiling kann die Qualität des Codes
(Ausführungszeit, Codegrösse) untersucht werden. Dadurch kann das System Compiler+Prozessor für den gegebenen Anwendungsmix optimiert werden.
Im folgenden wird ein Verfahren zur Codegenerierung bzw. zur Befehlsauswahl
vorgestellt, das heute in vielen portablen, aber auch in Compiler-Compilern verwendet wird: Codegenerierung durch Baumübersetzung. Danach werden Möglichkeiten zur
Modellierung von Zielarchitekturen beschrieben und zwei Fallbeispiele für retargetable
Compiler betrachtet.
4.5.1
Codegenerierung durch Baumübersetzung
Beispiel 4.32 Gegeben ist die folgende Anweisung:
a[i] := b + 1
Dabei sind a und i lokale Variablen, deren Laufzeitspeicheradressen durch consta und consti, relativ zum
Register SP (stackpointer), gegeben sind. Variable b ist eine globale Variable, die sich im Speicherplatz
memb befindet. Der ind-Operator ([]) behandelt sein Argument als Speicheradresse. Abb. 4.29 stellt den
Syntaxbaum für diese Anweisung mit den notwendigen Adressrechnungen dar.
Bei der Codegenerierung durch Baumübersetzung wird der Syntaxbaum durch Anwendung einer Folge von Umwandlungsgregeln auf einen Knoten reduziert. Die Umwandlungsgregeln haben die Form:
Ersetzung ← Muster
{Aktion}
(4.1)
Dabei stellt Ersetzung einen einzelnen Knoten, Muster einen Baum und Aktion eine
zu generierende Instruktion bzw. ein zu generierendes Codestück dar. Ein Baumübersetzungsschema ist eine Menge von Regeln der Form von Glg. (4.1).
4.5. RETARGETABLE COMPILER
111
:=
ind
+
+
memb
+
consta
const1
ind
regSP
+
consti
regSP
Abbildung 4.29: Syntaxbaum für die Anweisung a[i] := b + 1
reg i
+
reg i
{ADD Rj, Ri}
reg j
Abbildung 4.30: Baumumwandlungsregel für einen Additionsbefehl mit zwei Registeroperanden
Beispiel 4.33 Ein Beispiel einer Baumumwandlungsregel für die Addition zweier Registeroperanden
ist in Abb. 4.30 dargestellt. Falls ein gegebener Syntaxbaum einen Teilbaum enthält, der diesem Muster
gleicht, d.h., einen Teilbaum hat, dessen Wurzel mit dem Operator + markiert ist und dessen linker und
rechter Nachfolger in den Registern i und j stehen, so kann man diesen Teilbaum durch einen mit reg i
markierten Knoten ersetzen und die Anweisung ADD(Rj,Ri) generieren.
Die Codegenerierung besteht darin, schrittweise solange Baumumwandlungsregeln
anzuwenden, bis der Baum auf einen einzigen Knoten reduziert ist. Der gesamte Befehlssatz der Zielmaschine muss dafür in Form eines Baumübersetzungsschemas angegeben sein.
Beispiel 4.34 Abb. 4.31 zeigt eine Menge von Regeln, die Instruktionen einer Zielmaschine darstellen.
In Abb. 4.32 werden diese Regeln auf den Syntaxbaum in Abb. 4.29 angewandt. Zuerst wird Regel (1),
dann Regel (7) (dargestellt in Abb. 4.32a) angewendet. Es folgen die Regeln (6) (Abb. 4.32b)), (2), (8)
und (4) (Abb. 4.32d)). Bei Anwendung jeder Regel wird die entsprechende Aktion ausgeführt, d.h., Code
generiert. Die Reihenfolge der Anwendung der Regeln entspricht der Sequenz des generierten Codes, der
dann wie folgt aussieht:
MOV #a,R0
ADD SP,R0
ADD i(SP),R0
MOV b,R1
INC R1
KAPITEL 4. COMPILER UND CODEGENERIERUNG
112
(1)
reg i
const c
{MOV #c, Ri}
(2)
reg i
mem a
{MOV a, Ri}
:=
{MOV Ri, a}
(3) mem
mem a
reg i
(4) mem
:=
{MOV Rj,*Ri}
ind
reg j
reg i
(5) reg i
ind
{MOV c(Rj),Ri}
+
const c
(6) reg i
reg j
+
reg i
{ADD c(Rj), Ri}
ind
+
const c
(7) reg i
reg j
+
reg i
(8) reg i
{ADD Rj, Ri}
reg j
+
reg i
{INC Ri}
const 1
semantische Regel:
const muss 1 sein
if c=1 then {INC Ri}
else {ADD #c, Ri}
Abbildung 4.31: Baumübersetzungsschema
MOV R1,*R0
Einschränkungen der Anwendbarkeit einer Instruktion werden durch semantische
Prädikate von Mustern realisiert. Diese Prädikate müssen erfüllt sein, bevor ein Muster
passt. Bei Regel (8) in Abb. 4.31 zum Beispiel muss die Konstante c eins sein, damit der
Befehl INC Ri selektiert wird. Ansonsten wird der Befehl ADD #c,Ri generiert.
Die Überprüfung, welche Regeln passen, erfordert die Lösung eines pattern matching
Problems. Wenn mehrere Muster gleichzeitig passen, werden Heuristiken verwendet,
um ein Muster auszuwählen. Eine mögliche Heuristik ist, dasjenige Muster zu nehmen,
welches den grössten Teilbaum abdeckt. Baumübersetzungschemata werden auch mit
dynamischer Programmierung kombiniert [1]. Dies führt für Architekturen mit homogenen Registersätzen zu einer optimalen Befehlsauswahl. Es gibt Werkzeuge, die für
einen Prozessor, dessen Instruktionen als Baummuster (tree patterns) spezifiziert sind,
4.5. RETARGETABLE COMPILER
113
:=
=
a)
:=
b)
ind
ind
+
+
III: (6) {ADD i(SP), R0}
II: (7){ADD SP, R0}
+
+
reg 0
consta
memb
const1
+
reg 0
ind
regSP
const1
ind
+
+
I: (1) {MOV #a, R0}
consti
memb
regSP
consti
c)
regSP
d)
:=
:=
VI: (4) {MOV R1,*R0}
ind
ind
+
reg 0
memb
const1
reg 0
+
reg 1
const1
IV: (2) {MOV b, R1}
V: (8) {INC R1}
Abbildung 4.32: Anwendung eines Baumübersetzungsschemas
automatisch einen pattern matcher erzeugen.
4.5.2
Prozessormodellierung
Generell gibt es folgende Arten von Prozessormodellen:
• Verhaltensmodelle Dies sind Modelle, die den Instruktionssatz des Prozessors
beschreiben. Ein Instruktionssatzmodell kann manuell aus einem Datenblatt oder
automatisch aus einer geeigneten Prozessorbeschreibung extrahiert werden. Man
muss wissen, welche Instruktionen es gibt, welche Operanden diese benutzen und
wie viele Maschinenzyklen benötigt werden. Es gibt eine Reihe von Instruktionssatzsimulatoren, die den generierten Zielcode durch Simulation relativ schnell ausführen (ca. 2-3 Grössenordnungen langsamer als die wirkliche Zielmaschine). Der
Nachteil dieser Verhaltensmodelle ist ihre Ungenauigkeit. Zum Beispiel werden
die genauen Effekte des Pipelinings nicht berücksichtigt, was die Bestimmung der
Kostenwerte für Instruktionen erschwert. Verhaltensmodelle werden schon seit einigen Jahren benutzt und sind die Grundlage für alle Baumübersetzungsschemata
KAPITEL 4. COMPILER UND CODEGENERIERUNG
114
zur Codegenerierung.
• Strukturelle Modelle Diese Modelle beschreiben den Datenpfad und das Steuerwerk der Zielarchitektur auf der Registertransferebene. Strukturelle Modelle enthalten wesentlich mehr Details als Verhaltensmodelle und können somit potentiell
zu besseren Codegeneratoren führen. Die Erzeugung eines neuen Compilers dauert aber länger, da man die Zielarchitektur sehr genau spezifizieren muss. Simulationen auf der strukturellen Ebene sind maschinenzyklentreu. Allerdings benötigt
die Simulation grosser Programme sehr viel Rechenzeit.
• Gemischte Modelle Strukturelle und Verhaltensmodelle können auch gemischt
werden. Dabei werden die Instruktionen in einem Verhaltensmodell spezifiziert
und zusätzlich einige Komponenten durch ein strukturelles Modell. Ein Beispiel
für eine Prozessorbeschreibungssprache für gemischte Modellierung ist nML [19].
4.5.3
Fallbeispiele
Als Fallstudien für retargetable Compiler werden die Projekte FlexWare und CHESS
betrachtet. Eine Beschreibung dieser Projekte findet sich in [65] und [45].
• FlexWare
Das FlexWare System wurde bei SGS-Thomson entwickelt und besteht aus
zwei Hauptkomponenten: INSULIN, ein VHDL-basierter Instruktionssatzsimulator und CODESYN, ein Codegenerator. Der Codegenerator verwendet ein gemischtes (Struktur/Verhalten) Architekturmodell der Zielarchitektur. Die Codegenerierung wird mit einem Baumübersetzungsschema durchgeführt.
• CHESS
Das CHESS System wurde bei IMEC in Leuven entwickelt. Die Zielarchitekturen werden in der Prozessormodellierungssprache nML beschrieben, die Anwendungen in der Datenflusssprache DFL. Als interne Darstellung der Zielarchitektur
wird ein sogenannter Instruktionssatzgraph, der die Register und alle Instruktionen darstellt, verwendet. CHESS wurde auch mit einem Instruktionssatzsimulator
gekoppelt.
Kapitel 5
Systempartitionierung
Beim Entwurf von HW/SW-Systemen bezeichnet man die zwei Syntheseaufgaben Allokation und Bindung oft gemeinsam als Partitionierung. In der Allokation werden die
Systemkomponenten ausgewählt, in der Bindung wird die spezifizierte Funktionalität
des Systems auf diese Komponenten verteilt. Dabei müssen Entwurfsbeschränkungen
(constraints) eingehalten werden. Solche constraints können z.Bsp. Performanceanforderungen oder Kostenbeschränkungen sein. Partitionierungsprobleme auf der Systemebene zeichnen sich dadurch aus, dass die Anzahl der möglichen Partitionierungen in HW
und SW (Entwurfsraum) sehr gross sein kann.
Im folgenden wird vorgestellt, wie Systemsyntheseprobleme modelliert werden können, um sie mit automatisierten Methoden zu lösen. Danach wird das Partitionierungsproblem vorgestellt und allgemeine Partitionierungsalgorithmen sowie Verfahren zur
Lösung des HW/SW Partitionierungsproblems angegeben. Abschliessend werden Fallbeispiele für Partitionierungssysteme betrachtet, wobei zuerst Entwurfssysteme für HW
und danach für HW/SW-Systeme diskutiert werden.
5.1
Modelle für die Systemsynthese
Die Systemsynthese und damit auch die Partitionierung kann mit Graphen modelliert
werden. Man unterscheidet dabei drei Graphen:
• Problemgraph Dieser Graph beschreibt die zu implementierenden Objekte.
• Architekturgraph Dieser Graph gibt alle möglichen Zielarchitekturen, d.h. die
Komponenten und ihre Zusammenschaltung, an.
• Spezifikationsgraph Dieser Graph stellt Beschränkungen dar, denen die Abbildung von Objekten auf Komponenten unterliegt.
Problemgraph Ein Problemgraph ist ein Graph GP (VP , EP ), dessen Knoten funktionale
Objekte (z.Bsp. Tasks) und Kommunikationsobjekte (z.Bsp. ein Objekt, dessen Funktion
die Übertragung von Daten zwischen zwei Tasks ist) enthält. Die Kanten des Graphen
stellen Abhängigkeiten zwischen den Objekten dar.
115
KAPITEL 5. SYSTEMPARTITIONIERUNG
116
Beispiel 5.1 Abb. 5.1 zeigt einen Datenflussgraphen (DFG) und den zugehörigen Problemgraphen.
Man erhält den Problemgraphen, indem man für jede Kante des DFG einen Kommunikationsknoten einfügt.
1
2
1
2
5
6
3
3
7
4
4
Abbildung 5.1: DFG und Problemgraph
Architekturgraph Der Architekturgraph G A (VA , E A ) stellt alle verfügbaren Komponenten (Ressourcen) für die Implementierung dar. An Ressourcen unterscheidet man
funktionale Komponenten (Prozessoren, ASICs, FPGAs) und Kommunikationskomponenten (Busse, Punkt-zu-Punkt Verbindungen, etc.). Abhängig von dem Anwendungsfall können auch Speicherkomponenten zu den Ressourcen gezählt werden. Eine Kante
im Architekturgraphen bezeichnet eine gerichtete Kommunikationsverbindung. Wesentlich ist, dass der Architekturgraph alle möglichen Komponenten und Verbindungen zeigt.
In einer konkreten Implementierung müssen dann nicht notwendigerweise alle Komponenten und Kommunikationsverbindungen verwendet werden.
Beispiel 5.2 Abb. 5.2a) zeigt eine Architektur mit drei funktionalen Komponenten (einem RISCProzessor und zwei Hardwarmodulen HWM1 und HWM2) und zwei Kommunikationsressourcen (einen
bidirektionalen Bus BR1 und eine unidirektionale Punkt-zu-Punkt Verbindung BR2). Abb. 5.2b) zeigt den
entsprechenden Architekturgraphen.
Spezifikationsgraph Die Aufgabe ist es nun, die Knoten (funktionale und Kommunikationsobjekte) des Problemgraphen auf die Knoten (funktionale und Kommunikationsressourcen) abzubilden. Dazu konstruiert man den Spezifikationsgraphen
GS (VS , ES ), der aus dem Problemgraph, dem Architekturgraph und allen möglichen Abbildungen von Objekten zu Ressourcen besteht. Dabei beschränken Nebenbedingungen
die möglichen Abbildungen.
Beispiel 5.3 Abb. 5.3 zeigt den Spezifikationsgraphen für den Problemgraphen in Abb. 5.1 und den
Architekturgraph in Abb. 5.2. Der Knoten 1 kann zum Beispiel nur auf der Ressource v RISC ausgeführt
5.1. MODELLE FÜR DIE SYSTEMSYNTHESE
117
v_RISC
v_HWM1
HWM1
RISC
BR1
Shared bus
BR2
Point-to-point bus
v_BR1
HWM2
v_BR2
v_HWM2
a)
b)
Abbildung 5.2: Architektur und Architekturgraph
werden, der Knoten 3 auf v RISC und v HW M1 . Das Beispiel zeigt auch, dass es sinnvoll sein kann, Kommunikationsknoten auf funktionale Ressourcen abzubilden. Wenn Vorgänger und Nachfolgerknoten eines
Kommunikationsknoten auf dieselbe funktionale Ressource abgebildet werden (z.Bsp. auf einen Prozessor),
wird die Kommunikation zwischen den Knoten eine interne Kommunikation sein und keine externen
Kommunikationskanäle beanspruchen. In diesem Fall wird auch der Kommunikationsknoten auf den Prozessor abgebildet.
Das Modell des Spezifikationsgraphen erlaubt eine flexible Darstellung des Wissens
über die Auswahl von Systemkomponenten und Abbildungsmöglichkeiten von Objekten auf diese Komponenten. Eine konkrete Implementierung besteht aus einer Allokation, einer Bindung und einem Ablaufplan. Eine Allokation ist eine Auswahl der Ressourcen des Architekturgraphen, d.h. eine Teilmenge der Knoten von G A . Eine Bindung ist
eine Teilmenge der Kanten von GS , die von den Knoten des Problemgraphen zu Knoten
des Architekturgraphen führen.
Um eine gültige Bindung zu erhalten, muss man üblicherweise weitere Bedingungen
einführen. Zum Beispiel muss von jedem Knoten des Problemgraphen genau eine Kante
ausgehen, d.h., ein Objekt soll nicht gleichzeitig auf zwei Ressourcen abgebildet werden.
Wenn Vorgänger und Nachfolger eines Kommunikationsknoten im Problemgraph auf
dieselbe Ressource abgebildet werden, muss der Kommunikationsknoten auch auf diese Ressource abgebildet werden. Werden Vorgänger und Nachfolger auf verschiedene
Ressourcen abgebildet, muss der Kommunikationsknoten auf eine Kommunikationsressource abgebildet werden, die die zwei Ressourcen verbindet. Für die Modellierung
und die Zusatzbedingungen gibt es viele mögliche Varianten. Man könnte zum Beispiel
zulassen, dass Vorgänger und Nachfolger eines Kommunikationsknoten an Ressourcen
gebunden werden, die nicht direkt verbunden sind. Um die Kommunikation zu reali-
KAPITEL 5. SYSTEMPARTITIONIERUNG
118
1
v_RISC
5
3
v_BR1
7
v_HWM1
2
v_BR2
6
v_HWM2
4
Abbildung 5.3: Spezifikationsgraph
sieren, muss eine dazwischen liegende Ressource als routing Knoten genutzt werden.
Eine gültige Allokation ist eine Allokation, für die es mindestens eine gültige Bindung gibt. Hat man eine Allokation und eine Bindung bestimmt, muss ein Ablaufplan
gefunden werden. Dazu benötigt man die Ausführungszeiten der funktionalen Objekte auf den zugewiesenen funktionalen Ressourcen und die Übertragungszeiten für die
Kommunikationsobjekte auf den zugewiesenen Kommunikationsressourcen.
Eine gültige Implementierung besteht dann schliesslich aus einer gültigen Allokation,
einer gültigen Bindung und einem Ablaufplan. Eine gültige Implementierung muss aber
noch nicht den Entwurfsbeschränkungen genügen. Die Kosten können zu hoch oder die
Performance zu gering sein.
Abbildung von Tasks auf ein homogenes Multiprozessorsystem Ein Spezialfall eines allgemeinen Syntheseproblems ist die Abbildung einer Menge von k Tasks mit bekannten Ausführungszeiten auf eine Architektur mit m gleichartigen Prozessoren mit
lokalem Speicher, die über einen gemeinsamen Bus kommunizieren. In Abb. 5.4 ist ein
Beispiel für diesen Fall angegeben. Der Problemgraph besteht aus k Knoten, von denen einer ausgezeichnet ist, indem er Daten an alle anderen Knoten schickt. Die anderen Knoten sind voneinander unabhängig. Solche Problemgraphen ergeben sich oft bei
der Parallelisierung von Algorithmen. Die Zielarchitektur besitzt drei Prozessorelemen-
5.1. MODELLE FÜR DIE SYSTEMSYNTHESE
119
1
M
M
M
PE
PE
PE
....
2
3
k
bus
Abbildung 5.4: Problemgraph und Zielarchitektur
te (PE) mit lokalem Speicher (M).
In Abb. 5.5 ist der Spezifikationsgraph für dieses Problem dargestellt. Gesucht ist
eine Implementierung, die eine möglichst kurze Ausführungszeit besitzt. In diesem Fall
wird die Bindung dadurch vereinfacht, dass i) alle Prozessoren gleichartig sind und
somit jeder funktionale Knoten des Problemgraphen auf jeden Prozessor abbildbar ist
und ii) jede Kommunikation entweder intern ist oder den gemeinsamen Bus benutzen
muss. Die Hauptaufgabe liegt bei diesem Problem im Finden eines Ablaufplanes.
1
PE1
2
bus
....
PE2
PE3
k
Abbildung 5.5: Spezifikationsgraph
HW/SW Partitionierung Ein weiterer Spezialfall des allgemeinen Syntheseproblems
ist die HW/SW Partitionierung in ihrer einfachsten Variante. Die funktionalen Objekte
KAPITEL 5. SYSTEMPARTITIONIERUNG
120
des Problemgraphen werden dabei auf zwei funktionale Ressourcen abgebildet, eine
HW- und eine SW-Ressource. Typische Zielarchitekturen sind Ein-Prozessor/Ein-ASIC
Systeme. Abb. 5.6 zeigt ein Beispiel und den dazugehörigen Architekturgraphen. Um
zu modellieren, dass auf einer HW-Ressource nur eine bestimmte Anzahl von Gattern
realisiert werden kann, werden Kapazitätzgrenzen für die Ressource eingeführt, die die
Bindungsmöglichkeiten beschränken.
Prozessor
Prozessor
Bus
ASIC
Bus
ASIC
Abbildung 5.6: Architektur und Architekturgraph für ein einfaches HW/SWPartitionierungsproblem
5.2
Partitionierung
Bei der Partitionierung werden Objekte einer Menge (Objekte des Problemgraphen) zu
verschiedene Blöcken (Ressourcen des Architekturgraphen) zugeteilt. Ein wesentlicher
Punkt für die Partitionierung ist der Abstraktionsgrad der Spezifikation. Je nach Abstraktionsebene im Entwurf können die Objekte des Problemgraphen Tasks, Operationen,
Boolesche Funktionen, etc., und die Ressourcen Prozessoren, ASICs, FPGAs, einzelne
Rechenwerke, Gatter, Transistoren sein. Man unterscheidet zwischen funktionaler Partitionierung und struktureller Partitionierung. Eine strukturelle Partitionierung wird durchgeführt, nachdem die Struktur des Systems bereits vorliegt. Der übliche Abstraktionsgrad ist die Beschreibung einer Funktion auf Registertransferebene. Dort stellt sich oft
die Aufgabe, die Objekte auf mehrere Hardwareblöcke (z.Bsp. mehrere ASICs) aufzuteilen. Nachdem strukturelle Partitionierung auf einer feinen Ebene des Systementwurfes
eingesetzt wird, ist es hier meist leicht, genaue Abschätzungen für die Performance
und Kosten der Objekte zu erhalten. Andererseits können kaum mehr Abwägungen
zwischen Entwurfskriterien getroffen werden. Eine funktionale Partitionierung ist eine
Partitionierung des Systemverhaltens. Bei der funktionalen Partitionierung lassen sich
Alternativen leichter untersuchen, die Genauigkeit der Abschätzungen hingegen ist nur
gering.
Zur Bewertung von Partitionen werden verschiedenste Metriken verwendet. Beispiele
sind Kosten C, Ausführungszeit L, Datenrate R, Leistungsverbrauch P, Hardwarefläche
A, Anzahl von Pins, Testbarkeit, Fehlertoleranz, Grösse von Daten- und Programmspeicher. Eine Zielfunktion dient dazu, verschiedende Metriken in einem skalaren Gütemass
5.3. ALLGEMEINE PARTITIONIERUNGSALGORITHMEN
121
zu vereinigen. Den Wert einer Gütefunktion nennt man Kostenwert. Ein Beispiel für eine
Zielfunktion ist
f (C, L, P) = k1 · hC (C, C̄ ) + k2 · h L ( L, L̄) + k3 · h P ( P, P̄)
Dabei sind hi Funktionen, die angeben, wie stark ein Kriterium (C, L, P) die Entwurfsbeschränkungen verletzt. Im Idealfall ist hi gleich 0. Die Konstanten k i dienen dazu, die verschiedenen Kriterien zu gewichten. Oft werden die Funktionen hi auf die Nebenbedingungen normiert. Durch Wahl entsprechender Konstanten k i kann man dann
erreichen, dass der Kostenwert im Intervall [0 . . . 1] liegt.
Manche Partitionierungsverfahren verwenden neben Zielfunktionen oft auch sogenannte Closenessfunktionen. Im Unterschied zu einer Zielfunktion, die verschiedene Metriken einer gegebenen Partition zu einer Bewertungszahl zusammenfasst, gibt eine eine
Closenessfunktion an, wie wünschenswert die Zusammengruppierung einzelner Objekte
ist.
Das Problem bei der funktionaler Partitionierung ist, dass die Metriken einer konkreten Implementierung nicht exakt bekannt sind. Möglichkeiten, Kostenwerte zu erhalten,
sind i) einen Prototypen zu erzeugen (durch Synthese bzw. Compilation) oder ii) gute
Schätzwerte für die Parameter zu finden.
5.3
Allgemeine Partitionierungsalgorithmen
Definition 5.1 (Partitionierungsproblem) Gegeben ist eine Menge von Objekten O =
{o1 , o2 , · · · , on }. Gesucht ist eine Partition P = { p1 , p2 , · · · , pm }, so dass
• p1 ∪ p2 ∪ · · · ∪ pm = O,
• pi ∩ p j = { } ∀i, j : i 6= j, und
• die Kosten c( P) minimal sind.
Bei der Systempartitionierung wird jedes Element aus einer Menge von funktionalen
Objekten auf genau ein Element aus einer Menge von Systemkomponenten abgebildet.
Das Ziel ist, eine Partitionierung P mit einem möglichst geringen Kostenmass c( P) zu
finden. Bei n funktionalen Objekten und m Systemkomponenten gibt es O(mn ) mögliche Partitionierungen. Das systematische Durchsuchen des gesamten Lösungsraumes
(exhaustive search) ist deshalb schon für kleine Werte von n und m unrealistisch. Das allgemeine Partitionierungsproblem ist NP-vollständig. Will man es exakt lösen, ist man
auf Verfahren angewiesen, die im worst-case eine exponentielle Laufzeit aufweisen. Deshalb wurden eine Reihe von Heuristiken für das Partitionierungsproblem entwickelt.
Diese Heuristiken kann man in die folgenden zwei grundlegenden Ansätze einteilen:
• konstruktive Algorithmen: Dies sind Verfahren, die eine Partition durch schrittweises Hinzunehmen bzw. Gruppieren von Objekten oder Gruppen von Objekten erzeugen. Zur Gruppierung werden Closeness-Funktionen verwendet. Eine gültige
Partition ist erst am Ende des Verfahrens vorhanden.
KAPITEL 5. SYSTEMPARTITIONIERUNG
122
• iterative Algorithmen: Diese Algorithmen starten mit irgendeiner Anfangspartition und verbessern diese iterativ unter Berechnung einer Zielfunktion. Hier ist in
jedem Iterationsschritt eine gültige, wenn auch nicht notwendigerweise gute, Lösung vorhanden.
In den folgenden Abschnitten werden auch einige allgemeine Optimierungsverfahren behandelt: Simulated Annealing, Evolutionäre Algorithmen und Integer Linear Programs (ganzzahlige lineare Programme).
5.3.1
Konstruktive Partitionierungsverfahren
Random Mapping Dieses einfache Verfahren weist jedes Objekt zufällig einer Gruppe
(z.Bsp. HW oder SW) zu und besitzt eine Zeitkomplexität von O(n). Random mapping
wird häufig dazu verwendet, eine Anfangspartition für iterative Verfahren zu erzeugen.
Hierarchisches Clustering Hierarchisches Clustering bezeichnet eine Klasse konstruktiver Algorithmen [36], [44], [54], [11], die schrittweise Objekte zusammengruppieren.
Am Anfang stellt jedes Objekt eine eigene Gruppe dar. Objekte werden aufgrund ihrer
Closenesswerte gruppiert; nach jeder Gruppierung werden die Closenesswerte neu berechnet. Das Verfahren terminiert, wenn die gewünschte Anzahl von Gruppen erreicht
ist oder die Closenesswerte bestimmte Schranken unterschreiten. Ein Algorithmus, der
eine Zeitkomplexität von O(n2 ) besitzt, wird durch den Pseudocode in Abb. 5.7 beschrieben.
Beispiel 5.4 Abb. 5.8 zeigt die Anwendung des Algorithmus anhand eines Beispiels mit vier Objekten,
deren Closenesswerte durch die Kantengewichte gegeben sind. Zu Beginn des Verfahrens wird die Anfangspartition P = { p1 , p2 , p3 , p4 } mit numblocks = 4 und den Closenesswerten c12 = 30, c13 = 25,
c14 = 10, c23 = 15, c24 = 10 und c34 = 10 (siehe Abb. 5.8 links) gebildet.
Das Paar p1 , p2 weist nun mit c12 = 30 den grössten Closenesswert auf und wird deshalb zusammengruppiert. Wir erhalten die neue Partition P = P \ p1 \ p2 ∪ p0 mit p0 = { p1 , p2 }, d.h. P = { p3 , p4 , p0 }.
Als Terminierungsbedingung wählen wir die Bedingung numblocks == 2, d.h. eine Partition mit
zwei Blöcken. Innerhalb der WHILE-Schleife wird nun die Closeness neu berechnet. Es wird zunächst
die Closeness des neuen Blocks p0 zu p3 bestimmt. Wir nehmen an, dass die Closeness zweier Blöcke
p x , py das arithmetische Mittel der Gewichte aller Objektpaare oi , o j mit oi ∈ p x und o j ∈ py ist:
c30 = 1/2 ∗ (c13 + c23 ) = 20. Genauso erhalten wir c40 = 1/2 ∗ (c14 + c24 ) = 10. Die neue Partition und die neuen Gewichte sind ebenfalls in Abb. 5.8 dargestellt. Da nun die beiden Blöcke p3 und p0
die grösste Closeness aufweisen, werden diese beiden im zweiten Schritt zusammengruppiert. Wir erhalten die Partition P = { p00 , p4 } mit p00 = {o1 , o2 , o3 } und numblocks = 2. Das Verfahren terminiert an
dieser Stelle.
Dieses Verfahren generiert einen sogenannten clustertree, in dem die horizontalen
Linien Schnitte (cutlines) beschreiben, die Partitionen sind. Aus den generierten Partitionen kann man durch Bewertung mit einer Zielfunktion eine geeignete auswählen.
Ein weiterer Ansatz ist das multi-stage clustering, bei dem mehrere Closenessmetriken verwendet werden. Nachdem basierend auf einer Closenessfunktion der clustertree
erzeugt und eine Partition gewählt wurde, wird ausgehend von dieser Partition erneut
hierachisches Clustering, aber mit einer anderen Closenessfunktion, durchgeführt [44].
5.3. ALLGEMEINE PARTITIONIERUNGSALGORITHMEN
PROCEDURE HIERARCHICAL_CLUSTERING(O) {
/* Initialisiere jedes Objekt als eine Gruppe */
P : = { };
FOR i = 1 TO n
pi := {oi };
P : = P ∪ pi ;
ENDFOR
/* Berechne Closeness zwischen den Objekten */
FOR i = 1 TO n
FOR j = 1 TO n
ComputeCloseness(pi , p j );
ENDFOR
ENDFOR
numblocks = n;
k := n + 1
/* Vereinigen der Objekte und Closenessneuberechnung */
WHILE (Terminate(P) == FALSE)
pi , p j := FindClosestObjects(P);
p k : = { p i , p j };
P := P \ pi \ p j ∪ pk ;
numblocks := numblocks − 1;
FOREACH Block pl ∈ P \ pk
ComputeCloseness(pl , pk );
ENDFOR
k := k + 1;
ENDWHILE
RETURN(P);
END PROCEDURE
Abbildung 5.7: Pseudocode für Hierarchisches Clustering
123
KAPITEL 5. SYSTEMPARTITIONIERUNG
124
a)
c)
b)
30
2
1
25
15 10
10
20
5
3
6
3
10
4
10
10
4
4
p
6
p
5
p p p p
1 2 3 4
1 2 3 4
1 2 3 4
1 2 3 4
Abbildung 5.8: Hierarchisches Clustering
Als Closnessmetrike wird oft die Summe der Gewichte von Kanten, die zwischen
Objekten in verschiedenen Blöcken verlaufen, verwendet. Das führt dazu, dass die
Blöcke sehr gross werden. Eine heuristische Zielfunktion für das Clustering, die einerseits die Summe der Kantengewichte berücksichtigt, anderseits aber auch die Ausgewogenheit der Blockgrösse, wurde in [39] unter den Namen ratiocut vorgeschlagen.
Definition 5.2 (Ratiocut) Sei P = { pi , p j } und cut( P) die Summe der Gewichte der Kanten zwischen pi und p j . Die Anzahl der Objekte in pi und p j (Blockgrössen) sei size( pi ) und
size( p j ). Das Ratio von P ist gegeben durch
ratio =
cut( P)
size( pi )size( p j )
Möglichst kleine Werte von ratio sind erwünscht. Während der Zähler viele Objekte
zusammengruppieren würde, sorgt der Nenner für ausgewogene Blockgrössen.
Die Verfahren der konstruktiven Partitionierung haben generell das Problem, dass
es sehr schwierig sein kann, Closenessfunktionen zu definieren, die alle Entwurfsbeschränkungen geeignet beinhalten.
5.3.2
Iterative Partitionierungsverfahren
Kernighan-Lin Algorithmus Zahlreiche iterative Partitionierungsverfahren basieren
auf einem Algorithmus zur Generierung von Bipartitionen (Partitionen mit zwei
Blöcken), der von Kernighan und Lin vorgestellt [37], und in [18] und [42] weiterentwickelt und verbessert wurde. Das Verfahren, das vor allem beim VLSI-Entwurf eingesetzt wird, minimiert die Anzahl von Kanten zwischen zwei Blöcken. Dazu wird folgende Iteration durchlaufen:
• Bestimme für jedes Objekt den Kostengewinn, wenn man das Objekt in die andere
Gruppe umgruppiert.
5.3. ALLGEMEINE PARTITIONIERUNGSALGORITHMEN
125
• Gruppiere dasjenige Objekt in die andere Gruppe um, das den grössten Kostengewinn verursacht.
Dieser Algorithmus kann nicht aus einem lokalen Optimum (wo keine einzelne Verschiebung die Kosten verringert, aber vielleicht das gleichzeitige Verschieben mehrerer
Objekte) entweichen. Kernighan und Lin haben eine Erweiterung vorgeschlagen, bei der
das Objekt umgruppiert wird, das den grössten Kostengewinn oder das kleinste Kostenwachstum verursacht. Um zu verhindern, dass ein Objekt mehrfach umgruppiert wird,
kann jedes Objekt nur einmal umgruppiert werden. Dabei werden Objekte zunächst versuchsweise umgruppiert. Nachdem ein Objekt versuchsweise umgruppiert ist, sucht man
unter den restlichen n − 1 Objekten dasjenige aus, das am besten umgruppiert werden
kann usw., bis jedes Objekt einmal umgruppiert worden ist. In einer Tabelle merkt man
sich dabei für jeden Schritt, wie die Kosten der aktuellen Partition wären, wenn die
bisherigen Umgruppierungen tatsächlich durchgeführt würden. Nachdem alle Objekte
versuchsweise einmal umgruppiert wurden, wird die Partition mit den geringsten Kosten ausgewählt und nur die Objekte tatsächlich umgruppiert, die zu dieser Partition
führen. Diese Schritte stellen eine Iteration des Algorithmus dar. Der Algorithmus iteriert nun ausgehend von einer solchen Partition, bis keine neue Partition mit geringeren
Kosten gefunden werden kann.
Dieses Verfahren hat sich als robust erwiesen, und besitzt eine Zeitkomplexität von
O(n3 ). Man kann das Verfahren auch auf Partitionen mit m Blöcken erweitern, indem
man in jedem Schritt nicht nur das Objekt, sondern auch den Block bestimmt, der den
grössten Kostengewinn bzw. das kleinste Kostenwachstum verursacht. Die Zeitkomplexität ist dann O(mn3 ).
Simulated Annealing (SA) Dieses Standardoptimierungsverfahren [38] unterscheidet
sich von dem Kernighan-Lin Verfahren dadurch, dass ein Objekt mehrfach umgruppiert
werden kann. Die Struktur von SA wird durch den Pseudocode in Abb. 5.9 beschrieben. Ausgehend von einer Anfangspartition wird eine simulierte Temperatur langsam
erniedrigt. Die Funktion RandomMove() kreiert eine neue Partition durch zufälliges Auswählen und Umgruppieren eines Objektes in P. Die Funktion Accept() bestimmt, ob eine
neue Partition angenommen wird. In [38] wurde Accept() definiert mit:
Accept(delta_cost, temp) = min(1, e
− deltacost
temp
)
Das bedeutet, dass die Wahrscheinlichkeit für eine kostensteigernde Umgruppierung
mit abnehmender Temperatur immer kleiner wird. Die Prozedur Equilibrum() bestimmt,
ob der Partitionierungsprozess bei der aktuellen Temperatur temp ein Equilibrum erreicht hat. Dies tritt (approximativ) dann ein, wenn nach einer gewissen Anzahl von
Iterationen mit der aktuellen Temperatur keine Verbesserung mehr eintritt. Die Funktion DecreaseTemp() erniedrigt die Temperatur auf α × temp, wobei 0 < α < 1. Die Funktion Frozen() realisiert das Abbruchkriterium, wobei üblicherweise temp ≤ tempmin als
Abbruchkriterium genommen wird.
Simulated Annealing (simuliertes Ausglühen) ist angelehnt an den physikalischen
Vorgang des langsamen Abkühlens eines Metalls oder Glases aus der Schmelze. Dort
wird ein globales Energieminimum erreicht, wenn die Temperatur so langsam erniedrigt
126
KAPITEL 5. SYSTEMPARTITIONIERUNG
PROCEDURE SIMULATED-ANNEALING(P)
temp = Anfangstemperatur;
cost = c( P);
WHILE (Frozen == FALSE)
WHILE (Equilibrum == FALSE)
P0 = RandomMove(P);
cost0 = c( P0 );
deltacost = cost0 − cost;
IF (Accept(deltacost, temp) > Random[0,1))
P = P0 ;
cost = cost0 ;
ENDIF
ENDWHILE
temp = DecreaseTemp(temp);
ENDWHILE
RETURN(P);
END PROCEDURE
Abbildung 5.9: Pseudocode für Simulated Annealing
5.3. ALLGEMEINE PARTITIONIERUNGSALGORITHMEN
127
wird, dass sich immer ein thermisches Gleichgewicht bilden kann. Entsprechend findet
das Rechenverfahren SA ein globales Optimum [67, 48] unter den Annahmen, dass:
1. der Prozess bei jeder Temperatur das Equilibrum erreicht, und
2. die Temperatur beliebig langsam erniedrigt wird.
Die tatsächliche Zeitkomplexität des Verfahrens hängt stark von den Prozeduren ab
und kann zwischen exponentiell und konstant variieren. Je länger die Laufzeit ist, desto
besser werden die Ergebnisse. Oft werden die Funktionen Equilibrum(), DecreaseTemp()
und Frozen() so konstruiert, dass polynomielle Laufzeit erzielt wird. Simulated Annealing ist ein allgemeines Optimierungsverfahren, dass oft für Problemklassen eingesetzt
wird, für die keine effizienten Algorithmen bekannt sind. Im Vergleich zu anderen Verfahren zeichnet sich SA dadurch aus, dass eine Umgruppierung, die eine schlechtere
Lösungen darstellt, zugelassen wird. Dadurch kann SA aus lokalen Minima entweichen.
5.3.3
Partitionierung mit Evolutionären Algorithmen
Bei einem evolutionären Algorithmus (EA) wird nicht nur eine Partition erzeugt, sondern eine Menge von Partitionen. Diese Lösungsmenge stellt eine Population dar. Diese Population wird iterativ durch Auswahlverfahren verbessert. Die für EAs typischen
Auswahlverfahren sind Selektion, Kreuzung und Mutation. EAs gehören genauso wie Simulated Annealing zu den Standardverfahren der kombinatorischen Optimierung.
5.3.4
Partitionierung mit linearer Programmierung
Partitionierungsprobleme können auch in Form eines ganzzahligen linearen Programms
(ILP, integer linear program) formuliert werden. Die Zugehörigkeit eines Objektes oi ∈ O
zu einem Block pk wird durch die binäre Variable xi,k ausgedrückt. xi,k = 1 bedeutet,
dass o j zum Block pk gehört. Die Kosten für die Gruppierung des Objekts oi in pk sind
durch ci,k gegeben. Das ILP kann dann wie folgt aussehen:
xi,k ∈ {0, 1} 1 ≤ i ≤ n, 1 ≤ k ≤ m
(5.1)
m
∑ xi,k = 1
1≤i≤n
(5.2)
∑ xi,k · ci,k
1≤k≤m
(5.3)
k =1
n
ck =
i =1
Als zu minimierende Zielfunktion wird ∑m
k =1 ck gewählt. Kosten, die durch die Anzahl der Verbindungen zwischen Blöcken entstehen oder andere Beschränkungen, wie
maximale und minimale Anzahl von Objekten in Blöcken, lassen sich durch weitere Nebenbedingungen zum ILP modellieren. ILPs können exakt mit Branch-and-Bound Algorithmen [59] gelöst werden. ILPs sind NP-vollständig und exakte Verfahren zu deren
Lösung haben daher im worst-case eine exponentielle Laufzeit. Deshalb sind ILPs meist
nur für kleine Problemgrössen sinnvoll anwendbar. Weiterhin liegen auf Systemebe oft
nichtlineare Beschränkungen vor, die die Formulierung eines ILPs erschweren.
128
KAPITEL 5. SYSTEMPARTITIONIERUNG
PROCEDURE GREEDY_PARTITIONING
REPEAT
Pold =P;
FOR i = 1 TO n
IF ( f (Move(P,oi )) < f ( P))
P = Move(P,oi );
ENDIF
ENDFOR
UNTIL (P == Pold )
END PROCEDURE
Abbildung 5.10: Pseudocode für ein greedy Partitionierungsverfahren
5.4
Algorithmen zur HW/SW-Partitionierung
Das HW/SW-Partitionierungsproblem ist ein Spezialfall des allgemeinen Partitionierungsproblems. Es ist im einfachsten Fall ein Bipartitionierungsproblem, bei
dem die Objekte in einen Hardware- und einen Softwareblock eingeteilt werden, P =
{ pSW , p HW }. Oft verwendete Metriken sind dabei die benötigte Fläche einer Hardwarealisierung und die Performance. Man unterscheidet Ansätze zur HW/SW-Partitionierung
danach, ob sie softwareorientiert (z.Bsp. [17]) oder hardwareorientiert (z.Bsp. [27]) sind.
Im softwareorientierten Fall geht man von einer Anfangspartition in SW aus, P =
{O, {}}. Die Motivation für diese Anfangspartition ist, dass in SW auch alle komplexen Funktionen realisiert werden können (z.Bsp. Betriebssystemaufrufe), die in HW nur
sehr schwer zu implementieren wären. Der Nachteil einer solchen Partitionierung kann
darin bestehen, dass Performanceanforderungen nicht erfüllt werden. Dann muss man
bestimmte Objekte in die HW migrieren.
Im hardwareorientierten Fall geht man von einer Anfangspartitionierung in HW aus,
P = {{}, O}. Der Vorteil dieses Ansatzes ist, dass die Performanceanforderungen erfüllt
werden (andernfalls gibt es überhaupt keine Implementierung, die den Anforderungen
genügt). Der Nachteil ist, dass die Implementierung sehr komplex und teuer ist. Deshalb
muss man hier Objekte in die SW migrieren.
Eine Klasse von Algorithmen, die ausgehend von einer Anfangspartitionierung Objekte in den jeweils anderen Block migriert, bis keine Verbesserung mehr möglich ist, ist
nach dem Schema in Abb. 5.10 aufgebaut.
Dabei wird die Prozedur Move(P, oi ) verwendet, die eine neue Partition P0 erzeugt,
indem das Objekt oi in den jeweils anderen Block migriert wird. Dieser Algorithmus ist
ein greedy-Algorithmus, da die Objekte solange wie nur möglich (gierig) umgruppiert
werden.
Der Algorithmus in Abb. 5.11 ist eine Variante des in [27] beschriebenen Verfahrens, das HW-orientiert ist. Dieses Verfahren ist ein greedy-Verfahren, wobei jedoch
Migrationen von Objekten von HW nach SW nur dann durchgeführt werden, wenn die
Performancebedingungen erfüllt bleiben und eine Verbesserung der Zielfunktion erzielt
wird. Die Anfangspartition erfüllt diese Beschränkungen per definitionem. Die Funktion SatisfiesPerformance() liefert TRUE, falls P die Performanceanforderungen erfüllt.
5.5. ENTWURFSSYSTEME ZUR FUNKTIONALEN PARTITIONIERUNG
129
PROCEDURE HW_ORIENTED_GREEDY
P = {O, { }}
REPEAT
Pold =P;
FOREACH (oi ∈ p HW )
TryMove(P,oi );
ENDFOR
UNTIL (P == Pold )
END PROCEDURE
PROCEDURE TryMove(P,oi )
IF SatisfiesPerformance(Move(P,oi )) AND
f (Move(P,oi )) < f (P))
P = Move(P, oi );
FOREACH (o j ∈ Successors(oi ))
TryMove(P,o j );
ENDFOR
ENDIF
END PROCEDURE
Abbildung 5.11: Pseudocode eines hardwareorientierten greedy Partitionierungsverfahrens
Falls ein Objekt oi in die SW migriert wurde, wird versucht, alle benachbarten Objekte
ebenfalls zu migrieren. Der Nachteil eines solchen Greedyalgorithmus ist, dass er aus
einem lokalem Minimum nicht mehr entweichen kann.
Der duale, softwareorientierte Ansatz ist in [17] angewandt. In der Anfangspartition
ist alles in SW realisiert. Dann wird solange in die HW migriert, bis die Performanceanforderungen erfüllt sind. Die Partitionierung erfolgt mit Simulated Annealing, was den
Vorteil hat, dass aus einem lokalen Minimum entwichen werden kann.
5.5
5.5.1
Entwurfssysteme zur funktionalen Partitionierung
Funktionale Partitionierung im Hardwareentwurf
Yorktown Silicon Compiler (YSC) Der YSC [11] benutzt als Eingabe eine funktionale
Beschreibung auf der Ebene von arithmetischen und logischen Ausdrücken. Das Ziel
ist die Partitionierung der Operationen (Multiplikationen, Additionen, etc.) auf funktionale Blöcke des Datenpfades. Jeder Block wird anschliessend durch Logiksynthesetools
weiter verfeinert. Die Anzahl der Blöcke wird durch den Partitionierungsalgorithmus
bestimmt. Dazu wird hierarchisches Clustering mit folgender Closenessfunktion eingesetzt:
Closeness( pi , p j ) =
sharedwires( pi , p j )
maxwires( P)
c2
×
maxsize
min{size( pi ), size( p j )}
! c3
×
maxsize
size( pi ) + size( p j )
!
KAPITEL 5. SYSTEMPARTITIONIERUNG
130
In dieser Formel bedeuten:
• sharedwires( pi , p j ) = c1 × commoninputs( pi , p j ) + internalwires( pi , p j ),
• commoninputs( pi , p j ): Anzahl der Bits von gemeinsamen Eingängen, d.h., Eingängen, die sowohl von Objekten des Blocks pi als auch von Objekten des Blocks p j
benutzt werden,
• internalwires( pi , p j ): Anzahl der Verbindungen (in Bit) zwischen Objekten oi ∈ pi
und o j ∈ p j ,
• maxwires( P): max pi ,p j ∈ P:i6= j {sharedwires( pi , p j )},
• size( pi ): abgeschätzte Transistorzahl für die Realisierung von pi ,
• maxsize: maximale Grösse (in Transistoren) eines Blocks,
• c1 , c2 , c3 : Konstanten.
Der erste Term favorisiert das Gruppieren von Blöcken, die viele gemeinsame Daten
teilen. Der zweite Term sorgt für ausgeglichene Blöckgrössen und der dritte Term bewirkt, dass jeder einzelne Block eine gewisse Grösse nicht überschreitet. Das Clusteringverfahren im YSC terminiert, wenn die maximale Closeness zwischen zwei Blöcken eine
vorgegebene Schranke unterschreitet. Die erzielten Ergebnisse können dahingehend verbessert werden, dass man zusätzlich Ergebnisse der Logikminimierung einfliessen lässt.
BUD Im System BUD [54, 53] werden Partitionen mit der Granularität von CDFGs
(Multiplizier-, Addier-, logische Operationen, etc.) generiert. Diese Operationen sollen
an Module eines Datenpfades gebunden werden, wobei eine Partition eine Allokation
und eine Bindung darstellt. Der eingesetzte Algorithmus ist ein hierachisches Clusteringverfahren. Zu Beginn des Verfahrens wird die Closeness zwischen jedem Paar von
Operationen nach folgender Funktion bestimmt:
Closeness(oi , o j ) =
+
shareddata(oi , o j )
totaldata(oi , o j )
+
f cost(oi ) + f cost(o j ) − cost(oi , o j )
cost(oi , o j )
− n × par (oi , o j )
In dieser Formel bedeuten:
• shareddata(oi , o j ): Anzahl der von beiden Operationen oi , o j gemeinsam benutzten
Daten in Bits. Gemeinsame Nutzung von Daten tritt auf, wenn entweder beide
Knoten einen gemeinsamen direkten Vorgänger im CDFG besitzen, oder wenn o j
direkter Nachfolger von oi ist (oder umgekehrt).
5.5. ENTWURFSSYSTEME ZUR FUNKTIONALEN PARTITIONIERUNG
131
• totaldata(oi , o j ): Man zieht im CDFG eine Hülle um oi und o j und summiert die
Anzahl von Bits der Daten, die über Kanten in und aus dieser Hülle transportiert
werden.
• f cost(oi ): Kosten der funktionalen Einheit, die man benötigt, um oi zu realisieren.
• cost(oi , o j ): minimale Kosten (bzgl. Fläche und Performance) der benötigten funktionalen Einheiten, um beide Operationen zu implementieren.
• par (oi , o j ): 1, falls oi und o j parallel ausgeführt werden können, 0 sonst.
Der erste Term favorisiert die Zusammengruppierung von Objekten mit gemeinsamen Daten. Das Ziel ist dabei, die für die Verdrahtung benötigte Fläche zu reduzieren.
Der zweite Term favorisiert das Gruppieren von Operationen, die eine Ressource teilen können (z.Bsp. eine Addition und eine Subtraktion, die auf einer ALU ausgeführt
werden können). Der dritte Term soll verhindern, dass nebenläufige Operationen durch
Gruppierung sequentialisiert werden. Die einzelnen Terme können in BUD noch gewichtet werden.
BUD generiert basierend auf dieser Closenessfunktion einen hierarchischen Clustertree. Zur Berechnung der Closeness zwischen hierarchischen Objekten wird eine Mittelwertbildung eingesetzt. In jedem Schritt wird ein Ablaufplan bestimmt. Nachdem alle
Objekte in einem einzigen Block vereinigt wurden, wird unter allen während des Verfahrens erzeugten Partitionen diejenige mit den besten Eigenschaften (Hardwarefläche,
Latenz) ausgewählt.
APARTY
In Aparty [44] wird das Verfahren von BUD in zwei Punkten verbessert:
• Es werden neue Closenessmetriken zwischen hierarchischen Objekten definiert
(anstatt einer Mittelwertbildung).
• Es werden mehrere Closenessmetriken in einem Multi-stage Clustering Verfahren
verwendet.
5.5.2
Funktionale Partitionierung im Systementwurf
Vulcan Vulcan ist ein an der Stanford University entwickeltes Framework, das aus
zwei Teilsystemen besteht. Im ersten Teil wird die Partitionierung einer Spezifikation
auf verschiedene ASICS beschrieben [26], im zweiten Teilsystem die Partitionierung in
Hardware- und Softwarekomponenten [28]. Das zweite Teilsystem lässt sich stichwortartig beschreiben:
• Eingabe: Die Spezifikation erfolgt in Form eines Programms in der Programmiersprache HardwareC (eine Erweiterung von C um ein Prozesskonzept mit Interprozesskommunikation). Die Spezifikation enthält auch zeitliche Anforderungen
in Form von Minimal- und Maximalzeiten und Datenratenanforderungen. Aus
der Spezifikation wird eine interne Beschreibung, ein Sequenzgraph, generiert.
Das Modell kann auch Operationen mit nicht-deterministischer Berechnungszeit
beinhalten.
132
KAPITEL 5. SYSTEMPARTITIONIERUNG
• Zielimplementierung: Die Zielarchitektur ist ein Ein-Prozessorsystem mit zusätzlichen ASIC-Komponenten. Die Architektur besitzt einen globalen Systembus und
einen globalen Speicher, über den die Kommunikation zwischen Prozessor und
ASICs erfolgt. Der Prozessor ist dabei immer der Busmaster.
• Abstraktionsebene: Als zu partitionierende Objekte werden Anweisungsblöcke ohne Kontrollfluss, sogenannte Grundblöcke, und feinergranulare Objekte betrachtet.
Die Operationen einer Spezifikation werden unterschieden in
– externe Operationen mit nicht-deterministischer Berechnungszeit
– interne Operatonen mit nicht-deterministischer Berechnungszeit
– Operationen mit deterministischer Berechnungszeit
Die internen nicht-deterministischen Operationen werden in Software als sogenannte Programmthreads realisiert. Solche threads sind nebenläufige, in sich sequentielle Programme. Die externen nicht-deterministischen Operationen werden
in Hardware realisiert. Das bedeutet, dass die Hardware sämtliche Synchronisationen mit der Umgebung implementiert. Die Operationen mit deterministischer
Berechnungszeit werden partitioniert.
• Verfahren: Zur Partitionierung wird der bereits vorgestellte Agorithmus HARDWARE_ORIENTED_GREEDY verwendet. Die Zielfunktion besteht aus einer gewichteten Summe von Hardwarekosten, Programm- und Datenspeicheraufwand,
Erfüllbarkeit von Performanceanforderungen sowie Aufwand für die Synchronisation. Das Ergebnis ist eine Bipartition.
Cosyma Das an der Universität Braunschweig entwickelte System Cosyma [17] besitzt
folgende Eigenschaften:
• Eingabe: Die Spezifikation wird in der Sprache C x , einer Erweiterung von
ANSI-C um die Angabe von minimalen und maximalen Berechnungszeiten, einem Taskkonzept und Intertaskkommunikation, durchgeführt. Diese Spezifikation wird intern in einen Syntaxgraphen, der um eine Symboltabelle und Kontrollund Datenflussabhängigkeiten erweitert wird, umgewandelt.
• Zielimplementierung: Die Zielarchitektur ist ein Prozessor mit einem Coprozessor
für die Hardwareaufgaben. Prozessor und Coprozessor sind über einen gemeinsamen Speicher gekoppelt. Die Ausführung von Operationen in Hardware darf sich
zeitlich nicht mit der Ausführung von Softwareprozessen überlappen.
• Abstraktionsebene: Partitioniert wird auf der Ebene von Anweisungsblöcken
(Grundblöcken). Es werden Iterationen zwischen Partitionierung und Synthese
(HW, SW) durchgeführt.
• Verfahren: Das Verfahren ist softwareorientiert und besteht aus zwei geschachtelten Schleifen. In der inneren Schleife wird Simulated Annealing mit einer Zielfunktion eingesetzt, die den geschätzten Gewinn an Ausführungszeit bestimmt,
der bei einer Hardwarerealisierung einer Blockes erzielt würde. Dabei werden die
5.5. ENTWURFSSYSTEME ZUR FUNKTIONALEN PARTITIONIERUNG
133
Kommunikationszeiten berücksichtigt. In der äusseren Schleife werden Syntheseverfahren eingesetzt, um die geschätzten Werte der inneren Schleife zu aktualisieren.
SpecSyn In dem System SpecSyn [22] sind folgende Erweiterungen gegenüber den
anderen Ansätzen realisiert:
• explizite Modellierung von Bussen und Speichern als Hardwarekomponenten
• Konzept von Variablen und Kommunikationskanälen
• Allokation von mehr als einer HW- bzw. SW-Komponente
Dabei werden drei Bindungsprobleme betrachtet: Die Abbildung von funktionalen
Objekten auf Komponenten, von Variablen auf die Speicher und von Kommunikation
auf die Busse. Der Benutzer muss die Reihenfolge, in der diese drei Probleme gelöst werden, auswählen. Als Zielfunktionen werden gewichtete Summen von Überschreitungen
und Beschränkungen bestimmter Metriken betrachtet. Das System vereinigt eine ganze
Reihe verschiedener Partitionierungsalgorithmen, die auf Closenessmetriken beruhen.
134
KAPITEL 5. SYSTEMPARTITIONIERUNG
Kapitel 6
Abschätzung der Entwurfsqualität
6.1
Parameter von Schätzverfahren
Man ist auf Systemebene an guten Schätzverfahren für die Qualität eines Entwurfspunktes aus folgenden Gründen interessiert:
• Eine gute Schätzung ist eine Grundvoraussetzung für die erfolgreiche Exploration des Entwurfsraumes. Im speziellen gilt dies für Partitionierungsverfahren, die
Schätzungen benutzen, um bessere Partitionen zu finden.
• Die Alternative zum Schätzen ist das Rapid Prototyping, d.h., es wird automatisch ein Prototyp des Systems generiert, an dem die Systemparameter gemessen
werden. Rapid Prototyping ist viel zeitaufwendiger als eine Schätzung, so dass
in gleicher Zeit mit Schätzverfahren wesentlich mehr Entwurfsalternativen untersucht werden können.
Bei Schätzverfahren gibt es drei wichtige Parameter, die Exaktheit, die Treue und die
Rechenzeit für die Schätzung.
Definition 6.1 (Exaktheit) Sei E( D ) eine abgeschätzte und M ( D ) die exakte (gemessene) Metrik einer Implementierung D. Die Exaktheit A der Abschätzung ist gegeben durch
A = 1−
| E( D ) − M( D ) |
M( D)
Eine perfekte Abschätzung (Schätzung entspricht dem Messwert) erfüllt damit
A = 1. Im allgemeinen erlauben vereinfachte Modelle schnellere Abschätzungen, haben aber eine geringere Exaktheit. Andererseits ist bei der Schätzung wesentlich, dass
verschiedene Entwurfsalternativen im Vergleich richtig beurteilt werden, und nicht, dass
eine einzelne Entwurfsalternative möglichst exakt geschätzt wird.
Der Parameter Treue [43] gibt die prozentuale Anzahl der korrekt abgeschätzten Vergleiche von Entwurfsalternativen an. Man bezeichnet die Abschätzung beim Vergleich
von zwei Entwurfsalternativen als korrekt, wenn im Falle, dass ein Entwurfspunkt einen
grösseren gemessenen Wert als ein anderer Entwurfspunkt hat, auch der abgeschätzte
Wert für diesen Entwurfspunkt grösser ist als der abgeschätzte Wert für den zweiten
Entwurfspunkt (dies gilt sinngemäss auch für die Fälle gleicher oder kleinerer Werte.
135
KAPITEL 6. ABSCHÄTZUNG DER ENTWURFSQUALITÄT
136
Definition 6.2 (Treue) Sei D = { D1 , D2 , · · · , Dn } eine Menge von Implementierungen einer
funktionalen Spezifikation. Die Treue F einer Abschätzungsmethode ist die Prozentzahl der
korrekten Abschätzungen
F = 100 ×
n
n
2
µij
∑
∑
n ( n − 1 ) i =1 j = i +1
wobei µij mit 1 ≤ i, j, ≤ n, i 6= j gegeben ist durch
µij =

1






if
E( Di ) > E( D j ) ∧ M( Di ) > M( D j )∨
E( Di ) < E( D j ) ∧ M( Di ) < M( D j )∨
E ( Di ) = E ( D j ) ∧ M ( Di ) = M ( D j )
else
0
Beispiel 6.1 Abb. 6.1 zeigt zwei Methoden, die eine bestimmte Metrik schätzen. Das Schätzverfahren
in Abb. 6.1a) besitzt eine Treue von 100% und das Verfahren von Abb. 6.1b) eine Treue von 33%.
geschaetzt
gemessen
Metrik
Metrik
A
B
a)
C
Entwurfspunkte
A
B
C
Entwurfspunkte
b)
Abbildung 6.1: Schätzwerte von Schätzverfahren
Im allgemeinen gilt, dass exaktere Schätzverfahren eine höhere Treue besitzen. Exaktheit und Geschwindigkeit (Ausführungszeit) einer Schätzung bilden meistens einen
trade-off. Je detaillierter ein Systemmodell ist, desto exakter lassen sich die Systemparameter schätzen, aber desto länger dauert die Schätzung. In Tabelle 6.1 sind verschiedene,
für den Hardwareentwurf typische, Abschätzungsmodelle angegeben.
Beispiel 6.2 Wenn nur die Grösse der Speicher als Modell zur Flächenabschätzung benutzt wird, muss
lediglich die Speicherallokation erfolgt sein. Die Abschätzung ist genauer, wenn zusätzlich auch die Allokation der funktionalen Einheiten erfolgt ist. Um auch den Einfluss der Verdrahtung in die Schätzung
einfliessen zu lassen, müssen die Allokation, Bindung und Ablaufplanung erfolgt sein. Ausserdem muss
für die resultierende Architektur ein Floorplan vorliegen.
6.2. QUALITÄTSMASSE
Abschätzungsmodell
Mem
Mem + FU
Mem + FU + Reg
Mem + FU + Reg + Mux
Mem + FU + Reg + Mux + Wiring
137
Voraussetzung
Exaktheit
Treue
Geschwindigkeit
Speicher-Allokation
FU-Allokation
Lebenszeitanalyse
FU-Bindung
Floorplanning
gering
|
|
|
∨
hoch
gering
|
|
|
∨
hoch
schnell
∧
|
|
|
langsam
Tabelle 6.1: Abschätzungsmodelle für die Fläche. (Mem . . . Speicher, FU . . . funktionale
Einheiten, Reg . . . Register, Mux . . . Multiplexer)
6.2
Qualitätsmasse
Die zwei Hauptmasse für SW- und HW-Implementierungen sind die Performance und
die Kosten. Daneben gibt es - je nach Anwendungsgebiet - weitere wichtige Masse:
• Leistungsaufnahme: Die zur Zeit relevanteste Technologie ist CMOS. Bei CMOS wird
Leistung hauptsächlich für das Umladen der Kapazitäten beim Schalten aufgewendet. Die Leistungsaufnahme P ist proportional zur Taktfrequenz f , zur Kapazität C und zum Quadrat der Versorgungsspannung VDD :
2
P ∼ C × f × VDD
Die Leistungsaufnahme spielt eine Rolle bei der Dimensionierung der Versorgung,
für den Störabstand, bei der Auswahl der packagings, und bei der Dimensionierung der Kühlvorrichtungen.
• Energieaufnahme: Das Produkt aus mittlerer Leistungsaufnahme und Ausführungszeit einer Schaltung bzw. eines Tasks bestimmt die Energieaufnahme. Die Energieaufnahme spielt besonders bei mobilen Geräten eine entscheidende Rolle, da sie
die Lebenszeit der Batterien/Akkumulatoren bestimmt. Für Prozessoren wird als
Metrik neben der Energieaufnahme in [ J ] oft auch die auf einen Zyklus bezogene
Leistungsaufnahme [µW/MHz] angegeben.
• Testbarkeit: Das Testen einer Schaltung kann entweder durch Testgeräte (Anlegen
von Testsignalen und Überprüfen der Ausgänge) oder durch einen BIST (builtin self-test) erfolgen. Beim BIST enthält der Chip eine eigene Hardware für den
Selbsttest. BIST-Methoden erhöhen die Steuerbarkeit (controllability) und die Beobachtbarkeit (observability) der internen Signale und führen zu einer Reduktion der
Pinzahl. Andererseits erhöhen BIST-Techniken die Herstellungskosten.
Daneben gibt es eine Reihe von quantitativen und nicht-quantitativen Parametern.
Die Herstellungszeit z.Bsp. hängt stark von der gewählten Implementierungsvariante
ab. Entwurfszeit und time-to-market sind Parameter, die von der gewählten Entwurfsmethodik beeinflusst werden.
KAPITEL 6. ABSCHÄTZUNG DER ENTWURFSQUALITÄT
138
6.2.1
Performancemasse
Die Performancemasse werden in Masse für Hardwareimplementierungen, Softwareimplementierungen und Kommunikation unterteilt.
Performancemasse für Hardware
i1 i2
i3
150
i4 i5 i6
i1 i2
i3
i1 i2
i3
i4 i5 i6
80
80
150
80
i4 i5 i6
150
80
80
80
80
80
150
80
150
80
80
150
80
o1
o2
o1
o1
Taktperiode: 380 ns
Tex : 380 ns
Ressourcen : 2 x, 4 +
(a)
o2
o2
Taktperiode: 150 ns
Tex: 600 ns
Ressourcen : 1 x, 1 +
(b)
Taktperiode: 80 ns
Tex : 400 ns
Ressourcen : 1 x, 1 +
(c)
Abbildung 6.2: Zusammenhang zwischen Taktperiode, Latenz, Ausführungszeit und
Ressourcen
Wird eine Funktion in Hardware realisiert, unterscheidet man die Masse Taktperiode,
Latenz, Ausführungszeit und Datenrate.
• Taktperiode T:
Die Taktperiode hängt mit der verwendeten Technologie, der Ausführungszeit sowie den benötigten Ressourcen zusammen.
• Latenz L:
Die Latenz ist die Anzahl der benötigten Kontrollschritte (Anzahl der Taktschritte).
• Ausführungszeit Tex :
Die Ausführungszeit ergibt sich aus Taktperiode und Latenz durch: Tex = L × T.
• Datenrate R:
Die Datenrate bezeichnet die Anzahl der Durchläufe des Sequenzgraphen pro Sekunde. Werden die neuen Berechnungen erst gestartet, nachdem der Sequenzgraph komplett abgearbeitet ist (nicht-iterativer Ablaufplan), beträgt die Datenrate R = 1/Tex . Die Datenrate wird oft auch mit Durchsatz (throughput) bezeichnet.
6.2. QUALITÄTSMASSE
139
Durch Pipelining lässt sich die Datenrate steigern. Dazu müssen die Operationen
in Stufen eingeteilt werden. Diese Stufen werden durch Register getrennt. Dadurch erhöht sich zwar die Ausführungszeit geringfügig, die Taktperiode kann
aber kleiner gemacht werden (sie muss lediglich grösser als die grösste Verzögerung der einzelnen Stufen sein). Durch Pipelining von Operationen selbst kann
die Anzahl der Stufen weiter erhöht werden bzw. die Verzögerungen der Stufen
möglichst identisch gemacht werden. Hat man P Pipelinestufen mit identischer
Verzögerung, dann ergibt sich:
R=
1
Tex /P
.
Beispiel 6.3 In Abb. 6.2 sind Implementierungen eines Sequenzgraphen mit drei verschiedenen Taktperioden (380ns, 150ns und 80ns) dargestellt. In der Implementierungsvariante a) wird der komplette
Sequenzgraph in einem Taktzyklus abgearbeitet. Diese Variante besitzt die kürzeste Ausführungszeit, benötigt aber die meisten Ressourcen (2 Multiplizierer und 4 Addierer). Die Variante b) implementiert den
Sequenzgraphen in vier Taktzyklen und benötigt die wenigsten Ressourcen (1 Multiplizierer und 1 Addierer), hat allerdings die grösste Ausführungszeit. Die Variante c) verwendet fünf Taktzyklen (durch
Multizyklusoperationen) und ist gemessen in Performance pro Ressource die effizienteste Implementierung.
Beispiel 6.4 Abb. 6.3b) zeigt eine Implementierung eines Sequenzgraphen mit und ohne Pipelining. In
Abb. 6.3 werden Pipelineregister zwischen bestimmten Operationen eingefügt, und das Multiplizierwerk
wird in zwei Stufen implementiert (arithmetisches Pipelining).
i1
i2
i3
i1
i4
i2
1111
0000
0000
1111
0000
1111
i3
i4
1111
0000
0000
0000 1111
1111
0000
1111
T
1111
0000
0000
0000 1111
1111
0000
1111
0000 1111
1111
0000
Multiplizierer
mit Fliessband0000
1111
tiefe 2
0000
1111
(a)
Dauer(+) = 1 Takt
Dauer(x) = 2 Takte
o
o
Tex
(b)
Abbildung 6.3: Einfluss von Modulen mit Pipelining auf die Datenrate. a) Implementierung ohne Pipelining und b) mit Pipelining.
140
KAPITEL 6. ABSCHÄTZUNG DER ENTWURFSQUALITÄT
Performancemasse für Software
Die Laufzeit T eines Programmes auf einem Prozessor lässt sich folgendermassen angeben:
T = Ic × CPI × τ =
Ic × CPI
f
Dabei ist Ic die Anzahl der Instruktionen des Programmes (instruction count), CPI die
durchschnittliche Anzahl der benötigten Taktzyklen pro Instruktion (cycles per instruction) und τ = 1/ f die Taktperiode des Prozessors. Da die verschiedenen Instruktionen
verschiedene Ausführungszeiten haben können, ist der CPI Wert ein Mittelwert über die
Instruktionen des Programmes.
Bei GP-Prozessoren wird oft ein prozessorspezifischer CPI Wert ermittelt, indem man
die Instruktionen von Benchmark-Programmen untersucht. Basierend auf diesem Wert
wird die Performance eines Prozessors auch gerne durch seine MIPS-Rate (million instructions per second) angegeben:
Ic
f
=
T.106
CPI.106
Durch Pipelining und mehrere skalare Einheiten erreichen moderne GP-Prozessoren
CPI Werte von 0.6 bis 0.2, VLIW- und Vektormaschinen können CPI Werte bis 0.1 erreichen [34].
MIPS-Rate =
Beispiel 6.5 Ein Programm mit 6800 Instruktionen wird auf einem Prozessor mit einem CPI-Wert von
0.4, der mit 400MHz getaktet wird, ausgeführt. Die Ausführungszeit des Programmes ergibt sich zu
T=
Ic × CPI
6800 × 0.4
= 68µs
=
f
400 × 106
Die MIPS-Rate ist keine besonders gute Metrik für die Performance eines Prozessors
[64], da sie i) eigentlich spezifisch für ein bestimmtes Programm ist und ii) auch den
Effekt des Compilers berücksichtigt (über die Anzahl der Instruktionen). Die einzige
zuverlässige Metrik ist die Ausführungszeit. Weitere oft verwendete Performancemetriken sind:
• MFLOPS (million floating-point operations per second):
Dieser Wert berücksichtigt die Parallelität von Instruktionen. Bei einem DSP mit
einer MAC-Instruktion z.Bsp. werden zwei floating-point Operationen in einem
Instruktionszyklus durchgeführt. Der MFLOPS Wert ist aber nur ein Spitzenwert,
da er eine optimale Pipelinebelegung vorraussetzt.
• MACS (million multiply & accumulates per second)
Diese Metrik ist für DSPs die wichtigste Kennzahl. Da die meisten DSPs die MACOperation in einem Zyklus durchführen, entspricht der MACS-Wert dem Ausdruck 10−6 /Zykluszeit.
• MOPS (million operations per second)
Diese Metrik umfasst alle Operationen, auch die Operationen der speziellen
Adressrechenwerke und DMA-Controller. Auch hier wird eine optimale Belegung
6.2. QUALITÄTSMASSE
141
der Einheiten angenommen. Diese Annahme ist schon für einen Zyklus sehr unrealistisch und erst recht für eine sinnvoll lange Laufzeit.
Performancemasse für Kommunikation
Die Kommunikation zwischen nebenläufigen Prozessen wird häufig dadurch modelliert, dass ein Prozess einem anderen messages (Botschaften) schickt. Die messages werden über Kanäle gesendet. Jeder Kanal C hat eine maximale Bitrate, die er übertragen
kann. Man definiert weiters die Parameter:
• mittlere Kanalrate avgrate(C ):
avgrate(C ) =
Zahl der gesendeten Bits
Gesamtübertragungsdauer
• Spitzenrate peakrate(C ):
peakrate(C ) =
Anzahl der Bits einer message
Übertragungsdauer der message
Wenn die Übertragung einer message mit n Bits die Zeit t benötigt, dann ergibt sich
die Spitzenrate zu peakrate(C ) = n/t. Für die Implementierung von Kommunikationskanälen gibt es viele Möglichkeiten. Befinden sich die kommunizierenden Prozesse auf
einem Prozessor, wird der Kanal meist durch den Speicher realisiert. Befinden sich die
kommunizierenden Prozesse auf verschiedenen Chips, können Busse oder dedizierte
Links verwendet werden. Diese Kanäle sind charakterisiert durch ihre Geschwindigkeiten und Bitbreiten.
Beispiel 6.6 Abb. 6.4 zeigt den Datentransfer von 8Bit messages über einen Kommunikationskanal C.
Jede message belegt den Kanal für 100ns. In einer Periode von 1000ns werden in diesem Beispiel 56 Bits
gesendet. Damit erhält man eine mittlere Datenrate von avgrate(C ) = 56Bits/1000ns = 56Mb/s. Die
Spitzenrate ist peakrate(C ) = 8Bits/100ns = 80Mb/s. Ein physikalischer Kanal, der diese Kommunikation implementieren kann, muss demnach eine maximale Bitrate von 80Mb/s aufweisen.
8
8
200
8
8
400
8
600
8
8
800
1000
Zeit (ns)
Abbildung 6.4: Belegung eines Übertragungskanals.
Die Dauer eine Kommunikation zwischen zwei Prozessen wird oft durch folgende
Gleichung modelliert:
KAPITEL 6. ABSCHÄTZUNG DER ENTWURFSQUALITÄT
142
Tcomm = To f f set +
m_size
Bitrate
Die Kommunikationszeit setzt sich aus zwei Teilen zusammen, einer Offsetzeit To f f set
und dem Produkt aus Messagegrösse (m_size) in Bits und Bitrate des Kanals. Die Offsetzeit wird benötigt, um die Kommunikation zu initialisieren. Dies kann die Abarbeitung
eines Arbitrierungsprotokolls, der Aufruf von entsprechenden Betriebssystemfunktionen, etc., sein. Bei Kommunikationen mit relativ grosser Offsetzeit ist man bemüht, die
messages möglichst gross zu machen.
6.2.2
Kostenmasse
Hier werden nur Kostenmasse behandelt, die die Herstellung von HW/SW-Systemen
betreffen, und nicht Masse für die Entwicklungskosten. Die Herstellungskosten für
Hardware beinhalten die Kosten für die Maskenfertigung, die Herstellung der Wafer,
Packaging, Testen, etc. Meistens wird als Metrik für die Herstellungskosten eine Metrik
proportional zur benötigten Siliziumfläche verwendet. Dieses Flächenmass kann in mm2 ,
in λ2 (dabei ist λ die feature size der Halbleitertechnologie), in Anzahl der Transistoren,
in Anzahl der Gatter, Anzahl der RTL-Komponenten, oder auch in Anzahl von CLBs
(bei FPGAs) angegeben werden. Diese Metriken haben i.allg. eine hohe Treue. Beim
Packaging werden die Kosten häufig durch die Anzahl der Pins abgeschätzt.
Die Kosten für Software setzen sich aus den Kosten für den Prozessor und die Speicher zusammen. Die Grössen der Programm- und Datenspeicher beeinflussen indirekt
auch die Performance. Wenn Programm- und Datensegmente in den Cache des Prozessors passen, wird die Performance höher sein. Bei eingebetteten Prozessoren kommt
man eventuell mit den internen RAMs/ROMs aus und benötigt keine externe Speicherchips.
6.3
6.3.1
Abschätzung von Hardware
Abschätzung der Taktperiode
In den meisten CAD-Systemen für die Architektursynthese wird die Taktperiode vom
Designer vorgegeben. Hat man keine Vorgabe, muss man die Taktperiode abschätzen.
Dazu dienen die drei folgenden Verfahren: i) die Methode der maximalen Operatorverzögerungszeit, ii) die Methode der Minimierung des Clockschlupfs und iii) die ILPSuche.
Methode der maximalen Operatorverzögerungszeit Die Methode der maximalen
Operatorverzögerungszeit (maximal operator delay, (MOD)) wurde in [61, 35] beschrieben. del (vk ) ist die Verzögerung der funktionalen Einheit, die Operationen vom Typ k
realisiert. Die Taktperiode wird dann mit
T = max(del (vk ))
k
6.3. ABSCHÄTZUNG VON HARDWARE
143
geschätzt. Der Vorteil dieser Methode ist ihre einfache Implementierung und schnelle
Berechnung. Der Nachteil ist, dass bei der so bestimmten Taktperiode mit einer erheblichen Unterauslastung der schnelleren Funktionseinheiten gerechnet werden muss.
Methode der Minimierung des Clockschlupfs
stellt.
Diese Methode wurde in [57] vorge-
Definition 6.3 (Clockschlupf) Der Clockschlupf (clock slack) bezeichnet den proportionalen
Anteil einer Taktperiode, in dem eine funktionale Einheit vk nicht ausgenutzt wird:
slack( T, vk ) = (ddel (vk )/T e) ∗ T − del (vk )
Beispiel 6.7 Gegeben sind drei funktionale Einheiten (FUs), ein Multiplizierer mit einer Verzögerung
von 163ns, ein Subtrahierer mit einer Verzögerung von 56ns und ein Addierer mit einer Verzögerung von
49ns. Die MOD-Methode schätzt die Taktperiode mit 163ns. In Abb. 6.5 sieht man die Auslastung der
drei Einheiten mit dieser Taktperiode.
Operatoren
Taktperiode
Mul
Add
Schlupf
Sub
50
100
150
Belegung FU
Zeit (ns)
163
T(MOD)
Schlupf
Abbildung 6.5: Clockschlupf der funktionalen Einheiten bei der MOD-Methode zur Bestimmung der Taktperiode.
Im allgemeinen gilt, dass ein kleinerer Schlupf einer Funktionseinheit auch zu kleineren
Ausführungszeiten bei gleicher Anzahl von Ressourcen führt.
Definition 6.4 (Mittlerer Clockschlupf) Sei occ(vk ) die Anzahl der Operationen vom Typ
vk , und bezeichne |VT | die Anzahl unterschiedlicher Operationstypen, dann gilt für den mittleren Clockschlupf avgslack( T ) für eine gegebene Takperiode T:
|V |
avgslack( T ) =
∑k=T1 (occ(vk ) × slack ( T, vk ))
|V |
∑k=T1 occ(vk )
Damit kann man die Taktauslastung definieren:
KAPITEL 6. ABSCHÄTZUNG DER ENTWURFSQUALITÄT
144
Definition 6.5 (Taktauslastung) Die Taktauslastung
avgslack( T )
T
bezeichnet die prozentuale mittlere Auslastung aller Funktionseinheiten.
util ( T ) = 1 −
Mit diesen Definitionen kann man ein Optimierungsproblem formulieren, das in
einem Intervall Tmin bis Tmax die Taktperiode mit maximaler Taktauslastung finden soll.
ILP-Suche In [12] wurde ein Ansatz vorgeschlagen, der für diskrete Werte der Taktperiode ein Latenzminierungsproblem als ILP modelliert und Tex minimiert. Im Gegensatz
zur Methode der Clockschlupfminierung, die eine Heuristik ist und nicht immer die minimale Ausführungszeit bestimmt, ist das ILP-basierenden Verfahren exakt.
6.3.2
Abschätzung der Latenz
Die Anzahl der benötigten Kontrollschritte berechnet man mit Hilfe von Schedulingalgorithmen. Zur Abschätzung werden häufig Heuristiken wie Listscheduling verwendet.
6.3.3
Abschätzung der Ausführungszeit
Nach erfolgter Abschätzung der Taktperiode T und der Latenz L erhält man die Ausführungszzeit durch
Tex = L × T
6.3.4
FSMD Modell
Speicher
p1 AR
DR
Kontrolllogik
Controlreg.
Muxer
(CL)
RF
R1
R2
Register
p2
Zust.reg.
Muxer
p3
Next-State
logik
Statusreg.
FUs
Statusbits
Kontrollpfad
Datenpfad
Abbildung 6.6: Steuerwerk- und Datenpfadmodell (FSMD) für die Schätzung
6.3. ABSCHÄTZUNG VON HARDWARE
145
Im folgenden wird als Modell der Hardware ein FSMD (finite state machine and
datapath, Steuerwerk+Datenpfad) (siehe Abb. 6.6) angenommen. Dieses Modell ist charakteristisch für viele ASICs. In diesem Modell gibt es drei kombinatorische Pfade (p1,
p2, p3), die die Taktperiode begrenzen können. Der Pfad p1 führt vom Adressregister
über den Speicher zum Datenregister. Pfade der Gruppe p2 führen von den Registern
über ALUs zurück zu den Registern. Der Pafd p3 führt von den Statusbits, die von den
ALUs erzeugt werden, über das Steuerwerk zurück auf das Rechenwerk (zu ALUs und
Multiplexern). Die Taktperiode muss grösser sein als die grösste Verzögerung in diesen
Pfaden. Üblicherweise ist der Pfad p3 kritisch, d.h., dieser Pfad hat die grösste kombinatorische Verzögerung. Unter bestimmten Voraussetzungen kann diese Verzögerung
durch sogenanntes control pipelining [20, 66] reduziert werden. Dabei werden in den
Pfad p3 Register (Control- und Statusregister) eingefügt und das Steuerwerk in einem
pipeline-Modus betrieben.
Im Falle, dass kein control pipelinig vorliegt, erhält man folgende Bedingung für die
minimale Taktperiode T des Systems:
T ≥ del (SR) + del (CL) + del ( RF ) + del ( Mux ) +
del ( FU ) + del ( NS) + setup(SR) +
∑
(6.1)
del (ni )
1≤ i ≤6
Dabei bedeuten:
• del (SR): Verzögerung beim Lesen des Zustandsregisters (SR)
• del (CL): Verzögerung der Kontrollogik (CL)
• del ( RF ): Verzögerung beim Lesen des Registerfiles (RF)
• del ( Mux ): Verzögerung der Multiplexer
• del ( FU ): Verzögerung der Funktionseinheiten (FU)
• del ( NS): Verzögerung der Zustandsüberführungslogik (Next-State)
• setup(SR): Setupzeit des Zustandsregisters
• del (ni ): Leitungsverzögerungen der Leitungen ni .
6.3.5
Abschätzung der Fläche
Die Fläche eines Entwurfs lässt sich abschätzen, indem man die Anzahl und Typen
der allozierten Komponenten und dann aus gegebenen Technologiedatenbanken die
absoluten Flächenwerte bestimmt. Für das FSMD-Modell müssen der Datenpfad und
der Kontrollpfad abgeschätzt werden.
Datenpfad
Die Fläche des Datenpfades ergibt sich als Summe der Flächen von
• Speicherressourcen (RAM, Register)
KAPITEL 6. ABSCHÄTZUNG DER ENTWURFSQUALITÄT
146
• funktionalen Ressourcen (ALUs)
• Verbindungsressourcen (Multiplexer, Busse)
Eine worst-case Schranke für die Speicherressourcen erhält man z.Bsp. aus dem Sequenzgraphen unter der Annahme, dass man pro Variable genau ein Register alloziert.
Eine genauere Schätzung berücksichtigt die Wiederverwendbarkeit von Registern.
Beispiel 6.8 Abb. 6.7 stellt einen Sequenzgraphen nach der Ablaufplanung dar. Gleichzeitig sind in
Abb. 6.7b) die Lebenszeiten der Variablen eingezeichnet.
0
v1
1
v6
2 v10
v2
v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11
v3 v4
v5
v7
3
v9
v8
v11
4
v1
v2
5
Abbildung 6.7: Sequenzgraph mit Ablaufplan und Lebenszeit der Variablen
Eine Variable lebt von Beginn der sie berechnenden Operation bis zu dem Zeitpunkt,
an dem die letzte ihrer direkten Nachfolgeroperation beendet ist. Die maximale Anzahl
an benötigten Registern ist damit die maximale Anzahl sich überlappender Lebenszeitintervalle (siehe Abb. 6.7b)).
Beispiel 6.9 Für den Sequenzgraphen in Abb. 6.7 sind die Lebenszeiten der Variablen in Abb. 6.7b)
dargestellt. Die minimale Anzahl benötigter Register ist 5, da beispielsweise zu den Zeitschritten 0 bis 3
jeweils 5 Variablen lebendig sind.
Funktionale Ressourcen sind entweder über die Allokation vorgeben oder können
nach der Ablaufplanung prinzipiell mit dem gleichen Verfahren wie das für die Register abgeschätzt werden. Für jeden Kontrollschritt bestimmt man die benötigten Ressourcen und danach sucht man die minimale Anzahl von Ressourcen pro Ressourcentyp
(Multiplizierer, ALU, etc.), mit der man die Kontrollschritte abdecken kann. Ist kein vollständiger Ablaufplan gegeben, sondern beispielsweise nur eine Latenzschranke, dann
kann man z.Bsp. Listscheduling benutzen, um eine Abschätzung der Anzahl der Kontrollschritte und damit der benötigten Ressourcen zu erhalten.
Nachdem alle Operationen an funktionale Einheiten gebunden sind und alle Variablen an Register, kann man die Verbindungsressourcen abschätzen. Dies sind entweder
Multiplexernetzwerke oder Busse.
6.4. ABSCHÄTZUNG VON SOFTWARE
147
Kontrollpfad
Das Steuerwerk besteht im wesentlichen aus
• Zustandsregister
• Kontrollogik (Ansteuerung des Datenpfades)
• Folgezustandslogik
Definition 6.6 (Wortbreite Zustandsregister) Die Wortbreite width(SR) des Zustandsregisters bei L Kontrollschritten kann wie folgt abgeschätzt werden:
width(SR) = dlog2 Le
Die Kontrollogik und Folgezustandslogik kann entweder als ein/mehr-stufige Logik, als ROM oder mit programmierbaren logischen Arrays (PLAs) implementiert werden. Im Falle einer zweistufigen (z.Bsp. AND-OR) Logikrealisierung ist die Zahl der
OR-Gatter gleich der Summe der Ansteuerleitungen zum Datenpfad und der Zustandsleitungen. Die Grösse der OR-Gatter (insb. die Zahl der Eingänge) der Kontrollogik
(Ansteuerung des Datenpfads) entspricht der Anzahl der Zeitschritte, in der die Ausgangsleitung des Gatters angesteuert ist.
Zur Abschätzung der Anzahl der Eingänge der OR-Gatter in der Folgezustandslogik kann man annehmen, dass sich jedes Zustandsbit mit jedem Kontrollschritt ändert.
Die Anzahl der AND-Gatter kann abgeschätzt werden als Summe der Kontrollschritte, an denen irgend welche Ansteuerleitungen oder Folgezustandsleitungen angesteuert
werden (obere Schranke: L). Unter der Annahme, dass maximal eine Statusleitung des
Datenpfads eine Folgezustandsleitung beeinflusst, kann man die Anzahl der Eingänge
der AND-Gatter durch width(SR) + 1 abschätzen. Für eine bestimmte Technologie lässt
sich die Anzahl der Transistoren dieser Gatter und Register bestimmen und daraus auch
eine Schranke für die benötigte Chipfläche. Bei einer ROM- Implementierung muss das
ROM L Worte der Breite W speichern können, wobei W der Summe der Ansteuerleitungen und Folgezustandsleitungen entspricht. Die Fläche des gesamten Steuerwerks
lässt sich in diesem Fall als Summe aus der Fläche des ROMs der Grösse L × W und des
Zustandsregisters abschätzen.
6.4
Abschätzung von Software
Im Bereich von Echtzeitsystemen ist vor allem die obere Schranke der Programmausführungszeit (worst-case execution time, WCET) von Interesse. Bei einem hard real-time System muss der Designer beweisen, dass Zeitbeschränkungen immer eingehalten werden.
Die Bestimmung der WCET durch eine Simulation mit allen möglichen Eingangsdatenmustern und allen internen Systemzuständen ist nicht in sinnvoll kurzer Zeit möglich.
Im folgenden wird eine Methode vorgestellt, die eine geschätzte WCET basierend auf
statischen Programmanalysetechniken bestimmt. Die geschätzte WCET ist immer grösser als die wahre WCET; eine gute Schätzung approximiert die wahre WCET möglichst
nahe. Die Methode setzt eine Mikroprozessorarchitektur mit folgende Eigenschaften
voraus:
KAPITEL 6. ABSCHÄTZUNG DER ENTWURFSQUALITÄT
148
• Einprozessormodell
• Interrupts sind nicht erlaubt (gesperrt).
• Es gibt kein Betriebssystem, der Programmfluss ist ein single thread.
Bei der Schätzung der WCET kann man zwei wichtige Teilprobleme identifizieren:
• Programmpfadanalyse:
Dies ist die Untersuchung, welche Sequenzen von Instruktionen im ungünstigsten Fall ausgeführt werden. Das Ziel ist es, möglichst viele nie beschrittene Pfade
durch eine automatische Datenanalyse oder interaktiv mit Hilfe des Programmierers zu identifizieren und zu eliminieren. Das Problem dabei ist, dass die Anzahl
möglicher Programmpfade exponentiell mit der Programmgrösse wachsen kann
[60].
• Modellierung der Architektur:
Die WCET wird für ein spezifisches Prozessormodell berechnet. Probleme dabei
sind die Abschätzung von Compileroptimierungen, Instruktionspipelining und
die Speicherhierarchie (caches).
6.4.1
Programmpfadanalyse
Die WCET für beliebige (z.Bsp. in C geschriebene) Programme lässt sich nicht bestimmen. Bereits das sogenannte Halteproblem, die Bestimmung, ob ein Programm jemals
anhält, ist unentscheidbar. Um überhaupt eine Aussage über die WCET machen zu können, muss die Menge der Programme geeignet eingeschränkt werden. Kligerman und
Stoyenko haben gezeigt [40], dass das Problem unter folgenden Einschränkungen entscheidbar ist:
• keine rekursiven Funktionsaufrufe
• keine Zeigeroperationen
• beschränkte Schleifen
Die folgende Methode [52] bestimmt die Instruktionsausführungshäufigkeiten im
worst-case und formuliert ein ILP-Modell für die Berechnung der geschätztem WCET.
Dabei nimmt man zunächst an, dass die Ausführungszeit einer Instruktion konstant ist,
d.h., es gibt keine dynamischen Effekte durch Pipelinig und Caching.
Beispiel 6.10 Für das Programm
(x1)
(x2)
(x3)
(x4)
(x5
/* k >= 0 */
s = k;
WHILE (k < 10) {
IF (ok)
j++;
ELSE {
j=0;
6.4. ABSCHÄTZUNG VON SOFTWARE
(x6)
(x7)
149
ok = TRUE;
}
k++;
}
r=j;
ist der (entartete) CFG in Abb. 6.8 dargestellt.
d1
B1 s=k
d2
B2 WHILE(k<10)
d3
B3
d8
IF (ok)
d5
d4
B4
B5 j=0;
ok=TRUE;
j++
d6
d9
B7
B6
d7
k++;
r=j;
d10
Abbildung 6.8: CFG für das Programm in Beispiel 6.10
Definition 6.7 (WCET) Sei xi die Anzahl der Ausführungen eines Grundblocks Bi eines Programms P, das aus N Grundblöcken besteht, dann ist die Ausführungszeit WCET von P
N
WCET =
∑ ci ∗ xi
i =1
wobei ci die Ausführungszeit des Grundblocks Bi darstellt.
Die Werte xi sind abhängig von der Programmstruktur und i.allg. auch von den Werten der Programmvariablen. Für die Erstellung des ILP werden nun die Beschränkungen
analysiert. Grundsätzlich unterscheidet man zwei Arten von Beschränkungen, strukturelle und funktionale. Strukturelle Beschränkungen kommen aus dem CFG, z.Bsp. wird
bei einer Verzweigung entweder der eine oder der andere Pfad beschritten. Funktionale
Beschränkungen werden durch den Benutzer spezifiziert. Dies können z.Bsp. Schleifenschranken oder Pfadinformation (z.Bsp. wie oft ein Pfad durchlaufen wird) sein.
KAPITEL 6. ABSCHÄTZUNG DER ENTWURFSQUALITÄT
150
Im CFG der Abb. 6.8 sei xi die Anzahl der Ausführungen des Grundblocks Bi . Im
ersten Schritt zur Erstellung des ILPs weist man jeder Kante des CFGs eine Variable
di zu, die die Anzahl der Durchläufe durch diese Kante bezeichnet. Die Analyse eines
CFGs wird damit einem Netzwerkflussproblem ähnlich: Für jeden Basisblock muss gelten,
dass die Summe der Gewichte der Eingangskanten gleich der Summe der Gewichte der
Ausgangskanten und diese Zahl gleich xi ist.
Beispiel 6.11 Als strukturelle Beschränkungen für das Programm in Beispiel 6.10 erhält man:
d1
x1
x2
x3
x4
x5
x6
x7
=
=
=
=
=
=
=
=
1
d1 = d2
d2 + d8 = d3 + d9
d3 = d4 + d5
d4 = d6
d5 = d7
d6 + d7 = d8
d9 = d10
Die funktionalen Beschränkungen werden vom Benutzer/Programmierer spezifiziert. Dazu gehören beispielsweise Schranken für Schleifenzähler.
Beispiel 6.12 Im Programm des Beispieles 6.10 wird die Schleife zwischen 0 und 10 mal durchlaufen
(da aus dem Programmkontext bekannt ist, dass k ≥ 0, wenn das Programm aufgerufen wird). Dieses
Wissen kann man durch folgende Beschränkung festhalten:
0x1 ≤ x3 ≤ 10x1
Ein weiteres Beispiel für funktionale Beschränkungen sind Pfadinformationen. Z.Bsp. wird der
Grundblock B5 in einem Programmaufruf maximal einmal durchlaufen:
x5 ≤ 1x1
Es lassen sich auch komplexere Beschränkungen formulieren. Falls z.Bsp. der Programmierer weiss,
dass wenn der ELSE-Zweig ausgeführt wird, die Schleife genau 5 mal durchlaufen wird, kann dies mit
der Beschränkung
( x5 = 0) k ( x5 ≥ 1 & x3 = 5x1 )
formuliert werden.
Mit diesen Beschränkungen erhält man folgendes ILP-Modell:
Definition 6.8 (WCET-ILP) Gegeben sei ein CFG eines Programms P mit N Grundblöcken
Bi . Das ILP
N
WCET = max{ ∑ ci xi | d1 = 1 ∧
i =1
∑
j∈in( Bi )
dj =
∑
d k = xi
i = 1, · · · , N ∧
k ∈out( Bi )
funktionale Beschränkungen }
6.4. ABSCHÄTZUNG VON SOFTWARE
151
bestimmt die WCET von P.
In diesem ILP sind die Variablen die Werte d j , die xi treten nicht selbst als Optimierungsvariablen auf. Die Eigenschaften des obigen ILPs (Netzwerkflussproblems)
erlauben es, das ILP durch seine Relaxation als lineares Programm (LP) zu lösen. Dies
hängt allerdings von den funktionalen Beschränkungen ab.
6.4.2
Modellierung der Mikroarchitektur
Schätzung der Ausführungszeiten
Ein einfaches Schätzmodell, das unabhängig vom Zielprozessor ist, ist in Abb. 6.9 (aus
[21]) dargestellt. Als Grundlage für die Schätzung wird der erzeugte 3-Adress Zwischencode benutzt.
Spezifikation
Kompilation in
Drei-Adress- Code
8086
timing &
codesize
Generische
Instruktionen
"
Schatzung
Technologiedateien
Targetprozessoren
Software
Metrik
68000
timing &
codesize
MIPS
timing &
codesize
Abbildung 6.9: Modell für die Schätzung von Ausführungszeiten
Für jeden 3-Adress Befehl (generische Instruktion) gibt es in einer Technologiedatei (die
spezifisch für einen bestimmten Prozessor ist) eine Sequenz von Instruktionen der Zielmaschine. In diesen Technologiedateien wird die Ausführungszeit für den generischen
3-Adress Befehl sowie die Codegrösse festgehalten. Einen neuen Prozessortyp kann man
aufnehmen, in dem man eine neue Technologiedatei hinzufügt. Die Nachteile dieses
Schätzmodells sind:
• Geringe Genauigkeit durch Schätzung von wenigen Instruktionen, die i.allg. nur
einen kleinen Teil des Instruktionssatzes des Zielprozessors ausmachen. Spezialinstruktionen werden nicht berücksichtigt.
• Vernachlässigung des Einflusses spezifischer Compileroptimierungen für die Zielarchitektur
• Vernachlässigung architektureller Eigenschaften moderner Prozessoren, z.Bsp. Instruktionspipelining und mehrstufige Speicherhierarchien (Caches)
KAPITEL 6. ABSCHÄTZUNG DER ENTWURFSQUALITÄT
152
Modellierung von Cache
Um den Einfluss von Caches in eine statische Programmanalyse aufnehmen zu können,
muss das Programm mit einem Compiler auf die Zielarchitektur kompiliert sein. Die
Instruktionen eines Programmes werden in Cacheblöcken, sog. l-blocks, augeteilt. Ein lBlock ist eine Sequenz von Instruktionen, die in einem Cacheblock gespeichert werden.
Wenn eine Instruktion referenziert (instruction fetch) wird, gibt es einen Cache-Hit,
falls die Instruktion bereits im Cache ist, oder einen Cache-Miss, falls die Instruktion
nicht im Cache ist. Bei einem Cache-Miss muss die Instruktion erst aus dem Hauptspeicher (bzw. 2nd-level Cache) geholt werden. Cache-Miss und Cache-Hit führen zu
unterschiedlichen Ausführungszeiten für eine Instruktion. Das im letzten Abschnitt vorgestellte ILP muss deshalb modifiziert werden.
Beispiel 6.13 Abb. 6.10 zeigt die Instruktionsfolge eines CFGs mit drei Grundblöcken und die Abbildung der Instruktionen auf einen Instruktionscache. Die Instruktionen des Basisblocks B1 sind in drei
l-Blöcke eingeteilt, die auf die drei Cache-Blöcken 0, 1, 2 abgebildet sind, usw.
Instructions
Cache (directly mapped)
B1
Cacheblock
B2
B1,1
B3,1
0
B1,2
B3,2
1
B1,3
B3
B2,1
2
B2,2
3
Abbildung 6.10: CFG und Cachetabelle
Definition 6.9 (Cachekonflikt) Zwei l-Blöcke des CFG sind in Konflikt, wenn die Ausführung eines l-Blocks zu einer Verdrängung des anderen Blocks aus dem Instruktionscache führt.
Damit zwei l-Blöcke in Konflikt sein können, müssen sie notwendigerweise den gleichen Cacheblock belegen.
Beispiel 6.14 B1,1 und B3,1 sind in Konflikt. B2,2 ist nicht in Konflikt mit irgend einem anderen Block.
Blöcke B1,3 und B2,1 spielen eine besondere Rolle: Ein Cache-Miss bei der Ausführung einer der beiden
bedeutet das automatische Laden des anderen l-Blocks in den Cache (weil beide zusammen in einen Cacheblock passen). Folglich gibt es einen Cache-Hit, wenn der andere Block sofort anschliessend ausgeführt
wird.
6.4. ABSCHÄTZUNG VON SOFTWARE
153
Sei die Anzahl der Ausführungen von l-Block Bi,j gleich xi,j und die Anzahl der
hit bzw. x miss , dann gilt:
Cache-Hits bzw. Cache-Misses gleich xi,j
i,j
hit
miss
xi = xi,j = xi,j
+ xi,j
1 ≤ j ≤ ni
hit und cmiss die Kosten von Hit bzw. Miss von l-Block B , dann
Seien im weiteren ci,j
i,j
i,j
erhält man die WCET des Programmes zu:
N ni
WCET =
∑ ∑(ci,jhit xi,jhit + ci,jmiss xi,jmiss )
i
j
Während die bisherigen strukturellen Beschränkungen für das ILP unverändert weiter verwendet werden können, gibt es nun neue Cachebeschränkungen:
Sei l-Block Bk,l der einzige l-Block, der auf den Cacheblock l abgebildet ist. Dann
kann nur die erste Ausführung dieses Blocks zu einem Cache-Miss führen. Alle weiteren
Ausführungen sind automatisch Cache-Hits:
miss
xk,l
≤1
Diese Situation kommt aber nur bei kleinen Programmen vor. Für allgemeine Fälle,
wie z.Bsp. für die beiden l-Blöcke B1,3 und B2,1 , stellt man Gleichungen der Form
miss
miss
x1,3
+ x2,1
≤1
auf. Um in Konflikt stehende l-Blöcke zu analysieren, zeichnet man einen Cachekonfliktgraphen (CCG) [52] für jeden Cacheblock, der zwei oder mehrere in Konflikt stehende l-Blöcke enthält. Durch Analyse dieser Cachekonfliktgraphen erhält man weitere
Beschränkungen zum ILP.
Bei diesem Verfahren werden nur Direct Mapped Caches betrachtet, bei denen die
Abbildung von Instruktionen auf Cacheblöcke direkt aus dem Binärcode der Adresse
bestimmt wird. Bei einem Cache mit 16 Blöcken mit jeweils 32 Bytes werden 4 Bits der
Adresse direkt als Angabe des Cacheblocks benutzt. Existierende Caches sind nicht immer als Direct Mapped Caches angelegt, sondern manchmal als Assoziativspeicher bzw. in
einer Mischform.
Zur Zeit ist noch keine Methode bekannt, die die vorgestellten ILP-Techniken auf
andere Cacheformen erweitert. Auch die dynamischen Effekte der Instruktionspipeline
sind noch nicht ausreichend untersucht. Meist nimmt man vereinfachend an, dass die
Ausführungszeit einer bestimmten Instruktionssequenz in der Pipeline eine Konstante
ist. Diese Zeit wird dann in der Technologiedatei abgelegt. Führt eine Instruktion zu
einem Cache-Miss, wird die Zeit für das Laden eines neuen Cache-Blockes addiert.
6.4.3
Speicherbedarf
Ein einfaches Modell zur Berechnung des Programmspeicherbedarfs geht von einer Darstellung in 3-Adress Zwischencode aus. Sei instr_size( j) der Speicherbedarf der generischen Instruktion j, dann berechnet man den Programmspeicherbedarf eines Grundblocks Bi als
154
KAPITEL 6. ABSCHÄTZUNG DER ENTWURFSQUALITÄT
Basistyp
Bit, Boolean
Bitvector (n Bits)
Character
Integer,Natural,Positive
Real
Datenspeicherbedarf (Bytes)
1
dn/8e
1
4
8
Tabelle 6.2: Datenspeicherbedarfs für einige VHDL-Basistypen.
prog_size( Bi ) =
∑ instr_size( j).
j∈ Bi
Zur Berechnung des Datenspeicherbedarfs muss man die Datendeklarationen der
funktionalen Spezifikation betrachten. Der Speicherbedarf data_size(d) einer Deklaration d wird durch den Basistyp von d und die Anzahl der Elemente in d bestimmt.
Beispiel 6.15 In folgender VHDL-Spezifikation
VARIABLE x: BIT;
VARIABLE y: ARRAY (9 DOWNTO 0, 15 DOWNTO 0) OF INTEGER;
ist x vom Basistyp BIT und hat ein Element. Der Basistyp für y ist Integer und die Anzahl der
Elemente ist 160.
Den Datenspeicherbedarf für die Basistypen hält man in einer Tabelle fest. Abbildung 6.2 [21] zeigt eine solche Tabelle für einige Basistypen von VHDL. Dieser Speicherbedarf gilt für die Ausführung (Simulation) von VHDL.
Beispiel 6.16 In Beispiel 6.15 besteht y aus 160 Elementen. Aus Tabelle 6.2 geht hervor, dass ein Integer
4 Bytes benötigt. Daraus ergibt sich der Datenspeicherbedarf für y zu 160 ∗ 4 = 640 Bytes.
Sei D die Menge aller Deklarationen eines Programms. Eine Gesamtschätzung des
Datenspeicherbedarfs data_size kann man aus folgender Formel erhalten:
data_size =
∑ data_size(d)
d∈ D
Diese Formel ist exakt unter der (unrealisitischen) Annahme, dass die Lebenszeit
aller Deklarationen die gesamte Ausführungsdauer des Programms beträgt und dass
es keine dynamische Speicherallokation gibt. Eine genauere Schätzung kann nur unter
Miteinbeziehung eines (optimierenden) Compilers erfolgen, der eine Lebenszeitanalyse
der Variablen durchführt.
Kapitel 7
Weiterführende Hw/Sw-Codesign
Themen
7.1
Interface- und Kommunikationssynthese
Ein wichtiger Schritt bei der Synthese von Hw/Sw-Systemen ist die Interface-und Kommunikationssynthese. Meistens wird dieser Syntheseschritt nach der Synthese auf Systemebene (Allokation, Bindung, Ablaufplanung) durchgeführt, gemeinsam mit der
Hardware- und Softwaresynthese. Die Ergebnisse der Interface- und Kommunikationssynthese gehen dann iterativ in den nächsten Systemsyntheseschritt ein. In der Literatur wird der Begriff der Interfacesynthese gebraucht, um die Generierung von low-level
interfaces zu bezeichnen. Dazu gehören Schaltungen, die verschiedene Protokolle implementieren, device driver in Software, etc. Kommunikationssynthese bezeichnet die
Generierung von abstrakteren Interfaces, die z.Bsp. die Kommunikation zwischen Prozessen erlauben. Diese Kommunikationsfunktionen bedienen sich dann der low-level
Interfaces für die tatsächliche Kommunikation.
Interface- und Kommunikationssynthese haben besondere Bedeutung bei stark heterogenen Systemen, bei denen verschiedene Prozessoren mit ASICs und Speicherbausteinen zusammengeschaltet werden. Bei solchen Systemen kann die Anzahl der möglichen
Interfaces sehr gross werden, was eine automatisierte Entwurfsraumexploration erfordert. Eine aktuelle Ausprägung solcher heterogener Systeme sind Systems-on-a-Chip,
bei denen verschiedene Blöcke geistigen Eigentums (IP) integriert werden müssen [58].
In diesem Abschnitt wird ein kurzer Überblick über Kommunikationsmodelle gegeben
und verschiedene Teilgebiete der Interfacesynthese angesprochen.
7.1.1
Kommunikationsmodelle
Bei einer Kommunikation werden von einem oder mehreren Senderprozessen Daten
erzeugt und zu einem oder mehreren Empfängerprozessen gesendet. Es gibt eine Reihe
von Modellen, wie diese Kommunikation ablaufen kann. Generell unterscheidet man
zwischen asynchroner und synchroner Kommunikation:
• asynchrone Kommunikation
Bei der asynchronen Kommunikation sind der Sender und der Empfänger nicht
155
156
KAPITEL 7. WEITERFÜHRENDE HW/SW-CODESIGN THEMEN
koordiniert, d.h., Sender und Empfänger müssen nicht zum gleichen Zeitpunkt an
der Kommunikation (Datenübermittlung) teilnehmen. Das erfordert einen Bufferspeicher zwischen den kommunizierenden Prozessen. Im allgemeinsten Fall sind
sowohl die Schreib- als auch die Leseoperationen nicht blockierend. Der Empfänger hat dann keine Garantie, dass die Daten im Buffer aktuell bzw. neu sind.
Es gibt eine Vielzahl von erweiterten asynchronen Modellen, die die übertragenen
Daten mit Nummern oder Zeitstempeln versehen oder blockierende Schreiboperationen (falls der Buffer voll ist) und blockierende Leseoperationen (falls der Buffer
leer ist) implementieren.
• synchrone Kommunikation, Rendezvous
Bei der synchronen Kommunikation, im Gebiet der Parallelprogrammiersprachen
auch Rendezvous [32] genannt, müssen Sender und Empfänger zur gleichen Zeit
an der Kommunikation teilnehmen, damit die Daten übermittelt werden können.
Dieser Mechanismus ist inhärent blockierend, d.h., Sender bzw. Empfänger müssen solange warten, bis der andere Kommunikationspartner bereit ist. Für diese
Kommunikationsart ist kein Zwischenbuffer notwendig. Beispiele von Programmiersprachen, die das Rendezvousmodell benutzen, sind OCCAM und ADA.
In vielen Spezifikationsmodellen werden FIFOs (first-in, first-out), oft auch queues
genannt, als Kommunikationstyp verwendet. FIFOs stellen eine asynchrone Punkt-zuPunkt Verbindung zwischen zwei Prozessen her, bei der die vom Sender erzeugten Daten in einen first-in, first-out Buffer geschrieben werden. Der Empfänger kann diese
Daten aus dem Buffer lesen, wobei die Daten entfernt werden.
• FIFO mit unbegrenzter Kapazität
Bei dieser FIFO-Art besitzt der Buffer eine unendlich grosse Anzahl von Speicherplätzen. Der Sender kann nicht blockiert werden, da immer Speicherplätze
verfügbar sind. Wenn der Empfänger von einem leeren FIFO lesen will, gibt es
unterschiedliche Varianten. Entweder wird der Empfänger blockiert bis das FIFO
ein Datum enthält, oder er kann die Anzahl der Daten im FIFO durch Aufruf einer
Funktion feststellen, bevor er den blockierenden Lesezugriff durchführt.
• FIFO mit begrenzter Kapazität
In diesem Modell kann zusätzlich der Fall eintreten, dass der Sender ein Datum
auf ein volles FIFO schreiben will. Entweder muss der Sender blockiert werden,
oder es wird das letzte Datum überschrieben.
Eine weitere oft benutzte Kommunikationsform ist der Datenaustausch über gemeinsame Speicherberiche, koordiniert durch read-modify-write-Mechanismen. Read-modifywrite auf Befehlssatzebene bedeuet, dass es eine ununterbrechbare Instruktion gibt, die
eine Speicherstelle liest, modifiziert und wieder schreibt. Diese Instruktion (bei vielen
Prozessoren auch als test-and-set bezeichnet) kann nicht durch Interrupts unterbrochen
werden. Read-modify-write Zugriffe werden üblicherweise verwendet, um Semaphoren
zu implementieren. Mit Semaphoren kann der Zugriff auf gemeinsam genutzte Ressourcen bzw. Datenstrukturen koordiniert werden. Prozessoren, die den Aufbau von sharedmemory Multiprozessoren unterstützen, besitzen oft zusätzliche LOCK-Leitungen, mit
7.1. INTERFACE- UND KOMMUNIKATIONSSYNTHESE
157
Transmitters
Receivers
Buffer size
Blocking
reads
Blocking
writes
many
one
many
one
finite
zero (one)
no
yes
no
yes
Unbounded FIFO
Bounded FIFO
one
one
one
one
unbounded
bounded
yes
yes/no
no
yes/no
Read-modify-write
many
many
finite
yes/no
yes/no
Asynchronous
Synchronous
Tabelle 7.1: Parameter einiger Kommunikationsmodelle
denen der Zugriff von mehreren Prozessoren auf einen gemeinsamen Speicher koordiniert werden kann. Bestimmte Befehle setzen das LOCK-Signal, andere setzen es wieder
zurück. Die LOCK-Signale der Prozessoren werden in einer Speicherarbitrierungslogik
zusammengeführt.
Auch auf höherer Abstraktionsebene werden read-modify-write Modelle verwendet, um gegenseitigen Ausschluss zu gewährleisten. Bestimmte Ressourcen können von
mehreren Prozessen nur exklusiv genutzt werden (mutual exclusion). Ein Beispiel dafür
ist das Monitorkonzept von Modula-II. Ein Monitor ist ein abstrakter Datentyp (Datenstrukur mit Zugriffsfunktionen), in den nur ein Prozess zu einem gegebenen Zeitpunkt
eintreten kann. Andere Prozesse können nur eintreten, wenn der Monitor freigegeben
wurde.
Tabelle 7.1 zeigt die wichtigsten Eigenschaften dieser Kommunikationsmodelle. Die
FIFO-Modelle und das Rendezvous sind Verfahren, die genau einen Sender und einen
Empfänger benötigen. Bei den anderen Verfahren gibt es Varianten für mehrere Sender
und Empfänger. Die Einträge yes/no bedeuten, dass beide Modellvarianten vorgeschlagen worden sind.
7.1.2
Interfacesynthese
Hardwareinterfaces Bei Hardwareinterfaces unterscheidet man ebenfalls zwischen
asynchronen und synchronen Interfaces. Bei synchronen Interfaces benutzen Sender
und Empfänger dasselbe Taktsignal, bei asynchronen Interfaces gibt es keinen gemeinsamen Takt. In diesem Fall werden handshake-Protokolle verwendet. Asynchrone Interfaces
sind typisch für Board-Systeme. Sie scheinen sich aber auch für SoCs durchzusetzen, da
bei asynchronen Interfaces weniger Timingbedingungen an die verschiedenen IP-Blöcke
gestellt werden müssen.
Die am häufigsten verwendete Variante zur Spezifikation von Interfaces sind TimingDiagramme. Diese Diagramme werden sowohl für synchrone als auch für asynchrone
Protokolle verwendet, sind aber informal und deswegen nicht als Spezifikationsmodelle
für einen automatisierten Synthesevorgang geeignet. Für die Spezifikation von asynchronen Protokollen wurden formale Formalismen entwickelt, ein Beispiel sind Signal
Transition Graphs (STG) [13]. Synchrone Interfaces sind einfacher zu synthetisieren, da
Sender plus Empfänger als eine Schaltung mit einem gemeinsamen Clocksignal modelliert werden können. Weiterführende Literatur zur Synthese asynchroner Interfaces
158
KAPITEL 7. WEITERFÜHRENDE HW/SW-CODESIGN THEMEN
findet sich z.Bsp. in [47] [70], zur Synthese synchroner Interfaces in [29] [30] [62].
Softwareinterfaces Je nach System können verschiedene Softwareschichten an der
Durchführung einer Kommunikation beteiligt sein (siehe Abb. 7.1). Die low-level Interfaces stellen Routinen dar, die Daten auf die Schnittstellen eines Prozessors schreiben
bzw. Daten von den Schnittstellen lesen. Diese Routinen sind oft als Interruptserviceroutinen (ISRs) ausgeführt. Wird ein Betriebssytem bzw. ein Echtzeitbetriebssystem (RTOS,
real-time operating system) verwendet, gibt es als weitere Interfacestrukturen die device driver. Man unterscheidet zwischen zeitgesteuerten und ereignisgesteuerten Treibern
bzw. Systemen. Bei einem zeitgesteuerten System wird der Treiber in bestimmten Intervallen von Prozessen aufgerufen und versucht dann, eine Kommunikationsaufgabe
durchzuführen. Zum Beispiel kann in regelmässigen Abständen versucht werden, von
einem A/D-Wandler Daten zu lesen (polling). Die Architektur von zeitgesteuerten Treibern kann relativ einfach sein. Ereignisgesteuerte Systeme sind in der Lage, auf externe
Ereignisse zu reagieren. Tritt ein externes Ereignis auf, wird ein Interrupt generiert, was
in Folge zu einer Aktivierung einer ISR und danach eines device drivers führt.
Bei der Entwicklung von ISRs und device drivers ist vor allem das Zeitverhalten dieser Softwareblöcke von Interesse. Um Timingbedingungen einhalten zu können, muss
man die worst-case execution time (WCET) bestimmen.
user processes
software
device drivers (RTOS)
low-level routines (ISR)
00000000000000000000000000000
11111111111111111111111111111
11111111111111111111111111111
00000000000000000000000000000
00000000000000000000000000000
11111111111111111111111111111
00000000000000000000000000000
11111111111111111111111111111
00000000000000000000000000000
11111111111111111111111111111
00000000000000000000000000000
11111111111111111111111111111
00000000000000000000000000000
11111111111111111111111111111
00000000000000000000000000000
11111111111111111111111111111
00000000000000000000000000000
11111111111111111111111111111
hardware
Abbildung 7.1: Schichten bei SW-Interfaces
Hardware/Software Interfaces Diese Interfaces verbinden Software- und Hardwareblöcke [16] [15]. Aus der Sicht des Prozessors unterscheidet man zwei Interfacevarianten. Entweder besitzt der Prozessor eigene I/O Schnittstellen oder die Hardwareblöcke
werden memory mapped angebunden. Bei I/O Schnittstellen gibt es viele Varianten; manche Prozessoren besitzen einen eigenen I/O Adressraum und geben durch ein externes
Signal an, ob ein Datentransfer den Speicher- oder den I/O-Adressraum betrifft. I/O
Operationen werden durch spezielle Instruktionen durchgeführt. Bei einem memorymapped Interface werden die Hardwareblöcke wie Speicherbausteine angeschlossen
und bekommen einen Adressbereich zugeordnet. Man muss sicherstellen, dass beim
Datentransfer die Timinganforderungen des Speicherbusses eingehalten werden.
Die Schwierigkeiten bei der Verbindung von Hardware und Software liegen oft darin, bestimmte durch das Protokoll oder die Hardware vorgegebene Timingbedingungen
7.2. EMULATION UND RAPID PROTOTYPING
159
Design level
Description language
Primitives
Algorithm
Architecture
Register transfer
Logic
HLL
HLL, HDL
HDL
HDL, netlist
Instruction sets
Functional blocks
RTL primitives
Logic gates
Simulation time
(instructions/cycle)
10-100
1K-10K
1M-10M
10M-100M
Tabelle 7.2: Simulationsebenen beim Entwurf digitaler Systeme
auf dem Prozessor einzuhalten. Es kann dann notwedig werden, zum Prozessor noch
einen Interfaceblock (in Hardware) zu entwickeln, der gegenüber der Hardware das
Protokoll und die Timingbedingungen einhält und mit den Softwareinterfaces kommuniziert.
7.2
7.2.1
Emulation und Rapid Prototyping
Simulation vs. Emulation digitaler Schaltungen
Zwei gegenwärtig starke Trends im VLSI Design sind die steigende Komplexität der
Chips und die Forderung nach kürzeren Entwurfszeiten. Eine Auswirkung dieser
Trends ist ein wachsender Validierungsengpass (validation bottleneck). Bei der Validierung wird die korrekte Funktionalität und die Einhaltung der Entwurfsbeschränkungen
(Performance, Kosten, Leistungsaufnahme, etc.) eines Designs überprüft. Validierung
kann entweder durch formale Verifikation oder durch ausgiebige Simulation durchgeführt werden. Obwohl in bestimmten Bereichen (z.Bsp. Controller, die durch endliche
Automaten modelliert werden) in den letzten Jahren grosse Fortschritte in der formalen
Verifikation erzielt wurden und diese auch industriell eingesetzt wird, liegt der Schwerpunkt der Validierung bei komplexen ASICs oder Prozessoren nach wie vor bei der
Simulation. Validierung durch Simulation ist ein sehr zeitintensiver Prozess, und die
benötigte Simulationszeit steigt erfahrungsgemäss quadratisch mit der Chipkomplexität an [10]. Nachdem die time-to-market ganz erheblich über den Erfolg eines Produktes
entscheidet, ist es wichtig, Engpässe im Chipentwurf zu beseitigen. Im Falle der Validierung versucht man, den Simulationsaufwand durch Emulation und Prototyping zu
reduzieren.
Simulation wird auf mehreren Ebenen eines Designs eingesetzt und wird generell
zeitintensiver, je detaillierter der Entwurf wird. Tabelle 7.2 zeigt verschiedene Simulationsebenen beim Entwurf digitaler Systeme [31]. Viele digitale Systeme sind programmierbar, d.h. fallen in die Kategorie der Prozessoren. Solche Systeme werden auf der
höchsten Ebene durch ihren Instruktionssatz beschrieben. Der Instruktionssatz hat grosse Auswirkungen auf die Performance und die Flexibilität des Systems. Deshalb werden Instruktionssatzsimulatoren eingesetzt, um auf dieser hohen Ebene Implementierungsalternativen, d.h. verschiedenen Instruktionssätze, zu untersuchen. Diese Instruktionssatzmodelle werden meist in einer höheren Programmiersprache (HLL, high-level
language) entworfen und sind relativ effizient simulierbar. Als Richtwert wird 10-100
KAPITEL 7. WEITERFÜHRENDE HW/SW-CODESIGN THEMEN
160
Instruktionen/Zyklus angegeben. Dies bedeutet, dass der Prozessor, auf dem die Simulation ausgeführt wird, 10-100 Instruktionen benötigt, um einen Instruktionszyklus des
simulierten Prozessors auszuführen.
Auf der Architekturebene wird das Zielsystem genauer modelliert, indem die Interaktionen zwischen den grossen funktionalen Blöcken des Systems, wie CPU, Cachecontroller, Memorycontroller, I/O Schnittstellen, etc., beschrieben werden. Die Modellierung erfolgt entweder in einer HLL oder bereits in einer HDL (hardware description
language). Die Simulationsgeschwindigkeit hängt stark vom Detaillierungsgrad der Modelle ab; ein Richtwert ist 1K-10K Instruktionen für die Simulation eines Instruktionszyklus der Zielarchitektur.
Auf der Registertransferebene wird die Zielarchitektur taktzyklengetreu modelliert.
Die Simulation ist allerdings relativ langsam mit 1M-10M Instruktionen pro Instruktionszyklus. RTL Simulatoren sind derzeit die am häufigst verwendeten Simulatoren, um
Korrektheit und Performance eines Designs nachzuweisen.
Auf der Logikebene besteht das Modell aus einer Netzliste von Gattern. Die Simulationsgeschwindigkeit auf dieser Ebene ist allerdings viel zu gering, um sinnvoll lange
Simulationen durchzuführen. Diese Ebene ist der Einsatzbereich von FPGA-basierten
Emulationssystemen. Durch solche Emulationssysteme kann die Geschwindigkeit gegenüber der Simulation um einige Grössenordnungen erhöht werden und kommt in den
Bereich – und zum Teil darüber – der Simulationsgeschwindigkeit einer Architektursimulation. Dies erlaubt, statt der Architekturmodelle Modelle auf Logikebene zu verwenden und in gleicher Laufzeit wesentlich genauere Ergebnisse zu erzielen bzw. wesentlich
mehr Instruktionszyklen zu simulieren.
7.2.2
Aufbau von Emulationssystemen
Ein Emulationssystem für die Logiksimulation besteht aus zwei Teilen, der programmierbaren Hardware und der Systemsoftware. Die Aufgabe eines Logikemulators ist
es, ein Zieldesign auf Gatterebene auf die programmierbare Hardware (FPGAs) abzubilden. Nachdem interessierende Zieldesigns eine weit grössere Anzahl von Gattern
haben, als gegenwärtige FPGAs abbilden können, bestehen Logikemulatoren aus mehreren FPGAs, die in einer bestimmten Topologie verbunden sind und nach aussen wie
ein riesiger monolithischer FPGA-Baustein wirken. Die Aufgabe der Systemsoftware ist
es, das Zieldesign auf diese FPGAs aufzuteilen.
Architektur und Systemsoftware
Die Parameter bzw. Unterscheidungsmerkmale von Emulationssystemen sind:
• Typ der verwendeten FPGAs: Gatteranzahl, Anzahl von I/O Pins, mapping efficiency
• Systemarchitektur: Anzahl der FPGAs, Verbindungstruktur
• Systemsoftware
7.2. EMULATION UND RAPID PROTOTYPING
161
Ein FPGA hat eine bestimmte Anzahl von konfigurierbaren Logikblöcken (CLBs).
Die Struktur und der Aufbau dieser CLBs bestimmen die sog. mapping efficieny (Abbildungseffizienz) [8]. Dieser Parameter macht eine Aussage darüber, wie effizient ein
Zieldesign auf die CLBs eines FPGAs abgebildet werden kann. Diese Abbildungseffizienz variiert mit den Zieldesigns. Schaltungen, die grosse Teile von random logic beinhalten, werden i.a. auf CLBs mit feiner Granularität effizienter abbildbar sein als auf CLBs
mit grober Granularität. Für stark strukturierte Designs, z.Bsp. ALUs, Datenpfade, verhält es sich umgekehrt. Die Gesamtzahl der CLBs eines FPGAs bestimmt, wie gross die
von diesem FPGA emulierten Teile des Zieldesigns maximal sein können. Man gibt diesen Parameter meist in Anzahl von emulierten Gattern an. Ein weiterer Parameter von
FPGAs ist die Anzahl ihrer I/O Pins.
Für das gesamte Emulationssystem sind die wesentlichen Parameter die Anzahl der
FPGAs und die Verbindungsstruktur zwischen den FPGAs. Die Verbindungsstruktur
hat auch Einfluss darauf, wie einfach und wie effizient das Zieldesign abgebildet werden kann. Verbindungsstrukturen, bei denen ein FPGA nur mit wenigen FPGAs in der
lokalen Nachbarschaft verbunden ist, bezeichnet man als Verbindungen niedriger Dimensionalität [68]. Ein Beispiel dafür ist ein 2D-mesh, das nur Verbindungen zu den
vier direkten Nachbar-FPGAs (Norden, Süden, Westen, Osten) erlaubt. Solche niederdimensionalen Verbindungsstrukturen haben den Nachteil, dass sehr oft Signale von einem FPGA zu einem anderen FPGA durch Zwischen-FPGAs geroutet werden müssen.
Der Vorteil dieser einfachen Verbindungsstrukturen liegt in dem einfachen Aufbau und
den damit verbundenen geringen Kosten. Andererseits gibt es hoch-dimensionale Verbindungsstrukuren, wie z.Bsp. cross-bars, die eine direkte Verbindung beliebiger FPGAs
erlauben. Der Nachteil dieser Verbindungsstrukturen ist ihr enormer Verdrahtungsaufwand, was sich in hohen Kosten niederschlägt.
162
KAPITEL 7. WEITERFÜHRENDE HW/SW-CODESIGN THEMEN
design netlist
technology
mapping
design analysis
system level
partition, place
and route
FPGA compilation
(vendor specific)
FPGA configuration bits
Abbildung 7.2: Aufbau der Systemsoftware eines Emulationssystems
Der Aufbau der Systemsoftware von Emulationssystemen ist in Abb. 7.2 dargestellt. Im ersten Schritt wird die zu emulierende Netzliste eingelesen und die Gatter
auf die Elemente der Zieltechnologie (CLBs) abgebildet. Der nachfolgende Designanalyseschritt führt FPGA-spezifische Optimierungen und Timing-Analysen durch. Dann
folgt der zentrale strukturelle Partitionierungsschritt. Das gesamte Design muss in Einheiten aufgeteilt werden, die jeweils auf einen einzelnen FPGA passen, und diese Einheiten müssen bestimmten FPGAs des Systems zugewiesen werden. Eine wichtige Nebenbedingung dabei ist, dass die Leitungslängen von Signalen, die über FPGA-Grenzen
hinaus gehen, möglichst gering gehalten werden. Im letzten Schritt werden die FPGAHerstellerwerkzeuge verwendet, um die Designteile für die einzelnen FPGAs in Konfigurationsdaten für die FPGA-Bausteine zu übersetzen. Als zusätzliche Funktionalität
fügen Emulationssysteme üblicherweise noch Blöcke für Logikanalyse- und Stimuligeneratoren zum Design hinzu. Ein Stimulusgenerator erlaubt dem Benutzer, Testvektoren
an bestimmte Schaltungsteile anzulegen. Logikanalyse stellt dem Benutzer ein Interface
zur Verfügung, um die Signalverläufe bestimmter Signale zu beobachten und aufzuzeichnen.
Problemstellungen bei Emulationssystemen
Das Ziel bei Emulationssystemen ist es, die Emulationsgeschwindigkeit zu maximieren
und gleichzeitig die Kosten möglichst gering zu halten. Die Emulationsgeschwindigkeit
wird durch die längste Signalverzögerung im System bestimmt. Die Verzögerungen bestehen aus zwei Komponenten: der Signalverzögerung innerhalb der FPGAs und der
externen Signalverzögerung bei der Verbindung von FPGAs. Meistens ist die externe
Verzögerung grösser. Diese externe Signalverzögerung hängt stark von der Topologie
7.2. EMULATION UND RAPID PROTOTYPING
163
des Systems ab, d.h. der Anzahl von FPGAs, durch die das Signal geroutet werden
muss. Es gibt zwei Ansatzpunkte, um die externe Signalverzögerung zu reduzieren: i)
die Auslastung der einzelnen FPGAs zu maximieren, so dass möglichst wenige FPGAs
verwendet werden müssen und ii) die Leitungslängen zwischen den FPGAs zu verringern.
I/O pins
800
Cache Controller
700
600
500
400
XC4000
300
250
200
100
5000
100
1000
10000
100000
FPGA gates
Abbildung 7.3: Relation zwischen pin count und gate count [6]
Die Erfahrung mit Emulationssystemen hat gezeigt, dass die Auslastung der FPGAs
durch die Anzahl der I/O Pins beschränkt ist. Das heisst, um mehr Gatter eines FPGAs
nutzen zu können, müsste man mehr I/O Pins zur Verfügung haben. Dieser Umstand
der Pinbeschränkung wird durch Rent’s Rule ausgedrückt. Diese Regel schätzt den Kommunikationsaufwand in I/O Pins P für eine Partition mit G Gattern durch
P ∝ Gf
ab, wobei der Exponent f für strukturierte Chip Designs bei ca. 0.5 liegt. Dies entspricht
dem Verhältnis zwischen Umfang und Fläche, und ist somit in 2-dimensionaler Chiptechnologie realisierbar. Für random logic liegt f etwas höher als 0.5, was für gegenwärtige (FPGA-)Technologie zu einer Beschränkung wird. Abb. 7.3 zeigt diesen Zusammenhang zwischen den benötigten I/O Pins einer Partitionierung und der Anzahl der
Gatter. Die obere Kurve entspricht den Anforderungen eines Cache Controller Chips, die
untere Kurve gibt die Ressourcen der FPGA-Familie Xilinx XC4000 an. Die in Abb. 7.3
sichtbare Lücke hat zur Folge, dass für random logic die FPGAs eines Emulationssystems meistens nicht voll ausgelastet werden können.
Die Verzögerungen durch die Verbindungsstruktur können minimiert werden, indem man die Verbindungen so kurz wie möglich macht. Im Extremfall gibt es kein
routing, d.h., jeder I/O Pin ist mit jedem I/O Pin eines anderen FPGAs direkt verbindbar. Dies erfordert aber hochdimensionale Verbindungsstrukturen, die die Kosten des
Emulationssystems stark erhöhen. Grosse Signalverzögerungen führen zu einem weiteren Problem, das von der Systemsoftware gelöst werden muss: die mögliche hold-time
Verletzung von Flip-Flops (FFs). Dies ist z.Bsp. der Fall, wenn die Logik, die den Eingang
KAPITEL 7. WEITERFÜHRENDE HW/SW-CODESIGN THEMEN
164
eines FFs bestimmt, relativ schnell (kurze Verzögerungen), das Clock-Signal aber durch
routing über mehrere FPGAs sehr langsam ist. Ein weiteres Problem ist, dass die verschiedenen Clockeingänge der FFs unterschiedliche Phasenlagen haben können. Dieser
clock skew tritt vor allem bei gated clocks auf. Die Systemsoftware muss solche Situationen
erkennen und gegebenenfalls die Logik durch zusätzliche Elemente verzögern.
7.2.3
Beispiele für Emulationssysteme
In diesem Abschnitt werden die kommerziellen Emulationssysteme Enterprise Emulation System von Quickturn Systems und Virtual Wires Technologie von Virtual Machines
Work vorgestellt.
Enterprise System
Das Enterprise System [71] benutzt als Verbindungsnetzwerk einen partiellen Crossbar.
Das ist ein Kompromiss zwischen kurzen Leitungslängen und Kosten. Die I/O Pins
der FPGAs sind in Gruppen geteilt. Alle Leitungen verschiedener FPGAs einer Gruppe
werden in einem Crossbar Chip zusammengeführt (siehe Abb. 7.4). Das bedeutet, dass
Leitungen innerhalb einer Gruppe über maximal einen Crossbar Chip gehen und somit
eine relativ geringe und vor allem abschätzbare Verzögerung besitzen.
A
B
C
FPGA 3
FPGA 2
FPGA 1
D
A
B
C
A pins
B pins
crossbar chip
crossbar chip
D
A
B
C
FPGA 4
D
C pins
crossbar chip
A
B
C
D
D pins
crossbar chip
Abbildung 7.4: Partieller Crossbar des Enterprise Systems
Das Problem des clock skews löst die Enterprise Systemsoftware nicht durch die
Verzögerung zu schneller Logikteile, sondern durch die Reduktion der Clockverzögerungen durch Logiktransformationen.
Das Enterprise System ist skalierbar. Die unterste Ebene stellt einen partiellen
Crossbar wie in Abb. 7.4 dar, der auf einem Board implementiert ist. Mehrere solcher
Boards füllen ein Gehäuse, das die Grundeinheit des Systems ist und Designs bis zu
einer Grösse von 330K Gattern (Stand 1995) emulieren kann [10]. Für grössere Designs
können mehrere dieser Einheiten zusammengeschaltet werden.
7.2. EMULATION UND RAPID PROTOTYPING
165
Virtual Wires
Der Virtual Wires Ansatz [6] benutzt eine 2D-mesh Topologie und eine spezielle Technik,
virtual wires, um der I/O Pinbeschränkung von FPGAs zu begegnen. Bei diesem Ansatz
werden mehrere logische Leitungen auf einer kleineren Anzahl vorhandener physikalischer Leitungen gemultiplext. Statt eines Systemclocks werden alle Register mit dem
sogenannen virtual clock getaktet, der üblicherweise höher ist, als es der reguläre Systemclock wäre. Dieser Ansatz diskretisiert den Raum in eine Anzahl von spatial units,
die den Partitionen entsprechen, und eine Anzahl von timing units, die einer virtuellen
Clockperiode entsprechen. Der virtuelle Clock wird so gross gewählt, dass die Kommunikation zwischen zwei benachbarten FPGAs in einem Zyklus stattfinden kann. Mit
diesem Timingmodell sind beliebig lange Leitungen implementierbar, da die Verzögerung zwischen zwei FPGAs durch den virtuellen Clock berücksichtigt wird und längere
Pfade eine Aneinanderreihung von virtuellen Clockzyklen sind.
Der Partitionierungsschritt wird stark vereinfacht, da man sich nur mehr um die
Grösse der Partitionen und nicht mehr um die Anzahl der benötigten I/O Pins kümmern muss (es gibt eine unendlich grosse Anzahl virtueller I/O Pins). Die Systemsoftware fügt zu jeder Partition zusätzliche Logik (FFs, Multiplexer, Buffer) und einen
Controller (FSM) hinzu, die die Abläufe in der Partition zu den verschiedenen virtuellen Clockzyklen steuert. Dazu muss die Systemsoftware einen Ablaufplanungsschritt
durchführen. Obwohl also zusätzliche Systemlogik zum Zieldesign hinzukommt, hat
sich gezeigt, dass die Auslastung der einzelnen FPGAs durch diese virtual wires Technik höher als bei anderen Ansätzen ist. Dies beruht darauf, dass das Problem der I/O
Pinbeschränkungen reduziert wird.
Abb. 7.5 zeigt ein Beispiel für eine Schaltung, die auf vier FPGAs aufgeteilt werden
soll, Abb. 7.6 die partitionierten Schaltungen mit den zusätzlichen Logikteilen. Jedes
Register wird von dem virtual clock VC getaktet, die Multiplexer, Buffer und EnableEingänge der Register werden von einem Controller (virtual FSMs) gesteuert. Der Systemclock ist im System nicht mehr explizit sichtbar, er wird durch die FSMs emuliert.
Die FSMs durchlaufen zyklisch eine bestimmte Anzahl von Zuständen und steuern dabei die emulierten Gatter. Ein Zustandszyklus entspricht einem Zyklus des Systemclocks.
FPGA1
FPGA2
D
E
Q
FPGA4
FPGA1
FPGA2
FPGA4
FPGA3
FPGA3
D
E
Q
user clock
Abbildung 7.5: Zieldesign mit vier Partitionen
Zwischen FPGA3 und FPGA4 in Abb. 7.6 zum Beispiel müssen drei verschiedene
KAPITEL 7. WEITERFÜHRENDE HW/SW-CODESIGN THEMEN
166
FPGA1
FPGA2
D
E
D
Q
Q
E
D
1
Q
E
VC
VC
Q
VC
D
Q
D
E
E
VC
Virtual Wires FSM1
FPGA4
Virtual Wires FSM2
VC
FPGA3
Virtual Wires FSM4
D
E
Virtual Wires FSM3
2
Q
Q
D
E
VC
D
5
Q
E
Q
VC
D
E
D
Q
E
VC
6
VC
4
Q
D
E
D
3
Q
E
VC
VC
VC
VC ... virtual clock
Abbildung 7.6: Mit virtual wires partitioniertes Zieldesign
Signale in folgender Reihenfolge übertragen werden: i) der Ausgang Q von FF3 zum
Eingang D von FF6 im virtuellen Zyklus 1, ii) der Ausgang Q von FF 2, dessen Eingang
im virtuellen Zyklus 1 mit dem Ausgang von FF1 verbunden war, mit dem Eingang von
FF5 und iii) der vom FPGA4 berechnete Wert zum Eingang des FF4.
7.2.4
Einsatz von Emulationssystemen
Heute werden Emulationstechniken bei Prozessoren und komplexeren ASICs eingesetzt,
um time-to-market und Entwicklungskosten zu senken. Dabei bringt Emulation drei wesentliche Vorteile: Erstens hilft die Emulation, Fehler zu finden, bevor der Chip gefertigt
wird. Dadurch verbessern sich die Chancen, bereits mit dem ersten Fabrikationsgang
korrekte Chips zu bekommen, bzw. es werden weniger Fabrikationsdurchläufe bis zum
korrekten Chip benötigt (das Ziel ist immer: to get the first silicon right). Zweitens kann
durch Emulation die Systemsoftware für das Zielsystem (Betriebssystem, Anwendungen) getestet und korrigiert werden, bevor der Chip gefertigt ist. Das geht zwar auch
mittels Simulation, aber durch den Geschwindigkeitsvorteil der Emulation gegenüber
der Simulation können komplexere Anwendungen getestet werden. Bei der Entwicklung von GP-Prozessoren umfasst dies typischerweise das Booten eines multi-user Betriebssystems und graphische Anwendungen (z.Bsp. Unix mit X-Windows). Drittens
hilft die Emulation, Fehler im bereits gefertigten Chip zu lokalisieren. Der reale Chip
läuft mit der viel höheren Zielgeschwindigkeit, und Fehler treten dadurch schneller zu
Tage. Oft ist es ein Problem, den Fehler zu lokalisieren, da die internen Signale des Chips
nur sehr schwer zugänglich sind. Mit einem Emulationssystem kann man die Systemzu-
7.2. EMULATION UND RAPID PROTOTYPING
Quickturn systems
Emulation gates
Emulation frequency
Critical bugs found
Design development
Full config. time
MicroSPARC II
1
0.25 M
750 kHz
1
3 weeks
24 hours
167
SuperSPARC II
2
0.55 M
350 kHz
0
3 weeks
24 hours
UltraSPARC I
5
1M
350 kHz
1
3 weeks
36 hours
Tabelle 7.3: Erfahrungsdaten bei der Emulation von SPARC Prozessoren [23]
stände, bei denen der Fehler auftritt, nachstellen und durch die bessere Beobachtbarkeit
der Signale den Fehler leichter eingrenzen. Die Nachteile der Emulation sind die hohen Kosten für die Emulationssysteme und die Kosten und der Zeitaufwand für die
Vorbereitung eines Designs für die Emulation. Das Zieldesign liegt meist noch nicht als
Netzliste vor und muss erst synthetisiert werden, oder die vorliegende Netzliste muss
erst in die Zieltechnologie des Emulationssystems übersetzt werden.
Tabelle 7.3 zeigt Ergebnisse von der Emulation dreier SPARC Prozessoren. Der Zeitaufwand, um das Design für eine Emulation vorzubereiten, ist mit 3 Wochen relativ
hoch. Der automatische Übersetzungsprozess der Designnetzliste für das Emulationssystem ist auch zeitaufwendig. Für den UltarSPARC-I Prozessor z.Bsp. benötigte diese
Übersetzung 36 Stunden und belegte 75 Workstations [24]. Bei der angegebenen Emulationsgeschwindigkeit dauerte das Booten eines multi-user Betriebssystems in etwa zwei
Stunden. Die Kosten für ein Quickturn System betrugen zur Zeit dieser Emulationen
(1995) etwa US $2 pro Gatter, was die in Tabelle 7.3 angeführten Systeme in die Preiskategorie $0.5M . . . $2M bringt.
7.2.5
Rapid Prototyping Systeme
Die in den vorangehenden Abschnitten vorgestellten Emulationssysteme stellen für
digitale Schaltungen (Prozessoren, ASICs) Prototypen dar, an denen Untersuchungen
durchgeführt werden können. In den letzten Jahren sind eine Reihe von Rapid Prototyping Systemen entwickelt worden, die eine Emulation von heterogenen Zielsystemen
ermöglichen. Das sind Zielsysteme, die aus Prozessoren, ASICs, eventuell FPGAs, und
Speicherbausteinen bestehen. Oft werden die Prozessoren für ein Zielsystem nicht eigens enwickelt, sondern es werden verfügbare Standardprozessoren verwendet. In diesem Fall ist es sinnvoller, im Prototypen bereits den Zielprozessor oder einen vergleichbaren Prozessortyp einzusetzen.
Diese Rapid Prototyping Systeme erlauben die flexible Integration heterogener Komponenten dadurch, dass sie i) eine Reihe von Modulen (Prozessoren, Speicher, FPGAs)
verschiedener Hersteller und ii) eine programmierbare Verbindungsstruktur anbieten.
Für diese Verbindungsstruktur werden entweder FPGAs oder auch spezielle programmierbare Verbindungsbausteine, sog. FPICs (field-programmable interconnects) verwendet.
Ein Beispiel für eine Rapid Prototyping Umgebung sind die Entwicklungsboards von
APTIX [3], die auf FPICs basieren.
168
KAPITEL 7. WEITERFÜHRENDE HW/SW-CODESIGN THEMEN
Literaturverzeichnis
[1] A.V. Aho, M. Ganapathi, and S.W.K Tjiang. Code generation using tree matching
and dynamic programming. ACM Trans. Prog. Lang. and Systems, 11(4):491–516,
1989.
[2] Virtual Socket Interface Alliance.
www.vsi.org, October 1998.
VSI System Level Design Model Taxonomy.
[3] APTIX. http://www.aptix.com.
[4] Guido Araujo, Srinivas Devadas, Kurt Keutzer, Stan Liao, Sharad Malik, Ashok Sudarsanam, Steve Tjiang, and Albert Wang. Code Generation for Embedded Processors,
chapter Challenges in Code Generation for Embedded Processors, pages 48–64.
Kluwer, 1995.
[5] Guido Araujo and Sharad Malik. Optimal Code Generation for Embedded Memory Non-Homogeneous Register Architectures. In Eight International Symposium on
System Synthesis, 1995.
[6] J. Babb, R. Tessier, and A. Agarwal. Virtual Wires: Overcoming Pin Limitations
in FPGA-based Logic Emulators. In Workshop on FPGA-based Custom Computing
Machines, 1993.
[7] Garrick Blalock. Microprocessors Outperform DSPs 2:1. Microprocessor Report,
10(17), December 1996.
[8] S.D. Brown, R. J. Francis, J. Rose, and Z. G. Vranesic. Field-Programmable Gate Arrays.
Kluwer, 1992.
[9] Stephen Brown and Jonathan Rose. FPGA and CPLD Architectures: A Tutorial.
IEEE Design & Test of Computers, Summer 1996.
[10] M. Butts. Tutorial: FPGAs in Logic Emulation. In ICCAD, 1993.
[11] R. Camposano and R. K. Brayton. Partitioning before logic synthesis. In Proc.
ICCAD, 1987.
[12] S. Chaudhuri, S. A. Blythe, and R. A. Walker. An exact methododology for scheduling in a 3d design space. In Proc. of the 8th International Conference on System
Synthesis, pages 78–83, 1986.
169
170
LITERATURVERZEICHNIS
[13] T. A. Chu. On the models for designing VLSI asynchronous digital systems. Integration: the VLSI journal, 1986.
[14] Thomas M. Conte, Pradeep K. Dubey, Matthew D. Jennings, Ruby B. Lee, Alex
Peleg, Salliah Rathnam, Mike Schlansker, Peter Song, and Andrew Wolfe. Challenges to Combining General-Purpose and Multimedia Processors. IEEE Computer,
December 1997.
[15] M. Eisenring and J. Teich. Domain-specific interface generation from dataflow specifications. In Proceedings of Sixth International Workshop on Hardware/Software Codesign, CODES 98, pages 43–47, Seattle, Washington, March 15-18 1998.
[16] M. Eisenring and J. Teich. Interfacing hardware and software. In 8th International Workshop on Field-Programmable Logic and Applications, FPL’98, Lect ure Notes in
Computer Science, 1482, pages 520 – 524, Tallinn, Estonia, August 31 - September 3
1998.
[17] R. Ernst, J. Henkel, and T. Benner. Hardware-software cosynthesis for microcontrollers. IEEE Design & Test of Computers, pages 64–75, December 1994.
[18] C. M. Fiduccia and R. M. Mattheyses. A linear-time heuristic for improving network
partitions. In Proc. of the Design Automation Conference, 1982.
[19] M. Freericks. The nML machine description formalism. Technical Report 1991/15,
Comp. Science Dept., TU Berlin, 1991.
[20] D. Gajski, N. Dutt, A. Wu, and S. Lin. High Level Synthesis: Introduction to Chip and
System Design. Kluwer, Norwell, Massachusetts, 1992.
[21] D. Gajski, F. Vahid, S. Naranyan, and J. Gong. Specification and Design of Embedded
Systems. Prentice Hall, Englewood Cliffs, NJ, 1994.
[22] D. D. Gajski, F. Vahid, and S. Narayan. A system-level design methodology:
Executable-specification refinement. In Proc. of the European Conference on Design
Automation (EDAC), 1994.
[23] J. Gateley and M. Blatt. Reducing Time-to-Emulation through Flow Automation.
Nikkei Electronics, 1995.
[24] J. et al. Gateley. Ultarsparc-i emulation. In 32nd IEEE/ACM Design Automation
Conference, 1995.
[25] Linda Geppert. High-flying DSP architectures. IEEE Spectrum, November 1998.
[26] R. Gupta and G. De Micheli. Partitioning of functional models of synchronous
digital systems. In Proc. of the International Conference on Computer-Aided Design
(ICCAD), pages 216–219, 1990.
[27] R. Gupta and G. De Micheli. System-level synthesis using re-programmable components. In Proc. of the European Conference on Design Automation (EDAC), pages 2–7,
1992.
LITERATURVERZEICHNIS
171
[28] R. Gupta and G. De Micheli. Hardware-software cosynthesis for digital systems.
IEEE Design & Test of Computers, pages 29–41, October 1993.
[29] N. Halbwachs. Synchronous Programming of Reactive Systems. Kluwer, 1993.
[30] D. Harel. StateCharts: A visual formalism for complex systems. Science of Programming, (8), 1987.
[31] Rachid Helaihel and Kunle Olukotun. Emulation and Prototyping of Digital Systems. In Hardware/Software Codesign, Nato ASI Series, 1996.
[32] C. A. R. Hoare. Communicating Sequential Processes. Prentice-Hall, 1995.
[33] Merrill Hunt and James A. Rowson. Blocking in a system on a chip. IEEE Spectrum,
pages 35–41, November 1996.
[34] Kai Hwang. Advanced Computer Architecture. McGraw-Hill, 1993.
[35] R. Jain, M. Mlinar, and A. Parker. Area-time model for synthesis of non-pipelined
designs. In Proceedings of the International Conference on Computer-Aided Design, 1988.
[36] S. C. Johnson. Hierarchical clustering schemes. Psychometrika, pages 241–254, September 1967.
[37] B. W. Kernighan and S. Lin. An efficient heuristic procedure for partitioning graphs.
Bell System Technical Journal, February 1970.
[38] S. Kirkpatrick, C. D. Gelatt, and M. P. Vecchi. Optimization by simulated annealing.
Science, 220(4598):671–680, 1983.
[39] Y. C. Kirkpatrick and C. K. Cheng. Ratio cut partitioning for hierarchical designs.
IEEE Transactions on CAD, 10(7):911–921, July 1991.
[40] E. Kligerman and A. D. Stoyenko. Real-time euclid: A language for reliable realtime systems. IEEE Transactions on Software Engineering, 12(9):941–949, 1986.
[41] W. Kozuch and A. Wolfe. Compression of Embedded Systems Programs. In International Conference on Computer Design: VLSI in Computers and Processors, 1994.
[42] B. Krishnamurthy. An improved min-cut algorithm for partitioning VLSI networks.
IEEE Transactions on Computers, May 1984.
[43] F. J. Kurdahi, D. D. Gajski, C. Ramachandran, and V. Chaiyakul. Linking registertransfer in physical levels of design. IEICE Transactions on Information and Systems,
E76-D(9), September 1993.
[44] E. D. Lagnese and D. E. Thomas. Architectural partitioning for system level synthesis of integrated circuits. IEEE Trans. on CAD, 10(7):847–860, July 1991.
[45] Dirk Lanneer, Johan Van Praet, Augusli Kifli, Koen Schoofs, Werner Geurts, Filip
Thoen, and Gert Goossens. Code Generation for Embedded Processors, chapter CHESS:
Retargetable Code Generation for Embedded DSP Processors, pages 85–102. Kluwer, 1995.
172
LITERATURVERZEICHNIS
[46] Phile Lapsley, Jeff Bier, Amit Shoham, and Edward A. Lee. DSP Processor Fundamentals. IEEE Press, 1997.
[47] L. Lavagno and A. Sangiovanni-Vincentelli. Algorithms for synthesis and testing of
asynchronous circuits. Kluwer, 1993.
[48] T. Lengauer. Combinatorial Algorithms for Integrated Circuit Layout. John Wiley, New
York, 1990.
[49] Rainer Leupers. Retargetable Code Generation for Digital Signal Processors. Kluwer,
1997.
[50] S. Liao, S. Devadas, K. Keutzer, S. Tjiang, and A. Wang. Storage Assignment to
Decrease Code Size. In ACM SIGPLAN Conference on Programming Language Design
and Implementation, 1995.
[51] S. Y. Liao, S. Devadas, and K. Keutzer. Code Density Optimization for Embedded
DSP Processors Using Data Compression Techniques. In Chapel Hill Conference on
Advanced Research in VLSI, 1995.
[52] S. Malik, W. Wolf, A. Wolfe, Y.-T. Li, and T.-Y. Yen. Performance analysis of embedded processors. In Lecture notes NATO Workshop on Hardware/Software Codesign.
NATO Advanced Study Institute, Tremezzo, Italy, 1995.
[53] M. C. McFarland. Using bottom-up design techniques in the synthesis of hardware
from abstract behavioral descriptions. In Proc. 23rd Design Automation Conference,
pages 474–480, June 1986.
[54] M. C. McFarland and T. J. Kowalski. Incorporating bottom-up design into hardware
synthesis. IEEE Trans. on CAD, September 1990.
[55] Mentor Graphics Corporation. DSP Architect DFL User’s and Reference Manual, 1993.
[56] Giovanni De Micheli. Synthesis and Optimization of Digital Circuits. McGraw-Hill,
Inc., 1994.
[57] S. Narayan and D. D. Gajski. System clock estimation based on clock slack minimization. In Proceedings of the European Design Automation Conference (EuroDAC),
1992.
[58] Ross B. Ortega, Luciano Lavagno, and Gaetano Boriello. Models and Methods
for HW/SW Intellectual Property Interfacing. In Nato Advanced Study Institute on
System-level Synthesis, 1998.
[59] C. H. Papadimitriou and K. Steiglitz. Combinatorial Optimization (Algorithms and
Complexity). Prentice-Hall, 1982.
[60] C. Y. Park. Predicting Deterministic Execution Times of Real-Time Programs. PhD thesis, University of Washington, Technical Report 92-08-02, Department of Computer
Science and Engineering, Seattle 98195, August 1992.
LITERATURVERZEICHNIS
173
[61] A. C. Parker, J. Pizarro, and M. Mlinar. Maha: A program for datapath synthesis. In
Proc. IEEE 2rth Design Automation Conference, pages 461–466, New York, NY, 1986.
[62] R. Passerone, J. Rowson, and A. Sangiovanni-Vincentelli. Automatic synthesis of
interfaces between incompatible protocols. In Proceedings of the DAC, 1998.
[63] David A. Patterson and John L. Hennessy. Computer Architecture: A Quantitative
Approach. Morgan Kaufmann, 1996.
[64] David A. Patterson and John L. Hennessy. Computer Organization & Design. Morgan
Kaufmann, 1998.
[65] Pierre G. Paulin, Clifford Liem, Trevor C. May, and Shailesh Sutarwala. Code Generation for Embedded Processors, chapter FlexWare: A Flexible Firmware Development
Environment for Embedded Systems, pages 67–84. Kluwer, 1995.
[66] L. Ramachandran and D. D. Gajski. Architectural tradeoffs in synthesis of pipelined
controls. In Proceedings of the European Design Automation Conference (EuroDAC),
1993.
[67] F. Romeo and A. Sangiovanni-Vincentelli. Probabilistic hill-climbing algorithms. In
Proc. of the 1985 Chapel Hill Conference on VLSI, pages 393–417, 1985.
[68] Hauck S., Boriello G., and C. Ebeling. Mesh Routing Topologies for Multi-FPGA
Systems. In ICCD, 1994.
[69] J.A. Storer and T.G. Szymanski. Data Compression via Textual Substitution. Journal
of the ACM, 29(4):928–951, 1982.
[70] Jane S. Sun and Robert W. Brodersen. Design of system interface modules. ICCAD,
1992.
[71] J Varghese, M. Butts, and J. Batcheller. An Efficient Logic Emulation System. IEEE
Transactions on VLSI, 1(2), 1993.
[72] A. Wolfe and A. Chanin. Executing Compressed Programs on an Embedded RISC
Architecture. In Micro-25, 1992.