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