Hardware/Software Codesign - ETH TIK

Werbung
Hardware/Software
Codesign
Skriptum zur Vorlesung
WS 2006/2007
Marco Platzner, Lothar Thiele
Institut für Technische Informatik und Kommunikationsnetze
ETH Zürich
Vorwort
Dieses Skriptum 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 Skriptum soll als Referenz für manche
der in der Vorlesung behandelten Themenbereiche dienen.
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 Skriptum wird deshalb auf eine konsistente Übersetzung ins Deutsche verzichtet.
Das vorliegende Skriptum basiert auf den Vorlesungsunterlagen von Herrn Dr.Ing. Jürgen Teich und Dr. Marco Platzner, die die Vorlesung Hardware/Software Codesign
an der ETH Zürich mehrere Jahre lang gelesen haben.
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 . . . . . . .
1
1
2
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
5
5
5
9
15
18
24
28
31
31
34
3 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) . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
37
37
37
37
38
39
39
41
48
49
49
50
51
4 Systempartitionierung
4.1 Modelle für die Systemsynthese . . . . . . . . . . . . .
4.2 Partitionierung . . . . . . . . . . . . . . . . . . . . . .
4.3 Allgemeine Partitionierungsalgorithmen . . . . . . . .
4.3.1 Konstruktive Partitionierungsverfahren . . . .
4.3.2 Iterative Partitionierungsverfahren . . . . . . .
4.3.3 Partitionierung mit Evolutionären Algorithmen
4.3.4 Partitionierung mit linearer Programmierung .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
57
57
62
63
64
66
67
68
iii
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
iv
INHALTSVERZEICHNIS
4.4
4.5
Algorithmen zur HW/SW-Partitionierung . . . . . . . .
Entwurfssysteme zur funktionalen Partitionierung . . . .
4.5.1 Funktionale Partitionierung im Hardwareentwurf
4.5.2 Funktionale Partitionierung im Systementwurf .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
68
70
70
72
5 Compiler und Codegenerierung
5.1 Compiler – Aufbau . . . . . . . . . . . . . . . . . . . . . . .
5.1.1 Aufgaben eines Compilers . . . . . . . . . . . . . . .
5.1.2 Phasen eines Compilers . . . . . . . . . . . . . . . .
5.1.3 Zwischencode . . . . . . . . . . . . . . . . . . . . . .
5.1.4 Grundblöcke und Kontrollflussgraphen . . . . . . . .
5.2 Codegenerierung . . . . . . . . . . . . . . . . . . . . . . . .
5.2.1 Modellmaschine . . . . . . . . . . . . . . . . . . . . .
5.2.2 Einfacher Codegenerator . . . . . . . . . . . . . . . .
5.2.3 Registerbindung . . . . . . . . . . . . . . . . . . . .
5.2.4 Codegenerierung für DAGs . . . . . . . . . . . . . .
5.2.5 Codegenerierung mit Dynamische Programmierung .
5.3 Codeoptimierung . . . . . . . . . . . . . . . . . . . . . . . .
5.3.1 Peephole Optimierung . . . . . . . . . . . . . . . . .
5.3.2 Lokale Optimierung . . . . . . . . . . . . . . . . . .
5.3.3 Globale Optimierung . . . . . . . . . . . . . . . . . .
5.4 Codegenerierung für Spezialprozessoren . . . . . . . . . . .
5.4.1 Nicht-homogene Registersätze, irreguläre Datenpfade
5.4.2 Zuweisung von Speicheradressen und Adressregistern
5.4.3 Codekompression . . . . . . . . . . . . . . . . . . . .
5.5 Retargetable Compiler . . . . . . . . . . . . . . . . . . . . .
5.5.1 Codegenerierung durch Baumübersetzung . . . . . .
5.5.2 Prozessormodellierung . . . . . . . . . . . . . . . . .
5.5.3 Fallbeispiele . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
75
75
75
76
79
83
85
88
90
92
96
97
101
102
103
105
106
108
112
122
123
125
126
128
6 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
6.4.3 Speicherbedarf . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
131
131
133
133
137
138
138
139
140
140
141
143
144
146
149
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
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.
Historisch gesehen ist HW/SW Codesign ein Forschungsgebiet, dessen Entstehung,
Motivationen und Zielsetzungen durch folgende Entwicklungen geprägt sind:
1
2
KAPITEL 1. EINLEITUNG
• 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 CachingStrategien. 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.
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).
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 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 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
4
KAPITEL 1. EINLEITUNG
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 Co-Simulation und CoEmulation. 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.
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 Klas5
6
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
digital domain
sensors
interface
digital
target
system
analog domain
interface
actors
communication
ports
Abbildung 2.1: Grundstruktur eines typischen Hardware/Software-Systems
Beispiel
Laserdrucker
Autosteuerung
Mobiltelefon
Sensoren
Hitzesensoren
Füllstandsmesser
Drehzahlmesser
Positionsgeber
Druckmesser
Tastenfeld
Akkuladestandsm.
Mikrophon
Aktoren
Motoren
Heizelemente
Anzeigen
Zündung
Einspritzung
Lautsprecher
Display
Vibrationsgeber
Interfaces
A/D-Wandler
D/A-Wandler
Pulsformer
A/D-Wandler
Ereigniszähler
D/A-Wandler
A/D-Wandler
D/A-Wandler
Kommunikationsports
Ethernet-Transceiver
parallele Schnittstelle
serielle Schnittstelle
CAN-Bus Transceiver
serielle Schnittstelle
Tabelle 2.1: Beispiele für eingebettete Systeme und deren Komponenten
se 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 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.
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
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 cell-based
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 Untergruppen,
standard-cell Design und macro-cell Design. Beim standard-cell Design sind die LibraryZellen 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
8
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
design styles
semicustom
custom
array-based
cell-based
standard cells
macro cells
MPGAs
FPGAs
Abbildung 2.3: Entwurfsstile für integrierte Schaltungen
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 Flip-Flops. 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 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
2.2. IMPLEMENTIERUNGSARTEN
parameter
density
performance
design time
manufacturing time
cost at low volume
cost at high volume
custom
very high
very high
very long
medium
very high
low
9
cell-based
high
high
short
medium
high
low
MPGA
high
high
short
short
high
low
FPGA
medium-low
medium-low
very short
very short
low
high
Tabelle 2.2: Vergleich der Entwurfsstile
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, CADWerkzeugen, ü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 implemen-
10
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
tiert 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 GPProzessoren in eingebetteten Systemen sind die relativ komplexen Speicher- und I/OInterfaces 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 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.
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 200466 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/StoreEinheit (LSU) und eine Systemregistereinheit (SRU)), von denen z.B. maximal 4 in der instruction
fetch Phase sein können.
2.2. IMPLEMENTIERUNGSARTEN
processor
type
21164 ALPHA
(Digital)
21264 ALPHA
(Digital)
R10000
(MIPS)
PowerPC750
(IBM/Motorola)
PA-8500
(HP)
UltraSparc III
(SUN)
Pentium III
(Intel)
K6-III
(AMD)
11
64 bit RISC
skalar units ×
pipeline depth
4 × 10
clock
[MHz]
600
1st level cache
instr./ data
8 KB / 8 KB
2nd level cache
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
96 KB intern
Tabelle 2.3: Moderne GP-Prozessoren (N.A. = not available)
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
• viel Datenparallelität
• rechenzeitintensive Algorithmen
• 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
+
+ x ÷
Reorder Buffer
(6 Entry)
Completion Unit
Integer Unit 2
Integer Unit 1
I
32-Bit
Reservation Station
Reservation Station
2 Instructions
Additional Features
• Time Base Counter/Decrementer
• Clock Multiplier
• JTAG/COP Interface
• Thermal/Power Management
• Performance Monitor
MPC750 RISC Microprocessor Technical Summary
DTLB
SRs
(Original)
DBAT
Array
Data MMU
32-Bit
CR
System Register
Unit
Reservation Station
PA
Tags
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)
GPR File
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
Abbildung 2.4: Aufbau eines PowerPC750
[14]. Diese Erweiterungen basieren auf dem sub-word execution model, d.h., die Datenpfa-
2.2. IMPLEMENTIERUNGSARTEN
13
Abbildung 2.5: Layout des PowerPC750
de 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:
• 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 können
oder eine overflow-Semantik , die den Multimedia/DSP-Algorithmen angepasst ist.
14
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
Packed byte (8 x 8 bit)
quant7
quant6
quant5
quant4
quant3
quant2
quant1
quant0
Packed half-word (4 x 16 bit)
quant3
quant2
quant1
quant0
Packed word (2 x 32 bit)
quant1
quant0
Long word (64 bit, native data type)
quant0
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
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. IMPLEMENTIERUNGSARTEN
Subword instruction: UNPACK R3
R1
a3
R3
15
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
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.
• 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
16
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
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
Abbildung 2.9: Layout des Microcontrollers SIECO51 (Siemens Automotive). Der core
dieses Controllers ist ein 8051
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:
2.2. IMPLEMENTIERUNGSARTEN
17
• 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
• 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: userund 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
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/O-Operationen 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/OOperationen und die Codegenerierung. Die TPU z.B. wird durch das Schreiben mehrerer Register
18
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
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
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
2.2. IMPLEMENTIERUNGSARTEN
19
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 floatingpoint ALU wesentlich grösser und damit teurer. Für viele 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 fixed-point 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 DSP-Programme 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
20
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
der letzten Jahrzehnts behandelt: Multi-DSP Systeme, DSPs mit VLIW-Architektur 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).
Abbildung 2.11: Signalprozessor ADSP21060 (SHARC)
Das Operationswerk (links) besitzt 3 parallele Einheiten: eine ALU, eine Schiebeeinheit (MultiBit) und eine Multipliziereinheit. Die entsprechenden Befehle des Instruktionssatzes sind EinZyklen-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
2.2. IMPLEMENTIERUNGSARTEN
21
zwei Bänken, was typisch für DSP-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/OProzessor 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 32bit 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.
Abbildung 2.12: Aufbau des TMS320C80 (MVP) von Texas Instruments
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)
22
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
Abbildung 2.13: Datenpfad eines DSPs im TMS320C80
• 2 Adress-ALUs für Indexberechnungen
• 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 Instruktionswort 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
2.2. IMPLEMENTIERUNGSARTEN
23
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 generalpurpose Register, d.h., jedes Register kann für jede Operation verwendet werden. Der TMS320C6x
hat keines der typischen DSP-Merkmale, 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.
Abbildung 2.14: Architektur des TMS320C6x core
Abbildung 2.15: Instruktionswort des TMS320C6x
Desktop-DSP Wie schon im Abschnitt 2.2.1 erwähnt, werden Anwendungen der Signalverarbeitung zunehmend auf GP-Prozessoren gerechnet. Beim Desktop-Computing
24
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
(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
• 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 GPProzessoren. 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 GP-Prozessoren mehr und mehr DesktopDSP Anwendungen erobern und auch mehr DSP-Architektureigenschaften bekommen werden. Andererseits entwickeln sich DSPs auch weiter und übernehmen zunehmend Eigenschaften von RISC bzw. VLIW-Architekturen.
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 dem
Chip realisiert. Zur Speicherorganisation gehört auch die Registerstruktur, die entweder heterogen oder homogen sein kann. Bei homogenen Registersätzen kann jedes
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)
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
26
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
Abbildung 2.17: Vergleich der Preis/Leistungsverhältnisses von DSPs und GP-Prozessoren.
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)
Einheiten und Registern sowie eine gekoppelte Multiplizier-Addiereinheit. Der ASIP besitzt eine
Load/Store-Architektur mit Festkomma-Arithmetik und einem heterogenen Registersatz (Register
A1, A2, AR, R1, R2, MR). Als periphere Komponenten sind A/D-Wandler, D/A-Wandler, Timer,
serielle/parallele Schnittstellen und DMA-Controller vorgesehen.
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:
2.2. IMPLEMENTIERUNGSARTEN
D/A- A/D
27
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
• 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
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.
28
2.2.5
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
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).
Abbildung 2.19: Struktur eines FPGAs
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.
FPGAs werden durch das Setzen von Schaltern programmiert. Es gibt unterschiedliche Schalter-Technologien, die in Tabelle 2.4 aufgeführt sind. Die meisten FPGA-Familien
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 der Spannungsversor-
2.2. IMPLEMENTIERUNGSARTEN
29
Abbildung 2.20: Aufbau eines Logikblocks (CLB) der Xilinx XC4000 Serie
Abbildung 2.21: Aufbau eines I/O-Blocks der Xilinx XC4000 Serie
gung 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 damit dem Hardware-Entwurf. Mit herkömmlichen Entwurfswerkzeugen (Synthesewerkzeuge, Schematic Entry) werden Netzlisten von generischen Elementen (Gates, Flip-Flops)
erzeugt. Diese Netzlisten werden von den Tools der FPGA-Hersteller in Konfigurationsda-
30
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
Abbildung 2.22: Verbindungsstruktur der Xilinx XC4000 Serie
switch type
antifuse
EPROM
EEPROM
SRAM
reprogrammable
no
yes (out of circuit)
yes (in circuit)
yes (in circuit)
volatile
no
no
no
yes
Tabelle 2.4: Switch-Technologien für field-programmable devices
ten 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
2.3. SYSTEMAUFBAU
31
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 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.
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
32
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
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
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.
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]
2.3. SYSTEMAUFBAU
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, 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
soft
firm
hard
flexibility vs. predictability
very flexible, unpredictable
flexible, unpredictable
inflexible, very predictable
portability
unlimited
library mapping
process mapping
IP protection
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/D-Wandler-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 EinChip-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 DMOSLeistungstransistoren dar. Neben dem Mikrocontroller zur Steuerung der Motorbrücke existiert ein
34
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
Abbildung 2.24: Layout eines Ein-Chip-Systems von Texas Instruments (cDSP)
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.
• Kosten: In der geplanten Stückzahl ist die Benutzung von Standardchips kostengünstiger.
• 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 Ein-Chip
Systemen. Schliesslich kann eine Realisierung auch aus mehreren Platinen bestehen, wenn
2.3. SYSTEMAUFBAU
35
Abbildung 2.25: Layout des Ein-Chip-Systems SMARTPOWER
processor
80C51
15-vector
interrupt
timer0 (16 bit)
timer1 (16 bit)
8K8 ROM
(87C552 8K8
EPROM)
256x8 RAM
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
das System nicht auf ein Board passt. Multi-board Systeme können auch 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.
36
KAPITEL 2. ZIELARCHITEKTUREN FÜR HW/SW-SYSTEME
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
8K8
256
83/87
C562
8K8
256
83/87
C652
8K8
256
83/87
C662
8K8
256
83/87
C654
16K8
256
83/87
C528
32K8
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
83
C851
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
Tabelle 2.7: Konfigurationen des 8051 (Quelle: Philips Halbleiter)
83/87
C592
8K8
256
6x8
x
CAN
x
x
10 Bit
x
15
x
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/SWSysteme 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 Implementierung der minimierten Funktionen mit Gattern und Registern aus einer speziellen Bibliothek. Ein weiteres Beispiel für ein Syntheseverfahren ist die Architektur-Synthese. Hier
werden integrierte Schaltungen synthetisiert, die aus Speicherbausteinen, Steuerungslo37
38
KAPITEL 3. SYSTEMENTWURF – METHODEN UND MODELLE
gik 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 Si”
mulierens“ 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
• 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,
3.2. ABSTRAKTION UND ENTWURFSREPRÄSENTATIONEN
39
• 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 BlockDiagramms 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 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
40
KAPITEL 3. SYSTEMENTWURF – METHODEN UND MODELLE
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 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.
3.2. ABSTRAKTION UND ENTWURFSREPRÄSENTATIONEN
41
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.
Logik-Synthese
Aufgabe der Logik-Synthese ist die Generierung einer strukturellen Sicht auf Logik-Ebene.
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 LogikSynthese 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 Logik-Synthese ist
eine strukturelle Repräsentation, die Gatter, Register sowie ihre Verbindungen charakterisiert (Netzliste).
42
KAPITEL 3. SYSTEMENTWURF – METHODEN UND MODELLE
Operationswerk
Steuerwerk
Datenverteilung
Speicher
ALU
*
Steuerungseinheit
Abbildung 3.2: Beispiel einer strukturellen Sicht auf Architektur-Ebene
__
in / 0
in / 0
in / 1
x1
x0
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 mehr aufeinanderfolgende ’1’ im Eingangsstrom
erkennt. Diese Sichten lassen sich in einer Hardware-Beschreibungssprache 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;
3.2. ABSTRAKTION UND ENTWURFSREPRÄSENTATIONEN
43
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 WAIT-Anweisung 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.
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
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. 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:
44
KAPITEL 3. SYSTEMENTWURF – METHODEN UND MODELLE
• 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
P Sichten auf der Blockebene betrachten wir als Verhaltensbeschreibung ein C-Programm, das
i=100 2
i=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);
}
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.
3.2. ABSTRAKTION UND ENTWURFSREPRÄSENTATIONEN
45
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 CADWerkzeugen 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 timeto-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.
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 nichtfunktionale 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.
46
KAPITEL 3. SYSTEMENTWURF – METHODEN UND MODELLE
current frame
a[i]
prediction error
b[i]
Q
DCT
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 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
3.2. ABSTRAKTION UND ENTWURFSREPRÄSENTATIONEN
47
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.
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 VideoCodec
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
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).
48
3.2.3
KAPITEL 3. SYSTEMENTWURF – METHODEN UND MODELLE
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.
Flächenbedarf
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
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 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
3.3
49
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.
Beispiel 3.6 Das folgende Programmstück modelliert eine Schaltung, die eine Differentialgleichung der Form y 00 +3xy 0 +3y = 0 im Intervall [x, a] mit der Schrittweite dx und den Anfangswerten
y(0) = y, y 0 (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
50
KAPITEL 3. SYSTEMENTWURF – METHODEN UND MODELLE
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
x
u
dx
3
y
u
dx
y
dx
x
dx
x1
a
u
y1
c
u1
Abbildung 3.9: Datenflussgraph der Schleife in Beispiel 3.6 (Anweisungen 1 bis 4)
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.
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.
3.3. GRAPHENMODELLE FÜR KONTROLL- UND DATENFLUSS
51
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
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
52
KAPITEL 3. SYSTEMENTWURF – METHODEN UND MODELLE
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 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 Startund Endknoten können drei weitere Hierarchieknoten vorkommen: der Modulaufruf
(CALL), die Verzweigung (BR) und die Iteration (LOOP).
• Einheiten des Sequenzgraphen, die Blätter sind, besitzen ausser den Start- und Endknoten keine weiteren Hierarchieknoten.
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.
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
3.3. GRAPHENMODELLE FÜR KONTROLL- UND DATENFLUSS
53
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
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.
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
PROCEDURE submodul (m,n) IS
p := m + n;
q := m * n;
END submodul;
54
KAPITEL 3. SYSTEMENTWURF – METHODEN UND MODELLE
NOP
CALL
NOP
NOP
NOP
Abbildung 3.12: Modellierung von Modulaufrufen in Sequenzgraphen
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 * b;
y := x * c;
z := a + b;
3.3. GRAPHENMODELLE FÜR KONTROLL- UND DATENFLUSS
55
IF z > 0 THEN
p := m + n;
q := m * n;
END IF;
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.
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
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
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.
56
KAPITEL 3. SYSTEMENTWURF – METHODEN UND MODELLE
Kapitel 4
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.
4.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.
Beispiel 4.1 Abb. 4.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.
57
58
KAPITEL 4. SYSTEMPARTITIONIERUNG
1
2
1
2
5
6
3
3
7
4
4
Abbildung 4.1: DFG und Problemgraph
Architekturgraph Der Architekturgraph GA (VA , EA ) 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 4.2 Abb. 4.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. 4.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 4.3 Abb. 4.3 zeigt den Spezifikationsgraphen für den Problemgraphen in Abb. 4.1 und
den Architekturgraph in Abb. 4.2. Der Knoten 1 kann zum Beispiel nur auf der Ressource vRISC
ausgeführt werden, der Knoten 3 auf vRISC und vHW M 1 . 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.
4.1. MODELLE FÜR DIE SYSTEMSYNTHESE
59
v_RISC
v_HWM1
HWM1
RISC
BR1
Shared bus
BR2
Point-to-point bus
v_BR1
HWM2
v_BR2
v_HWM2
a)
b)
Abbildung 4.2: Architektur und Architekturgraph
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 GA . 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 realisieren, 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.
60
KAPITEL 4. SYSTEMPARTITIONIERUNG
1
v_RISC
5
3
v_BR1
7
v_HWM1
2
v_BR2
6
v_HWM2
4
Abbildung 4.3: Spezifikationsgraph
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. 4.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 Prozessorelemente (PE) mit
lokalem Speicher (M).
In Abb. 4.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
4.1. MODELLE FÜR DIE SYSTEMSYNTHESE
61
1
M
M
M
PE
PE
PE
....
2
3
k
bus
Abbildung 4.4: Problemgraph und Zielarchitektur
Hauptaufgabe liegt bei diesem Problem im Finden eines Ablaufplanes.
1
PE1
2
bus
....
PE2
PE3
k
Abbildung 4.5: Spezifikationsgraph
HW/SW Partitionierung Ein weiterer Spezialfall des allgemeinen Syntheseproblems
ist die HW/SW Partitionierung in ihrer einfachsten Variante. Die funktionalen Objekte des Problemgraphen werden dabei auf zwei funktionale Ressourcen abgebildet, eine
HW- und eine SW-Ressource. Typische Zielarchitekturen sind Ein-Prozessor/Ein-ASIC
Systeme. Abb. 4.6 zeigt ein Beispiel und den dazugehörigen Architekturgraphen. Um
62
KAPITEL 4. SYSTEMPARTITIONIERUNG
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 4.6: Architektur und Architekturgraph für ein einfaches HW/SWPartitionierungsproblem
4.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
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 · hL (L, L̄) + k3 · hP (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 ki dienen dazu, die
4.3. ALLGEMEINE PARTITIONIERUNGSALGORITHMEN
63
verschiedenen Kriterien zu gewichten. Oft werden die Funktionen hi auf die Nebenbedingungen normiert. Durch Wahl entsprechender Konstanten ki 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.
4.3
Allgemeine Partitionierungsalgorithmen
Definition 4.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 ∩ pj = { } ∀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.
• 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).
64
KAPITEL 4. SYSTEMPARTITIONIERUNG
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 , pj );
ENDFOR
ENDFOR
numblocks = n;
k := n + 1
/* Vereinigen der Objekte und Closenessneuberechnung */
WHILE (Terminate(P ) == FALSE)
pi , pj := FindClosestObjects(P );
pk := {pi , pj };
P := P \ pi \ pj ∪ pk ;
numblocks := numblocks − 1;
FOREACH Block pl ∈ P \ pk
ComputeCloseness(pl , pk );
ENDFOR
k := k + 1;
ENDWHILE
RETURN(P );
END PROCEDURE
Abbildung 4.7: Pseudocode für Hierarchisches Clustering
4.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. 4.7 beschrieben.
Beispiel 4.4 Abb. 4.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. 4.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,
4.3. ALLGEMEINE PARTITIONIERUNGSALGORITHMEN
65
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 px , py das arithmetische Mittel der Gewichte aller Objektpaare oi , oj
mit oi ∈ px und oj ∈ 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. 4.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.
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 4.8: Hierarchisches Clustering
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].
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 4.2 (Ratiocut) Sei P = {pi , pj } und cut(P ) die Summe der Gewichte der
Kanten zwischen pi und pj . Die Anzahl der Objekte in pi und pj (Blockgrössen) sei size(pi )
und size(pj ). Das Ratio von P ist gegeben durch
ratio =
cut(P )
size(pi )size(pj )
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.
66
KAPITEL 4. SYSTEMPARTITIONIERUNG
Die Verfahren der konstruktiven Partitionierung haben generell das Problem, dass es
sehr schwierig sein kann, Closenessfunktionen zu definieren, die alle Entwurfsbeschränkungen geeignet beinhalten.
4.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.
• 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. 4.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
)
4.3. ALLGEMEINE PARTITIONIERUNGSALGORITHMEN
67
PROCEDURE SIMULATED-ANNEALING(P )
temp = Anfangstemperatur;
cost = c(P );
WHILE (Frozen == FALSE)
WHILE (Equilibrum == FALSE)
P 0 = RandomMove(P );
cost0 = c(P 0 );
deltacost = cost0 − cost;
IF (Accept(deltacost, temp) > Random[0,1))
P = P 0;
cost = cost0 ;
ENDIF
ENDWHILE
temp = DecreaseTemp(temp);
ENDWHILE
RETURN(P );
END PROCEDURE
Abbildung 4.9: Pseudocode für Simulated Annealing
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 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.
4.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
68
KAPITEL 4. SYSTEMPARTITIONIERUNG
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.
4.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
oj 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
m
X
ck =
(4.1)
xi,k = 1
1≤i≤n
(4.2)
xi,k · ci,k
1≤k≤m
(4.3)
k=1
n
X
i=1
P
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.
4.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 , pHW }. 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.
4.4. ALGORITHMEN ZUR HW/SW-PARTITIONIERUNG
69
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 4.10: Pseudocode für ein greedy Partitionierungsverfahren
PROCEDURE HW ORIENTED GREEDY
P = {O, { }}
REPEAT
Pold =P ;
FOREACH (oi ∈ pHW )
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 (oj ∈ Successors(oi ))
TryMove(P ,oj );
ENDFOR
ENDIF
END PROCEDURE
Abbildung 4.11: Pseudocode eines hardwareorientierten greedy Partitionierungsverfahrens
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. 4.10 aufgebaut.
Dabei wird die Prozedur Move(P, oi ) verwendet, die eine neue Partition P 0 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. 4.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. 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 Performanceanforde-
70
KAPITEL 4. SYSTEMPARTITIONIERUNG
rungen erfüllt sind. Die Partitionierung erfolgt mit Simulated Annealing, was den Vorteil
hat, dass aus einem lokalen Minimum entwichen werden kann.
4.5
4.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 , pj ) =
sharedwires(pi , pj )
maxwires(P )
¶c2 µ
×
maxsize
min{size(pi ), size(pj )}
¶c3 µ
×
maxsize
size(pi ) + size(pj )
In dieser Formel bedeuten:
• sharedwires(pi , pj ) = c1 × commoninputs(pi , pj ) + internalwires(pi , pj ),
• commoninputs(pi , pj ): Anzahl der Bits von gemeinsamen Eingängen, d.h.,
Eingängen, die sowohl von Objekten des Blocks pi als auch von Objekten des Blocks
pj benutzt werden,
• internalwires(pi , pj ): Anzahl der Verbindungen (in Bit) zwischen Objekten oi ∈ pi
und oj ∈ pj ,
• maxwires(P ): maxpi ,pj ∈P :i6=j {sharedwires(pi , pj )},
• 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:
¶
4.5. ENTWURFSSYSTEME ZUR FUNKTIONALEN PARTITIONIERUNG
Ã
Closeness(oi , oj ) =
Ã
+
shareddata(oi , oj )
totaldata(oi , oj )
71
!
+
f cost(oi ) + f cost(oj ) − cost(oi , oj )
cost(oi , oj )
!
− n × par(oi , oj )
In dieser Formel bedeuten:
• shareddata(oi , oj ): Anzahl der von beiden Operationen oi , oj 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 oj direkter
Nachfolger von oi ist (oder umgekehrt).
• totaldata(oi , oj ): Man zieht im CDFG eine Hülle um oi und oj 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 , oj ): minimale Kosten (bzgl. Fläche und Performance) der benötigten funktionalen Einheiten, um beide Operationen zu implementieren.
• par(oi , oj ): 1, falls oi und oj 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.
72
KAPITEL 4. SYSTEMPARTITIONIERUNG
4.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 Hardwareund 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.
• 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 Kontroll- und Datenflussabhängigkeiten erweitert wird, umgewandelt.
4.5. ENTWURFSSYSTEME ZUR FUNKTIONALEN PARTITIONIERUNG
73
• 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 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.
74
KAPITEL 4. SYSTEMPARTITIONIERUNG
Kapitel 5
Compiler und Codegenerierung
5.1
5.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. 5.1).
Quellprogramm
Zielprogramm
COMPILER
Fehlermeldungen
Abbildung 5.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. 5.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.
75
76
KAPITEL 5. COMPILER UND CODEGENERIERUNG
skeletal source program
preprocessor
source program
compiler
target assembly program
assembler
relocatable machine code
linker / loader
library (relocatable object files)
absolute machine code
Abbildung 5.2: Werkzeuge beim Übersetzungsprozess
5.1.2
Phasen eines Compilers
Ein Compiler besteht, wie in Abb. 5.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
Sätze werden durch die Grammatik der Quellsprache definiert. Die Grammatik wird
durch rekursive Regeln ausgedrückt. Die Regeln
5.1. COMPILER – AUFBAU
77
Quellprogramm
Analyse
Lexikalische Analyse
Syntaxanalyse
Semantikanalyse
Symboltabellenverwaltung
Fehlerbehandlung
Zwischencodegenerierung
Codeoptimierung
Codegenerierung
Synthese
Zielprogramm
Abbildung 5.3: Die Phasen eines Compilers
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 5.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. 5.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.
78
KAPITEL 5. COMPILER UND CODEGENERIERUNG
Bezeichner
position
Parsebaum
Syntaxbaum
Zuweisung
:=
:=
Ausdruck
+
Ausdruck
Bezeichner
Ausdruck
*
initial
Bezeichner
Zahl
rate
60
Ausdruck
+
position
*
initial
Ausdruck
rate
60
Abbildung 5.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.
5.1. COMPILER – AUFBAU
79
– 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 5.2 Abb. 5.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.
5.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. 5.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. 5.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.
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
80
KAPITEL 5. COMPILER UND CODEGENERIERUNG
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 5.5: Repräsentation des Quellprogrammes nach den verschiedenen Compilerphasen
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
5.1. COMPILER – AUFBAU
81
Syntaxbaum
DAG
:=
:=
+
a
a
+
*
*
-
b
b
c
*
b
-
c
c
Abbildung 5.6: Die graphische Zwischendarstellungen Syntaxbaum und DAG
den Operatoren ist einer der Zuweisungsoperator. Der obige 3-Adress Befehl definiert die
Variable x und verwendet (referenziert) y und z.
Beispiel 5.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
– goto L
Diese Anweisung ist ein unbedingter Sprung zum Label L, der Adresse einer
weiteren 3-Adress Anweisung.
82
KAPITEL 5. COMPILER UND CODEGENERIERUNG
– 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 5.4 Die folgenden beiden 3-Adress Codesequenzen ergeben sich durch die Zwischencodegenerierung aus dem Syntaxbaum und dem DAG in Abb. 5.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
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.
5.1. COMPILER – AUFBAU
83
• 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.
5.1.4
Grundblöcke und Kontrollflussgraphen
Definition 5.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 5.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.
• 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.
84
KAPITEL 5. COMPILER UND CODEGENERIERUNG
Beispiel 5.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. 5.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 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.
Beispiel 5.6 Abb. 5.8 zeigt den Kontrollflussgraphen zum Beispiel der Berechnung des Skalarprodukts, in dem Knotenmengen, die Grundblöcke darstellen, zu jeweils einem Knoten zusammengefasst sind. Die Bedingungen für die verschienden Kontrollflusspfade sind nicht dargestellt.
5.2. CODEGENERIERUNG
85
t6, prod
+
prod
t5
*
0
[]
a
t4
t2
[]
<=
t1, t3
b
*
4
+
19
t7, i
1
i0
Abbildung 5.7: DAG zur Berechnung des Skalaproduktes
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 5.8: Kontrollflussgraph mit Grundblöcken als Knoten
5.2
Codegenerierung
In Abb. 5.3 ist die Codegenerierung als letzte Phase eines Compilers dargestellt. Der
Codegenerator liest die (eventuell optimierte) Zwischendarstellung des Quellprogramms
86
KAPITEL 5. COMPILER UND CODEGENERIERUNG
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.
• 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.
5.2. CODEGENERIERUNG
87
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 5.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 5.8 Dieses Beispiel zeigt, wie die Reihenfolge, in der die Berechnungen ausgeführt werden, den resultierenden Zielcode beeinflusst. Gegeben ist der DAG in Abb. 5.9, der die Anweisung
(a + b) - (e - (c + d))
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:
88
KAPITEL 5. COMPILER UND CODEGENERIERUNG
t4
t1
t3
-
+
t2
a
b
e
+
c
d
Abbildung 5.9: DAG für den Grundblock in Beispiel 5.8
/* 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
5.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
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 5.1 listet
die möglichen Adressierungsarten auf. Dabei gibt die contents(x) den Inhalt des Registers
oder der Speicherstelle x an.
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:
5.2. CODEGENERIERUNG
mode
absolute
register
indexed
indirect register
indirect indexed
immediate
form
M
R
c(R)
*R
*c(R)
#c
89
address
M
R
c + contents(R)
contents(R)
contents(c + contents(R))
c
added cost
1
0
1
0
1
1
Tabelle 5.1: Adressierungsarten der Modellmaschine
MOV
RO, a
ADD
R2, R1
SUB
*R2, *R1
MOV
*4(R3), b
Beispiele für Codesequenzen mit indizierten Anweisungen sind in Tabelle 5.2, für
Pointer-Anweisungen in Tabelle 5.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.
statement
a := b[i]
i in register Ri
code
cost
MOV b(Ri),R 2
a[i] := b
MOV b,a(Ri)
3
i in memory Mi
code
cost
MOV Mi,R
4
MOV b(R),R
MOV Mi,R
5
MOV b,a(R)
i in stack
code
MOV Si(A),R
MOV b(R),R
MOV Si(A),R
MOV b,a(R)
cost
4
5
Tabelle 5.2: Beispiele für Codesequenzen mit indizierten Anweisungen
statement
a := *p
p in register Rp
code
cost
MOV *Rp,a 2
*p := a
MOV a,*Rp
2
p in memory Mp
code
cost
MOV Mp,R 3
MOV *R,R
MOV Mp,R 4
MOV a,*R
p in stack
code
MOV SP(A),R
MOV *R,R
MOV a,R
MOV R,*SP(A)
cost
3
4
Tabelle 5.3: Beispiele für Codesequenzen mit Pointeranweisungen
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
90
KAPITEL 5. COMPILER UND CODEGENERIERUNG
Register oder eine Speicherstelle abzulegen. Das folgende Beispiel zeigt die Codesequenz
für die 3-Adress Abweisung if x<y goto z:
CMP
JLE
5.2.2
x,y
z
Einfacher Codegenerator
Definition 5.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 5.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.
(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 Register-Deskriptor
und einen Adress-Deskriptor. Der Register-Deskriptor ist eine Datenstruktur, die eine
5.2. CODEGENERIERUNG
91
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 Adress-Deskriptor 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 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.
92
KAPITEL 5. COMPILER UND CODEGENERIERUNG
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.
5.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, 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 5.5 (Registerkonfliktgraph) Ein Registerkonfliktgraph Gk (Vk , Ek ) ist ein
ungerichteter Graph, in dem die Knotenmenge Vk symbolische Register darstellt. Die Kantenmenge Ek = {(vi , vj ) : vi 6∼ vj ; vi , vj ∈ Vk } drückt die Konfliktrelationen der Register
aus. vi 6∼ vj bedeutet, dass vi an einem Punkt aktiv ist, an dem vj definiert wird.
Beispiel 5.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. 5.10 dargestellt. Der daraus resultierende Konfliktgraph ist in Abb. 5.11 dargestellt. In dem
Graph existiert je eine Kante zwischen zwei Knoten (Variablen), deren Aktivitätsintervalle sich
schneiden.
5.2. CODEGENERIERUNG
93
1 Periode
t1
t2
t3
t4
i=1 i=2 i=3 i=4
Abbildung 5.10: Definitionszeitpunkte und Aktivitätsintervalle der Variablen in Beispiel
5.9
t1
t1
t2
t4
t2
t4
t3
t3
a) Konfliktgraph
..
b) Farbung mit zwei Farben
Abbildung 5.11: Registerkonfliktgraph zu Beispiel 5.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 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.
0
2. Entferne vi und alle Kanten, die von vi ausgehen. Dadurch wird ein neuer Graph 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.)
0
3. (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
94
KAPITEL 5. COMPILER UND CODEGENERIERUNG
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 5.10 Der Konfliktgraph in Abb. 5.11 kann mit der vorgestellten Heuristik nicht mit
k = 2 Farben gefärbt werden (obwohl es, wie Abb. 5.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;
Verwendungszähler
Definition 5.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:
X
(verwendet(x, B) + 2 · aktiv(x, B))
Blöcke B∈L
5.2. CODEGENERIERUNG
95
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.
bcdf
a := b+c
d := d-b
e := a+f
B1
acdef
acde
acdf
B2
f := a-d
b := d+f
e := a-c
cdef
cdef
b := d+c
bcdef
B3
bcdef
B4
bdef
bdef
Abbildung 5.12: Kontrollflussgraph zu Beispiel 5.11
Beispiel 5.11 Abb. 5.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.
X
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
Damit ergibt sich:
X
B∈L
verwendet(a, B) = 2
96
KAPITEL 5. COMPILER UND CODEGENERIERUNG
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
5.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 5.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:
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 5.12 Für den DAG in Abb. 5.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
5.2. CODEGENERIERUNG
97
1
*
2
3
−
+
4
*
6
5
−
+
8
7
c
9
d
10
e
+
11
a
12
b
Abbildung 5.13: DAG für Beispiel 5.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
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.
5.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 5.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.
98
KAPITEL 5. COMPILER UND CODEGENERIERUNG
Beispiel 5.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.
op
T1
T2
Abbildung 5.14: Syntaxbaum T für E
Abb. 5.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.
Definition 5.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
T 1, T 2 oder T 2, T 1) 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 P 0 gibt, für die gilt:
• P 0 besitzt keine höheren Kosten als P
• P 0 benutzt nicht mehr Register als P
• P 0 wertet den Baum benachbart aus
5.2. CODEGENERIERUNG
99
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 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.
Beispiel 5.14 Die Abb. 5.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 := Mj
100
KAPITEL 5. COMPILER UND CODEGENERIERUNG
+
-
*
a
b
c
/
d
e
Abbildung 5.15: Syntaxbaum zu Beispiel 5.14
Ri
Ri
Ri
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 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
5.3. CODEOPTIMIERUNG
101
demnach der Kostenvektor (8, 8, 7).
Die Ergebnisse der Phasen 1 bis 3 sind in Abb. 5.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
R0:=R0+R1
+ (8,8,7)
erst rechts
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 5.16: Dynamische Programmierung zur optimalen Befehlsauswahl, Ablaufplanung und Registervergabe
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.
5.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 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
102
KAPITEL 5. COMPILER UND CODEGENERIERUNG
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.
5.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 peephole optimization durchgeführt werden,
aufgezählt:
• Entfernung überflüssiger Anweisungen
Beispiel 5.15 Durch die Codegenerierung werden häufig überflüssige Load- und StoreBefehle 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 5.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
5.3. CODEOPTIMIERUNG
103
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 5.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 5.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
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 5.19 Besitzt die Zielmaschine z.Bsp. autoinkrement/autodekrement Adressierungsmodi, können Anweisungen, die Adresszähler erhöhen bzw. erniedrigen, oft eliminiert
werden.
5.3.2
Lokale Optimierung
Definition 5.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:
104
KAPITEL 5. COMPILER UND CODEGENERIERUNG
• Eliminierung gemeinsamer Teilausdrücke (common subexpression elimination)
Beispiel 5.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
• Umbenennung von Zwischenvariablen (variable renaming)
Beispiel 5.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 5.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.
5.3. CODEOPTIMIERUNG
5.3.3
105
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 5.23 In der folgenden Anweisungssequenz
(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 5.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) {
....
}
106
KAPITEL 5. COMPILER UND CODEGENERIERUNG
transformiert werden.
• Induzierte Variablen und Operatorreduktion
Beispiel 5.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
(1)
(2)
(3)
(4)
....
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 -.
5.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.
5.4. CODEGENERIERUNG FÜR SPEZIALPROZESSOREN
107
• 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 (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
108
KAPITEL 5. COMPILER UND CODEGENERIERUNG
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 Instruktion 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.
5.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 5.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 5.26 Abb. 5.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 5.4 zeigt einen Teil der Instruktionen des Prozessors. In Abb. 5.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).
5.4. CODEGENERIERUNG FÜR SPEZIALPROZESSOREN
109
Abbildung 5.17: Blockschaltbild des DSPs TMS320C25
Mit diesen Instruktionen soll für den DAG in Abb. 5.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. 5.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 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
110
KAPITEL 5. COMPILER UND CODEGENERIERUNG
add:
apac:
spac:
a
a
+
a
+
a
m
-
a
mpy:
p
a
mpyk:
p
p
lack:
CONST
pac:
p
a
sacl:
a
m
lac:
m
a
lt:
m
t
p
*
*
t
p
m
t
CONST
Abbildung 5.18: Instruktionen des TMS320C25 als tree pattern
number
1
2
3
4
5
6
7
8
9
10
instruction
add
apac
spac
mpy
mpyk
lack
pac
sacl
lac
lt
destination
a
a
a
p
p
a
a
m
a
t
cost
1
1
1
1
1
1
1
1
1
1
Tabelle 5.4: Teil der Instruktionen des TMS320C25
m6
a
p
a
p
*
*
a
m0
m1
m5
t
m4
+
m2
m3
t
a
Abbildung 5.19: DAG für Beispiel 5.26
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.
5.4. CODEGENERIERUNG FÜR SPEZIALPROZESSOREN
/* Version a) */
lt
m1
mpy
m0
pac
sacl m7
lac
m3
add
m2
sacl m5
lt
m4
mpy
m5
lac
m7
spac
sacl m6
/* 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
111
/* 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 NFS-Ablaufpläne.
Gesucht sind Verfahren zur Codegenerierung, die für Prozessoren mit solchen nichthomogenen 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 5.2.5, wobei jeder Befehl des Prozessors als tree pattern (wie in
Abb. 5.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 5.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 rj wird mit den Instruktionen beschriftet, die Operanden aus ri lesen und das Ergebnis
nach rj 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. 5.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 5.4 beschriftet.
Definition 5.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 gleicher Beschriftung hat, und ii) es mindestens einen gerichteten Zyklus
112
KAPITEL 5. COMPILER UND CODEGENERIERUNG
1, 2, 3
2, 3, 7
a
1, 9
8
4, 5
p
t
4
10
m
Abbildung 5.20: Registertransfergraph für den TMS320C25
zwischen r1 und r2 gibt, gilt: In jedem möglichen Zyklus zwischen r1 und r2 existiert ein
Speicherknoten.
Aus Abb. 5.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.
5.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. 5.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.
Der Motorola 56000 bietet auch direkte und indirekte Adressierungmodi an. Bei der
direkten Adressierung wird die effektive Adresse aus dem Instruktionswort extrahiert. Für
5.4. CODEGENERIERUNG FÜR SPEZIALPROZESSOREN
113
Abbildung 5.21: Adressrechenwerke von DSPs
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
Indexregisters, die Modifikationsregister werden für Post-Modifikationen der Indexregister
114
KAPITEL 5. COMPILER UND CODEGENERIERUNG
verwendet.
Abbildung 5.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. 5.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 5.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.
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
5.4. CODEGENERIERUNG FÜR SPEZIALPROZESSOREN
operation
AR load
MR load
ARP load
MRP load
immediate modify
auto-increment
auto-decrement
auto-modify
functionality
AR[ARP] = value
MR[MRP] = value
ARP = value
MRP = value
AR[ARP] += value
AR[ARP] ++
AR[ARP] -AR[ARP] += MR[MRP]
115
cost
1
1
0
0
1
0
0
0
Tabelle 5.5: Operationen der generischen Adressgenerierungseinheit
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 5.23: Adressierungsoperationen für Beispiel 5.27
Beispiel 5.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. 5.23a) zeigt die Adressgenerierung mit
einem Adressregister AR, falls die Variablen in der Reihenfolge a, b, c, d im Speicher abgelegt sind.
116
KAPITEL 5. COMPILER UND CODEGENERIERUNG
Die gesamten Kosten für die Adressierung sind gleich 9. Abb. 5.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. 5.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 5.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 , vj ), vi , vj ∈ V, vi 6= vj
verbinden Variablen, die in der Zugriffssequenz aufeinander folgen und die Gewichte der
Kanten ω : E → N0 geben die Transitionshäufigkeit zwischen den Variablen an.
Definition 5.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 , vj ) für zwei Variablen vi , vj ∈ V in Bezug auf eine Adresszuweisung
π ist |π(vi ) − π(vj )|. Variablen in aufeinanderfolgenden Speicherzellen haben demnach eine
Distanz von 1. Damit lässt sich ein Kostenwert für eine Adresszuweisung angeben:
cost(π) = 1 +
X
ω(e)
e={vi ,vj }∈Ê
mit
Ê = {{vi , vj } ∈ E | δπ (vi , vj ) > 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:
(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
5.4. CODEGENERIERUNG FÜR SPEZIALPROZESSOREN
1
a
b
a
4
b
117
0
c
1
a
2
d
3
b
4
3
1
3
1
1
c
2
d
c
Zugriffsgraph
d
Pfad mit maximalem Gewicht
b)
a)
Adresszuweisung
c)
Abbildung 5.24: Berechnung der optimalen Adresszuweisung für Beispiel 5.28
• sie keinen Zyklus in P erzeugt
• sie keinen Knoten in P mit fanout > 2 erzeugt
Beispiel 5.28 Der Zugriffsgraph für die Zugriffssequenz in Beispiel 5.27 ist in Abb. 5.24a) dargestellt. Abb. 5.24b) zeigt den Pfad mit maximalem Gewicht, und Abb. 5.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 5.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
X
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 mit einem Wert m
geladen, sind nicht nur Variablen mit der Adressdifferenz von 1 benachbart, sondern auch
mit der Adressdifferenz von m.
118
KAPITEL 5. COMPILER UND CODEGENERIERUNG
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[f1 (i1 )][f2 (i2 )] . . . [fn (in )].
Dabei sind die Indizierungsfunktionen von der Art fj (ij ) = {c, ij + c, ij − c, c + ij , c −
ij }, wobei c ∈ N0 und ij 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
adr(A[m1 ][m2 ] . . . [mn ]) = base(A) +
n
X
mj .aj
j=1
wobei
( Q
n
aj =
k=j+1 Nk ,
1,
1≤j<n
j=n
Beispiel 5.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][4][1] führt zu folgender Adresse im Speicher:
adr(A[5][4][1] = base(A)+m1 .a1 +m2 .a2 +m3 .a3 = base(A)+5×20+4×5+1×1 = base(A)+121.
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
Jeder Arrayreferenz kann ein sogenannter update value (UV) zugeordnet werden. Der
UV einer Referenz ist die Differenz der Adressen in aufeinanderfolgenden Iterationen der
Schleife.
U V (A[f1 (i)] . . . [fn (i)]) = adr(A[f1 (i1 ) . . . fn (i1 )]) − adr(A[f1 (i0 ) . . . fn (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
5.4. CODEGENERIERUNG FÜR SPEZIALPROZESSOREN
119
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 5.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:
Definition 5.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 , rj ) zwischen zwei Knoten besteht, falls
1. i < j
2. U V (ri ) = U V (rj )
3. |adr(ri ) − adr(rj )| ≤ 1
120
KAPITEL 5. COMPILER UND CODEGENERIERUNG
1
2
3
4
5
6
7
Abbildung 5.25: Intra-Iterationsgraph zu Beispiel 5.30
Eine Kante von ri nach rj gibt an, dass die Adresse von rj 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 rj ein Adressregister teilen. Abb. 5.25 zeigt den IntraIterationsgraph zum Beispiel 5.30.
Als nächstes betrachtet man zwei aufeinander folgende Iterationen der Schleife.
Definition 5.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 , rj0 ) zwischen zwei Knoten besteht, falls
1. i > j
2. U V (ri ) = U V (rj )
3. |adr(ri ) + U V (ri ) − adr(rj )| ≤ 1
In diesem Fall kann die Adresse für ri in der nächsten Iteration (adr(ri ) + U V (ri ))
von der Adresse von rj in der aktuellen Iteration ohne Zusatzkosten berechnet werden.
Abb. 5.26 zeigt den Inter-Iterationsgraphen für das Beispiel 5.30.
Schliesslich erhält man den gesamten Distanzgraphen durch Vereinigung von Intraund Inter-Iterationsgraphen.
Definition 5.17 (Distanzgraph)
Für einer Sequenz von Arrayreferenzen (r1 , r2 , . . . , rk ) in einer Schleife mit dem IntraIterationsgraph Gintra (V1 , E1 ) und dem Inter-Iterationsgraph Ginter (V2 , E2 ) erhält man
den Distanzgraphen Gdist (V1 ∪ V2 , E1 ∪ E2 ).
5.4. CODEGENERIERUNG FÜR SPEZIALPROZESSOREN
1
1’
2
2’
3
3’
4
4’
5
5’
6
6’
7
7’
121
Abbildung 5.26: Inter-Iterationsgraph zu Beispiel 5.30
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 5.31 Abb. 5.27 zeigt den Distanzgraph zu Beispiel 5.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:
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[3])
AR4 = adr(A[7])
FOR i=2 TO 1024 DO
AR1 --
/*
/*
/*
/*
AR1
AR2
AR3
AR4
zeigt
zeigt
zeigt
zeigt
auf
auf
auf
auf
r1
r4
r3
r7
*/
*/
*/
*/
/* bereitet (2) vor */
122
KAPITEL 5. COMPILER UND CODEGENERIERUNG
1
1’
2
2’
3
3’
4
4’
5
5’
6
6’
7
7’
Abbildung 5.27: Distanzgraph zu Beispiel 5.30
AR1 ++
AR3 ++
AR2 ++
AR1
AR2
AR4 ++
ENDFOR
/*
/*
/*
/*
/*
/*
bereitet
bereitet
bereitet
bereitet
bereitet
bereitet
(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.
5.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
5.5. RETARGETABLE COMPILER
123
Zugriff (random access) auf die Worte des Zielcodes (Sprünge, Schleifen). Die im generalpurpose 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. 5.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 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.
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.
5.5
Retargetable Compiler
Retargetable Compiler sind Compiler, die sich relativ rasch an neue Zielarchitekturen
anpassen lassen. Man unterscheidet drei Arten von retargetable Compilern:
124
KAPITEL 5. COMPILER UND CODEGENERIERUNG
Abbildung 5.28: Hardwareerweiterung für das EPM-Modell zur Codekomprimierung
• 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.
• 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 GnuC 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.
5.5. RETARGETABLE COMPILER
125
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.
5.5.1
Codegenerierung durch Baumübersetzung
Beispiel 5.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. 5.29 stellt den Syntaxbaum für diese Anweisung mit den notwendigen Adressrechnungen dar.
:=
ind
+
+
+
consta
memb
const1
ind
regSP
consti
+
regSP
Abbildung 5.29: Syntaxbaum für die Anweisung a[i] := b + 1
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}
(5.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. (5.1).
Beispiel 5.33 Ein Beispiel einer Baumumwandlungsregel für die Addition zweier Registeroperanden ist in Abb. 5.30 dargestellt. Falls ein gegebener Syntaxbaum einen Teilbaum enthält, der
126
KAPITEL 5. COMPILER UND CODEGENERIERUNG
reg i
+
reg i
{ADD Rj, Ri}
reg j
Abbildung 5.30: Baumumwandlungsregel für einen Additionsbefehl mit zwei Registeroperanden
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 5.34 Abb. 5.31 zeigt eine Menge von Regeln, die Instruktionen einer Zielmaschine
darstellen. In Abb. 5.32 werden diese Regeln auf den Syntaxbaum in Abb. 5.29 angewandt. Zuerst
wird Regel (1), dann Regel (7) (dargestellt in Abb. 5.32a) angewendet. Es folgen die Regeln (6)
(Abb. 5.32b)), (2), (8) und (4) (Abb. 5.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
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. 5.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, automatisch
einen pattern matcher erzeugen.
5.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
5.5. RETARGETABLE COMPILER
127
(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
{ADD c(Rj), Ri}
+
reg i
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 5.31: Baumübersetzungsschema
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 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
128
KAPITEL 5. COMPILER UND CODEGENERIERUNG
:=
=
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
c)
consti
regSP
d)
:=
:=
VI: (4) {MOV R1,*R0}
ind
+
reg 0
memb
ind
const1
reg 0
+
reg 1
const1
IV: (2) {MOV b, R1}
V: (8) {INC R1}
Abbildung 5.32: Anwendung eines Baumübersetzungsschemas
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].
5.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
5.5. RETARGETABLE COMPILER
129
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.
130
KAPITEL 5. COMPILER UND CODEGENERIERUNG
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
| E(D) − M (D) |
A=1−
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.
131
132
KAPITEL 6. ABSCHÄTZUNG DER ENTWURFSQUALITÄT
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 X
n
X
2
µij
n(n − 1) i=1 j=i+1
wobei µij mit 1 ≤ i, j, ≤ n, i 6= j gegeben ist durch


1



µij =




if
0
E(Di ) > E(Dj ) ∧ M (Di ) > M (Dj )∨
E(Di ) < E(Dj ) ∧ M (Di ) < M (Dj )∨
E(Di ) = E(Dj ) ∧ M (Di ) = M (Dj )
else
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 tradeoff. 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
Mem
Mem
Mem
+
+
+
+
FU
FU + Reg
FU + Reg + Mux
FU + Reg + Mux + Wiring
133
Voraussetzung
Speicher-Allokation
FU-Allokation
Lebenszeitanalyse
FU-Bindung
Floorplanning
Exaktheit
gering
|
|
|
∨
hoch
Treue
gering
|
|
|
∨
hoch
Geschwindigkeit
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/M Hz] angegeben.
• Testbarkeit: Das Testen einer Schaltung kann entweder durch Testgeräte (Anlegen
von Testsignalen und Überprüfen der Ausgänge) oder durch einen BIST (built-in selftest) 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.
6.2.1
Performancemasse
Die Performancemasse werden in Masse für Hardwareimplementierungen, Softwareimplementierungen und Kommunikation unterteilt.
134
KAPITEL 6. ABSCHÄTZUNG DER ENTWURFSQUALITÄT
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. 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
6.2. QUALITÄTSMASSE
135
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 1111
0000
0000
0000 1111
1111
0000
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.
Performancemasse für Software
Die Laufzeit T eines Programmes auf einem Prozessor lässt sich folgendermassen angeben:
T = Ic × CP I × τ =
Ic × CP I
f
Dabei ist Ic die Anzahl der Instruktionen des Programmes (instruction count), CP I die
durchschnittliche Anzahl der benötigten Taktzyklen pro Instruktion (cycles per instruction) und τ = 1/f die Taktperiode des Prozessors. Da die verschiedenen Instruktionen
136
KAPITEL 6. ABSCHÄTZUNG DER ENTWURFSQUALITÄT
verschiedene Ausführungszeiten haben können, ist der CPI Wert ein Mittelwert über die
Instruktionen des Programmes.
Bei GP-Prozessoren wird oft ein prozessorspezifischer CP I Wert ermittelt, indem man
die Instruktionen von Benchmark-Programmen untersucht. Basierend auf diesem Wert
wird die Performance eines Prozessors auch gerne durch seine M IP S-Rate (million instructions per second) angegeben:
f
Ic
=
6
T.10
CP I.106
Durch Pipelining und mehrere skalare Einheiten erreichen moderne GP-Prozessoren CP I
Werte von 0.6 bis 0.2, VLIW- und Vektormaschinen können CP I Werte bis 0.1 erreichen
[34].
M IP S-Rate =
Beispiel 6.5 Ein Programm mit 6800 Instruktionen wird auf einem Prozessor mit einem CPIWert von 0.4, der mit 400M Hz getaktet wird, ausgeführt. Die Ausführungszeit des Programmes
ergibt sich zu
T =
6800 × 0.4
Ic × CP I
=
= 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 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
6.2. QUALITÄTSMASSE
137
• 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 = 56M b/s. Die Spitzenrate ist peakrate(C) = 8Bits/100ns = 80M b/s. Ein physikalischer Kanal, der diese Kommunikation implementieren kann, muss demnach eine maximale
Bitrate von 80M b/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:
Tcomm = Tof f set +
m size
Bitrate
Die Kommunikationszeit setzt sich aus zwei Teilen zusammen, einer Offsetzeit Tof 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
138
KAPITEL 6. ABSCHÄTZUNG DER ENTWURFSQUALITÄT
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 ILP-Suche.
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
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 Diese Methode wurde in [57] vorgestellt.
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.
Im allgemeinen gilt, dass ein kleinerer Schlupf einer Funktionseinheit auch zu kleineren
Ausführungszeiten bei gleicher Anzahl von Ressourcen führt.
6.3. ABSCHÄTZUNG VON HARDWARE
139
Operatoren
Taktperiode
Mul
Add
Schlupf
Sub
100
50
Belegung FU
150
Zeit (ns)
163
T(MOD)
Schlupf
Abbildung 6.5: Clockschlupf der funktionalen Einheiten bei der MOD-Methode zur Bestimmung der Taktperiode.
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 :
P|VT |
avgslack(T ) =
k=1 (occ(vk ) × slack(T, vk ))
P|VT |
k=1 occ(vk )
Damit kann man die Taktauslastung definieren:
Definition 6.5 (Taktauslastung) Die Taktauslastung
util(T ) = 1 −
avgslack(T )
T
bezeichnet die prozentuale mittlere Auslastung aller Funktionseinheiten.
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.
140
6.3.3
KAPITEL 6. ABSCHÄTZUNG DER ENTWURFSQUALITÄT
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
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(M ux) +
del(F U ) + del(N S) + setup(SR) +
X
1≤i≤6
del(ni )
(6.1)
6.3. ABSCHÄTZUNG VON HARDWARE
141
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(M ux): Verzögerung der Multiplexer
• del(F U ): Verzögerung der Funktionseinheiten (FU)
• del(N S): 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)
• 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.
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.
142
KAPITEL 6. ABSCHÄTZUNG DER ENTWURFSQUALITÄT
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
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.
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
6.4. ABSCHÄTZUNG VON SOFTWARE
143
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 realtime 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:
• 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
144
KAPITEL 6. ABSCHÄTZUNG DER ENTWURFSQUALITÄT
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 worstcase 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
(x6)
(x7)
/* k >= 0 */
s = k;
WHILE (k < 10) {
IF (ok)
j++;
ELSE {
j=0;
ok = TRUE;
}
k++;
}
r=j;
ist der (entartete) CFG in Abb. 6.8 dargestellt.
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 W CET
von P
W CET =
N
X
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
6.4. ABSCHÄTZUNG VON SOFTWARE
145
d1
B1 s=k
d2
B2 WHILE(k<10)
d3
B3
d5
d4
B4
B5 j=0;
ok=TRUE;
j++
d6
B6
d9
B7
d8
IF (ok)
d7
k++;
r=j;
d10
Abbildung 6.8: CFG für das Programm in Beispiel 6.10
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.
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
= 1
= d1 = d2
x2
x3
= d2 + d8 = d3 + d9
= d3 = d4 + d5
x4
x5
= d4 = d6
= d5 = d7
x6
x7
= d6 + d7 = d8
= d9 = d10
Die funktionalen Beschränkungen werden vom Benutzer/Programmierer spezifiziert.
Dazu gehören beispielsweise Schranken für Schleifenzähler.
146
KAPITEL 6. ABSCHÄTZUNG DER ENTWURFSQUALITÄT
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
W CET = max{
N
X
ci xi | d1 = 1 ∧
i=1
X
j∈in(Bi )
dj =
X
dk = xi
i = 1, · · · , N ∧
k∈out(Bi )
funktionale Beschränkungen }
bestimmt die WCET von P .
In diesem ILP sind die Variablen die Werte dj , 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.
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:
6.4. ABSCHÄTZUNG VON SOFTWARE
147
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
• 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)
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 l-Block
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.
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.
148
KAPITEL 6. ABSCHÄTZUNG DER ENTWURFSQUALITÄT
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
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.
Sei die Anzahl der Ausführungen von l-Block Bi,j gleich xi,j und die Anzahl der Cachemiss
Hits bzw. Cache-Misses gleich xhit
i,j bzw. xi,j , dann gilt:
miss
xi = xi,j = xhit
1 ≤ j ≤ ni
i,j + xi,j
miss die Kosten von Hit bzw. Miss von l-Block B , dann
Seien im weiteren chit
i,j
i,j und ci,j
erhält man die WCET des Programmes zu:
W CET =
ni
N X
X
i
hit
miss miss
(chit
i,j xi,j + ci,j xi,j )
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:
xmiss
≤1
k,l
6.4. ABSCHÄTZUNG VON SOFTWARE
149
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
xmiss
1,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
prog size(Bi ) =
X
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.
150
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.
Sei D die Menge aller Deklarationen eines Programms. Eine Gesamtschätzung des
Datenspeicherbedarfs data size kann man aus folgender Formel erhalten:
data size =
X
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.
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.
[13] T. A. Chu. On the models for designing VLSI asynchronous digital systems. Integration: the VLSI journal, 1986.
151
152
LITERATURVERZEICHNIS
[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.
[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.
LITERATURVERZEICHNIS
153
[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 real-time
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.
[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.
154
LITERATURVERZEICHNIS
[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.
[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.
LITERATURVERZEICHNIS
155
[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.
Herunterladen