pdf - 1572k
Transcription
pdf - 1572k
Einführung in die Informatik Benjamin Gufler Erstellt mit LATEX II Inhaltsverzeichnis I 1 1 Information, ihre Repräsentation und Verarbeitung 1.1 Information in der Informatik . . . . . . . . . . . . . . . . . . . . . . 1.2 Modellbildung in der Informatik . . . . . . . . . . . . . . . . . . . . 1.3 Informationsrepräsentationssysteme . . . . . . . . . . . . . . . . . . 1.4 Aussagen als Beispiel für Informationen . . . . . . . . . . . . . . . . 1.4.1 Boolesche Terme . . . . . . . . . . . . . . . . . . . . . . . . . 1.4.2 Boolesche Algebra der Wahrheitswerte . . . . . . . . . . . . . 1.4.3 Interpretation boolescher Terme . . . . . . . . . . . . . . . . 1.4.4 Gesetze der booleschen Algebra . . . . . . . . . . . . . . . . . 1.4.5 Anwendung der Gesetze der booleschen Algebra . . . . . . . 1.5 Information und Repräsentation: Normalform . . . . . . . . . . . . . 1.5.1 Umformung von Repräsentation als Informationsverarbeitung 1.5.2 Zeichenfolgen: Wörter über einem Alphabet . . . . . . . . . . 3 3 3 4 5 5 6 7 8 9 10 10 10 2 Algorithmen und Rechenstrukturen 2.1 Der Begriff Algorithmus . . . . . . . . . . . . . . . . . . . . . 2.1.1 Textersetzungsalgorithmen . . . . . . . . . . . . . . . 2.1.2 Deterministische Textersetzung . . . . . . . . . . . . . 2.1.3 Durch Textersetzungssysteme induzierte Abbildungen 2.2 Rechenstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.1 Funktionen und Mengen als Rechenstrukturen . . . . 2.2.2 Signaturen . . . . . . . . . . . . . . . . . . . . . . . . 2.2.3 Grundterme . . . . . . . . . . . . . . . . . . . . . . . . 2.2.4 Terme mit Identifikatoren . . . . . . . . . . . . . . . . 2.3 Algorithmen als Termersetzungssysteme . . . . . . . . . . . . 2.3.1 Termersetzung . . . . . . . . . . . . . . . . . . . . . . 2.3.2 Termersetzungssysteme . . . . . . . . . . . . . . . . . 2.3.3 Korrektheit von TES . . . . . . . . . . . . . . . . . . . 2.4 Aussagenlogik und Prädikatenlogik . . . . . . . . . . . . . . . 2.4.1 Aussagenlogik . . . . . . . . . . . . . . . . . . . . . . . 2.4.2 Prädikatenlogik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 13 14 15 16 16 16 18 19 20 22 22 22 23 24 25 26 3 Programmiersprachen 3.1 Syntax - BNF . . . . . . . . . . . . . . . . . 3.1.1 BNF . . . . . . . . . . . . . . . . . . 3.1.2 Syntaxdiagramme . . . . . . . . . . 3.1.3 Kontextbedingungen . . . . . . . . . 3.2 Semantik . . . . . . . . . . . . . . . . . . . 3.3 Implementierung von Programmiersprachen 3.4 Methodik der Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 29 30 32 32 32 33 33 III . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . IV INHALTSVERZEICHNIS 4 Applikative / funktionale Sprachen 4.1 Rein applikative Sprachen . . . . . . . . . . . . . . . . . . . . . . . . 4.1.1 Syntax von Ausdrücken . . . . . . . . . . . . . . . . . . . . . 4.1.2 Bedeutung von Ausdrücken . . . . . . . . . . . . . . . . . . . 4.1.3 Konstanten und Identifikatoren . . . . . . . . . . . . . . . . . 4.1.4 Bedingte Ausdrücke . . . . . . . . . . . . . . . . . . . . . . . 4.1.5 Funktionsapplikation . . . . . . . . . . . . . . . . . . . . . . . 4.1.6 Funktionsabstraktion . . . . . . . . . . . . . . . . . . . . . . . 4.2 Deklarationen in applikativen / funktionalen Sprachen . . . . . . . . 4.2.1 Elementdeklaration . . . . . . . . . . . . . . . . . . . . . . . . 4.2.2 Funktionsdeklaration . . . . . . . . . . . . . . . . . . . . . . . 4.3 Rekursive Funktionsdeklarationen . . . . . . . . . . . . . . . . . . . . 4.3.1 Induktive Deutung rekursiver Funktionsdeklarationen . . . . 4.3.2 Deutung rekursiv deklarierter Funktionen durch kleinste Fixpunkte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.4 Rekursionsformen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.4.1 Lineare Rekursion . . . . . . . . . . . . . . . . . . . . . . . . 4.4.2 Repetitive Rekursion . . . . . . . . . . . . . . . . . . . . . . . 4.4.3 Kaskadenartige Rekursion . . . . . . . . . . . . . . . . . . . . 4.4.4 Vernestete Rekursion . . . . . . . . . . . . . . . . . . . . . . . 4.4.5 Verschränkte Rekursion . . . . . . . . . . . . . . . . . . . . . 4.5 Techniken applikativer Programmierung . . . . . . . . . . . . . . . . 4.5.1 Spezifikation der Aufgabenstellung . . . . . . . . . . . . . . . 4.5.2 Strukturierung und Entwurf . . . . . . . . . . . . . . . . . . . 4.5.3 Hinweise zur Strukturierung der Aufgabenstellung und der Lösung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.5.4 Rekursion und Spezifikation . . . . . . . . . . . . . . . . . . . 4.5.5 Parameterunterdrückung . . . . . . . . . . . . . . . . . . . . 4.5.6 Effizienz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.5.7 Dokumentation . . . . . . . . . . . . . . . . . . . . . . . . . . 4.5.8 Test / Integration von Programmen . . . . . . . . . . . . . . 4.6 Korrektheitsbeweise . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.6.1 Induktion und Rekursion . . . . . . . . . . . . . . . . . . . . 4.6.2 Terminierungsbeweise . . . . . . . . . . . . . . . . . . . . . . 35 35 35 36 38 38 40 40 43 43 44 44 47 5 Zuweisungsorientierte Programmierung 5.1 Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . 5.1.1 Syntax . . . . . . . . . . . . . . . . . . . . . . . . . 5.1.2 Programmvariable und Zuweisungen . . . . . . . . 5.1.3 Zustände . . . . . . . . . . . . . . . . . . . . . . . 5.1.4 Funktionale Bedeutung von Anweisungen . . . . . 5.1.5 Operationelle Bedeutung von Anweisungen . . . . 5.2 Einfache Anweisungen . . . . . . . . . . . . . . . . . . . . 5.3 Zusammengesetzte Anweisungen . . . . . . . . . . . . . . 5.3.1 Sequentielle Komposition . . . . . . . . . . . . . . 5.3.2 Bedingte Anweisung . . . . . . . . . . . . . . . . . 5.3.3 Wiederholungsanweisung . . . . . . . . . . . . . . 5.4 Variablendeklarationen und Blöcke . . . . . . . . . . . . . 5.5 Prozeduren . . . . . . . . . . . . . . . . . . . . . . . . . . 5.5.1 Prozedurdeklaration . . . . . . . . . . . . . . . . . 5.5.2 Prozeduraufruf . . . . . . . . . . . . . . . . . . . . 5.5.3 Globale Programmvariable in Prozeduren . . . . . 5.5.4 Rekursion . . . . . . . . . . . . . . . . . . . . . . . 5.6 Block und Abschnitt, Bindung: Gültigkeit / Lebensdauer 63 63 63 64 64 64 64 65 65 65 66 67 68 69 69 70 71 71 72 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 49 49 50 50 51 52 52 52 54 55 56 56 56 58 58 58 59 60 INHALTSVERZEICHNIS 5.7 V Programmtechniken für Zuweisungen . . . . . . . . . . . . . . . . . . 73 6 Sortendeklarationen 6.1 Deklaration von Sorten . . . . . . . . . 6.1.1 Skalare durch Enumeration . . 6.1.2 Direktes Produkt / Tupelsorten 6.1.3 Direkte Summe / Varianten . . 6.1.4 Teilbereichsorten . . . . . . . . 6.2 Felder . . . . . . . . . . . . . . . . . . 6.2.1 Einstufige Felder . . . . . . . . 6.2.2 Felder und selektives Ändern . 6.2.3 Mehrstufige Felder . . . . . . . 6.3 Endliche Mengen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 77 77 78 79 80 80 81 81 82 83 7 Sprünge und Referenzen 7.1 Kontrollfluss . . . . . . . . . . 7.1.1 Marken und Sprünge . . 7.1.2 Kontrollflussdiagramme 7.2 Referenzen und Zeiger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 85 85 86 86 8 Rekursive Sorten 8.1 Sequenzartige Rechenstrukturen . . . . . . . . . . . . . . . . 8.1.1 Rechenstruktur der Sequenzen . . . . . . . . . . . . . 8.1.2 Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . 8.1.3 Warteschlangen (Queue) . . . . . . . . . . . . . . . . . 8.1.4 Dateien . . . . . . . . . . . . . . . . . . . . . . . . . . 8.2 Baumartige Rechenstrukturen . . . . . . . . . . . . . . . . . . 8.3 Rekursive Sortenvereinbarungen . . . . . . . . . . . . . . . . . 8.3.1 Verwendung von Rekursion für Sortenvereinbarungen 8.4 Geflechte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8.4.1 Einfache Geflechte . . . . . . . . . . . . . . . . . . . . 8.4.2 Geflechte über rekursiv vereinbarten Sorten . . . . . . 8.4.3 Verkettete Listen . . . . . . . . . . . . . . . . . . . . . 8.4.4 Zweifach verkettete Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 89 89 89 91 91 91 95 96 96 97 97 98 98 9 Objektorientiertes Programmieren 9.1 Klassen und Objekte . . . . . . . . . . . . . . 9.1.1 Klassen . . . . . . . . . . . . . . . . . 9.1.2 Objekte . . . . . . . . . . . . . . . . . 9.2 Vererbung . . . . . . . . . . . . . . . . . . . . 9.2.1 Vererbungsbeziehung . . . . . . . . . . 9.2.2 Bemerkungen zur Objektorientierung . . . . . . . . . . . . . . . . . . . . . . . . . 101 101 102 103 104 104 106 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . II Rechnerstruktur, Hardware, Maschinennahe Programmierung 107 1 Codierung / Informationstheorie 1.1 Codes / Codierung . . . . . . . . . . . 1.1.1 Binärcodes einheitlicher Länge 1.1.2 Codes variabler Länge . . . . . 1.1.3 Serien-/ Parallelwortcodierung 1.1.4 Codebäume . . . . . . . . . . . 1.2 Codes und Entscheidungsinformation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111 111 111 112 113 113 114 VI INHALTSVERZEICHNIS 1.3 Sicherung von Nachrichtenübertragung . . . . . . . . . . . . . . . . . 115 1.3.1 Codesicherung . . . . . . . . . . . . . . . . . . . . . . . . . . 115 1.3.2 Übertragungssicherheit . . . . . . . . . . . . . . . . . . . . . . 116 2 Binäre Schaltnetze und Schaltwerke 2.1 Boolesche Algebra / Boolesche Funktionen . . . . . . . . 2.1.1 Boolesche Funktionen . . . . . . . . . . . . . . . 2.1.2 Partielle Ordnung auf BF . . . . . . . . . . . . . 2.2 Normalformen boolescher Funktionen . . . . . . . . . . . 2.2.1 Das boolesche Normalformtheorem . . . . . . . . 2.2.2 Vereinfachte Normalformen (DNF) . . . . . . . . 2.3 Schaltnetze . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.1 Schaltfunktionen und Schaltnetze . . . . . . . . . 2.3.2 Darstellung von Schaltnetzen . . . . . . . . . . . 2.3.3 Halbaddierer . . . . . . . . . . . . . . . . . . . . 2.3.4 Arithmetische Schaltnetze . . . . . . . . . . . . . 2.3.5 Zahldarstellung . . . . . . . . . . . . . . . . . . . 2.3.6 Weitere arithmetische Operationen . . . . . . . . 2.3.7 Schaltnetze zur Übertragung von Information . . 2.4 Schaltwerke . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.1 Schaltwerksfunktionen . . . . . . . . . . . . . . . 2.4.2 Schaltfunktionen als Schaltwerksfunktionen . . . 2.4.3 Schaltwerke . . . . . . . . . . . . . . . . . . . . . 2.4.4 Schaltwerksfunktionen und endliche Automaten . 2.4.5 Schaltwerke zum Speichern: Verzögerungsnetze . 2.4.6 Klassen von Schaltwerken . . . . . . . . . . . . . 2.4.7 Komposition von Schaltwerken und Schaltnetzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 117 117 119 119 119 120 121 121 122 124 126 127 130 130 131 131 132 132 132 133 134 135 3 Aufbau von Rechenanlagen 3.1 Strukturierter Aufbau von Rechenanlagen . . . . . . 3.1.1 Der Rechnerkern . . . . . . . . . . . . . . . . 3.1.2 Speichereinheit . . . . . . . . . . . . . . . . . 3.1.3 E/A . . . . . . . . . . . . . . . . . . . . . . . 3.1.4 Befehle und Daten auf Maschinenebene . . . 3.1.5 Operandenspezifikation und Adressrechnung . 3.1.6 Der Befehlszyklus . . . . . . . . . . . . . . . 3.2 Hardwarekomponenten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137 137 139 139 139 139 141 143 144 4 Maschinennahe Programmierung 4.1 Maschinennahe Programmiersprachen . . . . . . . . . 4.1.1 Binärwörter als Befehle . . . . . . . . . . . . . 4.1.2 Der Befehlsvorrat der MI . . . . . . . . . . . . 4.1.3 Einfache Maschinenprogramme . . . . . . . . . 4.1.4 Assemblersprachen . . . . . . . . . . . . . . . . 4.1.5 Ein- und Mehradressform . . . . . . . . . . . . 4.1.6 Unterprogrammtechniken . . . . . . . . . . . . 4.2 Adressiertechniken und Speicherverwaltung . . . . . . 4.2.1 Konstante . . . . . . . . . . . . . . . . . . . . . 4.2.2 Operandenversorgung über Register . . . . . . 4.2.3 Absolute Adressierung . . . . . . . . . . . . . . 4.2.4 Relative Adressierung . . . . . . . . . . . . . . 4.2.5 Indizierung, Zugriff auf Felder . . . . . . . . . . 4.2.6 Symbolische Adressierung . . . . . . . . . . . . 4.2.7 Geflechtsstrukturen und indirekte Adressierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145 145 146 146 147 149 150 151 153 153 154 154 154 154 155 156 INHALTSVERZEICHNIS 4.3 III 4.2.8 Speicherverwaltung . . . . . . . . . . . . . . . . . . 4.2.9 Stackverwaltung von blockstrukturierten Sprachen Techniken maschinennaher Programmierung . . . . . . . . 4.3.1 Auswertung von Ausdrücken / Termen . . . . . . . 4.3.2 Maschinennahe Realisierung von Ablaufstrukturen VII . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Systeme und systemnahe Programmierung 158 158 160 161 162 165 1 Prozesse, Interaktion, Koordination in verteilten Systemen 1.1 Prozesse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.1.1 Aktionsstrukturen als Prozesse . . . . . . . . . . . . . . . . 1.1.2 Strukturierung von Prozessen . . . . . . . . . . . . . . . . . 1.1.3 Sequentielle Prozesse und Spuren . . . . . . . . . . . . . . . 1.1.4 Zerlegen von Prozessen in Teilprozesse . . . . . . . . . . . . 1.1.5 Aktionen als Zustandsübergänge . . . . . . . . . . . . . . . 1.2 Systembeschreibungen durch Mengen von Prozessen . . . . . . . . 1.2.1 Petri-Netze . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2.2 Prozessalgebra . . . . . . . . . . . . . . . . . . . . . . . . . 1.2.3 Synchronisation und Koordination von Agenten . . . . . . . 1.2.4 Beschreibung von Prozessen mit Prädikaten . . . . . . . . . 1.3 Programmiersprachen zur Beschreibung. . . . . . . . . . . . . . . . . 1.3.1 Parallelität in anweisungsorientierten Programmiersprachen 1.3.2 Kommunikation durch Nachrichtenaustausch . . . . . . . . 1.3.3 Nachrichtenaustausch über gemeinsame Variablen . . . . . 1.3.4 Sprachmittel zum Erzeugen paralleler Abläufe . . . . . . . . 1.3.5 Nebenläufigkeit in Java . . . . . . . . . . . . . . . . . . . . Thread-Erzeugung in Java . . . . . . . . . . . . . . . . . . . Synchronisation in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169 169 169 171 172 173 174 176 177 181 183 184 186 186 188 193 198 201 202 202 2 Betriebssysteme und systemnahe Programmierung 2.1 Grundlegende Betriebssystem – Aspekte . . . . . . . . . . 2.1.1 Aufgaben eines Betriebssystems . . . . . . . . . . . 2.1.2 Betriebsarten . . . . . . . . . . . . . . . . . . . . . 2.1.3 Ein einfaches Betriebssystem für den Stapelbetrieb 2.1.4 Ein einfaches Betriebssystem für Multiplexbetrieb 2.2 Benutzerrelevante Aspekte eines Betriebssystems . . . . . 2.2.1 Kommandosprache des Betriebssystems . . . . . . 2.2.2 Benutzerverwaltung . . . . . . . . . . . . . . . . . 2.2.3 Zugriff auf Rechenleistung . . . . . . . . . . . . . . 2.2.4 Dateiorganisation und Verwaltung . . . . . . . . . 2.2.5 Übertragungsdienste . . . . . . . . . . . . . . . . . Das Client – Server – Modell . . . . . . . . . . . . Middleware . . . . . . . . . . . . . . . . . . . . . . 2.2.6 Zuverlässigkeit und Schutzaspekte . . . . . . . . . 2.3 Betriebsmittelverteilung . . . . . . . . . . . . . . . . . . . 2.3.1 Prozessorvergabe . . . . . . . . . . . . . . . . . . . 2.3.2 Hauptspeicherverwaltung . . . . . . . . . . . . . . 2.3.3 Betriebsmittelvergabe im Mehrprogrammbetrieb . 2.3.4 Zuteilung der E/A – Geräte . . . . . . . . . . . . . 2.4 Techniken der Systemprogrammierung . . . . . . . . . . . 2.4.1 Unterbrechungskonzept . . . . . . . . . . . . . . . 2.4.2 Koordination und Synchronisation . . . . . . . . . 2.4.3 Segmentierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205 206 206 209 210 212 214 215 215 215 215 217 218 218 220 220 220 221 222 222 223 223 225 227 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . VIII 2.5 INHALTSVERZEICHNIS 2.4.4 Seitenaustauschverfahren . . . . . . . . . . . . . 2.4.5 Verschiebbarkeit von Programmen . . . . . . . . 2.4.6 Simultane Benutzbarkeit von Unterprogrammen 2.4.7 Steuerung von E/A – Geräten . . . . . . . . . . . 2.4.8 Kommunikationsdienste . . . . . . . . . . . . . . Betriebssystem – Strukturen . . . . . . . . . . . . . . . . 3 Interpretation und Übersetzung 3.1 Lexikalische Analyse . . . . . . 3.2 Zerteilen von Programmen . . . 3.3 Kontextbedingungen . . . . . . 3.4 Semantische Behandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228 228 229 230 232 235 von Programmiersprachen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239 241 242 243 243 IV 245 1 Formale Sprachen 1.1 Relation, Graphen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.1.1 Zweistellige Relationen . . . . . . . . . . . . . . . . . . . . . . 1.1.2 Wege in Graphen, Hüllenbildung . . . . . . . . . . . . . . . . 1.2 Grammatiken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2.1 Reduktive und generative Grammatiken . . . . . . . . . . . . 1.2.2 Die Sprachhierarchie nach Chomsky . . . . . . . . . . . . . . 1.2.3 Strukturelle Äquivalenz von Ableitungen . . . . . . . . . . . . 1.2.4 Sackgassen, unendliche Ableitungen . . . . . . . . . . . . . . 1.3 Chomsky – 3 – Sprachen, endliche Automaten, reguläre Ausdrücke . 1.3.1 Reguläre Ausdrücke . . . . . . . . . . . . . . . . . . . . . . . 1.3.2 Endliche Automaten . . . . . . . . . . . . . . . . . . . . . . . 1.3.3 Äquivalenz der Darstellungsformen . . . . . . . . . . . . . . . 1.3.4 Äquivalenz von regulären Ausdrücken, endlichen Automaten und Chomsky – 3 – Grammatiken . . . . . . . . . . . . . . . 1.3.5 Minimale Automaten . . . . . . . . . . . . . . . . . . . . . . 1.4 Kontextfreie Sprachen und Kellerautomaten . . . . . . . . . . . . . . 1.4.1 BNF – Notation . . . . . . . . . . . . . . . . . . . . . . . . . 1.4.2 Kellerautomaten . . . . . . . . . . . . . . . . . . . . . . . . . 1.4.3 Äquivalenz von Kellerautomaten und kontextfreien Sprachen 1.4.4 Greibach – Normalform . . . . . . . . . . . . . . . . . . . . . 1.4.5 LR(k) – Sprachen . . . . . . . . . . . . . . . . . . . . . . . . 1.4.6 LL(k) – Grammatiken . . . . . . . . . . . . . . . . . . . . . . 1.4.7 Rekursiver Abstieg . . . . . . . . . . . . . . . . . . . . . . . . 1.4.8 Das Pumping – Lemma für kontextfreie Sprachen . . . . . . . 1.5 Kontextsensitive Grammatiken . . . . . . . . . . . . . . . . . . . . . 249 249 249 251 252 253 254 255 256 258 258 259 260 2 Berechenbarkeit 2.1 Hypothetische Maschinen . . . . . . . . . . . . . . . . . . . . . . . . 2.1.1 Turing – Maschinen . . . . . . . . . . . . . . . . . . . . . . . 2.1.2 Registermaschinen . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Rekursive Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.1 Primitiv rekursive Funktionen . . . . . . . . . . . . . . . . . . 2.2.2 µ – rekursive Funktionen . . . . . . . . . . . . . . . . . . . . 2.2.3 Allgemeine Bemerkungen zur Rekursion . . . . . . . . . . . . 2.3 Äquivalenz der Berechenbarkeitsbegriffe . . . . . . . . . . . . . . . . 2.3.1 Äquivalenz von µ – Berechenbarkeit und Turing – Berechenbarkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275 275 276 279 280 280 282 284 284 260 265 266 266 267 268 269 270 272 272 273 274 284 INHALTSVERZEICHNIS IX 2.3.2 2.4 Äquivalenz von Registermaschinen- und Turing – Berechenbarkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.3 Churchs These . . . . . . . . . . . . . . . . . . . . . . . . . . Entscheidbarkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.1 Nicht berechenbare Funktionen . . . . . . . . . . . . . . . . . 2.4.2 (Nicht) entscheidbare Prädikate . . . . . . . . . . . . . . . . . 2.4.3 Rekursion und rekursiv aufzählbare Mengen . . . . . . . . . . 286 286 286 286 287 288 3 Komplexitätstheorie 3.1 Komplexitätsmaße . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.1 Zeitkomplexität . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.2 Bandkomplexität . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.3 Zeit- und Bandkomplexitätsklassen . . . . . . . . . . . . . . . 3.1.4 Polynomiale und nichtdeterministisch polynomiale Zeitkomplexität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.5 Nichtdeterminismus in Algorithmen — Backtracking . . . . . 3.2 N P – Vollständigkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.1 Das Erfüllungsproblem . . . . . . . . . . . . . . . . . . . . . . 3.2.2 N P – vollständige Probleme . . . . . . . . . . . . . . . . . . 3.3 Effiziente Algorithmen für N P – vollständige Probleme . . . . . . . 3.3.1 Geschicktes Durchlaufen von Baumstrukturen . . . . . . . . . 3.3.2 Alpha / Beta – Suche . . . . . . . . . . . . . . . . . . . . . . 3.3.3 Dynamisches Programmieren . . . . . . . . . . . . . . . . . . 3.3.4 Greedy – Algorithmen . . . . . . . . . . . . . . . . . . . . . . 291 291 291 292 293 4 Effiziente Algorithmen und Datenstrukturen 4.1 Diskussion ausgewählter Algorithmen . . . . . 4.1.1 Komplexität von Sortierverfahren . . . . 4.1.2 Wege in Graphen . . . . . . . . . . . . . 4.2 Bäume . . . . . . . . . . . . . . . . . . . . . . . 4.2.1 Geordnete, orientierte, sortierte Bäume 4.2.2 Darstellung von Bäumen durch Felder . 4.2.3 AVL – Bäume . . . . . . . . . . . . . . 4.2.4 B – Bäume . . . . . . . . . . . . . . . . 4.3 Effiziente Speicherung großer Datenmengen . . 4.3.1 Rechenstruktur der Mengen (mit Zugriff 4.3.2 Mengendarstellung durch AVL – Bäume 4.3.3 Streuspeicherverfahren . . . . . . . . . . 307 307 307 308 309 309 309 310 310 310 311 311 314 . . . . . . . . . . . . . . . . . . . . . . . . . . . über . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Schlüssel) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 294 295 297 297 298 299 300 301 303 304 X INHALTSVERZEICHNIS Teil I 1 Kapitel 1 Information, ihre Repräsentation und Verarbeitung Informatik: Wissenschaft, Technik und Anwendung der Modellierung, maschinellen Verarbeitung, Speicherung, Übertragung und Darstellung von Information und informationsverarbeitenden Prozessen. Hardware: Alle Geräte der Informationsverarbeitung und Kommunikationstechnik. Software: System von Programmen und Daten zur Bearbeitung einer bestimmten Fragestellung oder Aufgabe. Programm: In einer formalen Sprache abgefasste Verarbeitungsvorschrift. Wir unterscheiden im Umgang mit Information: • Repräsentation (äußere Form der Darstellung) • abstrakte Deutung: Information • Bezug zur Realität • Richtigkeit 1.1 Information in der Informatik Zentrale Fragen: • schematische Darstellung von Information (Informationsrepräsentationssysteme), Daten- und Objektstrukturen sowie deren Bezüge untereinander • Regeln und Vorschriften zur Verarbeitung von Informationen in der Gestalt von Daten und Repräsentationen 1.2 Modellbildung in der Informatik Modell: Vereinfachte Abbildung eines Ausschnitts der Wirklichkeit mit Mitteln der Informatik Modellbildung: Wahl eines Modells 3 4 KAPITEL 1. INFORMATION, IHRE REPRÄSENTATION . . . Modellierungsgegenstände: • Aspekte des Anwendungsgebietes • Strukturen der Informatik: Daten, Programme, Prozesse, Hardware, etc. Einsatzgebiete: • Eingebettete Hardware / Software - Systeme zur Steuerung von technischen oder physischen Prozessen unter Zeitabsonderung (Fly-by-wire in Flugzeugen, in Fahrzeugen, Airbag, Stabilitätskontrolle, Motormanagement, Handys, Waschmaschine, Unterhaltungselektronik etc.) • Rechensysteme mit Nutzerschnittstelen (PC, WS, PDA) für Informationsverarbeitungsaufgaben (Textverarbeitung) • Vernetzte Rechner (Rechnernetz, Internet, Telekommunikation, . . . ) zur Lösung von Kommunikations- und Datenübertragungsaufgaben 1.3 Informationsrepräsentationssysteme R . . . Menge von Repräsentationen A . . . Menge von Abstraktionen (Informationen) I: R → A . . . Interpretation(sabbildung) (A, R, I) . . . Informationsrepreäsentationssystem Beispiel: Zahlen N . . . Menge der natürlichen Zahlen (einschließlich 0) Repräsentationsmengen für natürliche Zahlen: • Dezimalschreibweise • Binärschreibweise • Römische Zahlen • Strichzahlen: SZ={ε, I, II, III, . . .} • Hexadezimal • Oktal • BCD-codiert • Exponentialdarstellung • Primzahldarstellung • ... I: SZ → N (N, SZ, I) Informationsrepräsentationssystem Oft kann eine Information durch unterschiedliche Repräsentationen dargestellt werden. Für r1 , r2 ∈ R sagen wir: r1 und r2 sind bedeutungsgleich oder semantisch äquivalent, wenn I[r1 ] = I[r2 ] ist. 1.4. AUSSAGEN ALS BEISPIEL FÜR INFORMATIONEN 1.4 5 Aussagen als Beispiel für Informationen Beispiele für Aussagen: • Es ist kalt. • Es regnet. • ... Für jede elementare Aussage ist es - für ein vorgegebenes Bezugssystem - möglich, ihr den Wert wahr oder falsch zuordnen. Beispiel: Modellierung durch elementare Aussagen - Airbag C = Crashsensor spricht an A = Airbag löst aus Z = Zündung ist ein Zusammengesetzte Aussage: (C und Z) dann A Charakterisierung von Aussagen (Was ist eine Aussage?) - Aristoteles, 384 - 322 v. Chr.: Eine Aussage ist ein sprachliches Gebilde, von dem es sinnvoll ist zu sagen, es sei wahr oder falsch. Beispiel für eine paradoxe Aussage: Die Aussage dieses Satzes ist falsch. Dieser Satz ist reflexiv und widersprüchlich. Wie solche Widersprüche vermeiden? Durch Abkehr von der natürlichen Sprache und Verwendung eines präzise festgelegten Repräsentationssystems für Aussagen. 1.4.1 Boolesche Terme Atomare Aussagen: Menge vorgegebener Aussagen, die nicht weiter in Aussagen zerlegt werden können Zusammengesetzte Aussagen: Aus Unteraussagen zusammengesetzte Aussagen Aussagen können durch aussagenlogische Operationen (Verknüpfungen) zusammengesetzt werden. 6 KAPITEL 1. INFORMATION, IHRE REPRÄSENTATION . . . Definition: Boolesche Terme gegeben: E . . . Menge von elementaren, atomaren Aussagen (Analogie: Zahlen) ID . . . Menge von Identifikatoren (insbesondere ist x, y, z ∈ ID) (Analogie: Variablen, Unbekannte) Die Menge der booleschen Terme ist wie folgt festgelegt: (1) true, false sind boolesche Terme. (2) Alle atomaren, elementaren Aussagen in E und alle Identifikatoren sind boolesche Terme. (3) Ist t ein boolescher Term, so ist (¬t), gesprochen Term. nicht t, ein boolescher (4) Sind t1 und t2 boolesche Terme, so sind folgende Gebilde auch boolesche Terme: • (t1 ∧ t2 ) und • (t1 ∨ t2 ) oder • (t1 ⇒ t2 ) impliziert • (t1 ⇔ t2 ) äquivalent Nach (3) und (4) bilden wir zusammengesetzte boolesche Terme. ¬, ∧, ∨, ⇒, ⇔ heißen boolesche (oder aussagenlogische) Operatoren / Verknüpfungen. Ein boolescher Term heißt elementar, wenn er keine Identifikatoren enthält. Abkürzungen (jeweils semantisch äquivalent): • (t1 ∧ t2 ) steht für (¬((¬t1 ) ∨ (¬t2 ))) • (t1 ⇒ t2 ) steht für ((¬t1 ) ∨ t2 ) • (t1 ⇔ t2 ) steht für ((t1 ∧ t2 ) ∨ ((¬t1 ) ∧ (¬t2 ))) Wir sagen für x ∈ ID: x kommt im booleschen Term t frei vor, falls der Identifikator x im Term t auftritt. 1.4.2 Boolesche Algebra der Wahrheitswerte Die Menge der Wahrheitswerte hat genau zwei Elemente. Es gibt viele Möglichkeiten, die Menge der Wahrheitswerte darzustellen: • {wahr, f alsch} • {true, f alse} • {L, 0} • {1, 0} • {>, ⊥} • {{∅} , ∅} Festlegung: B = {L, 0} Menge der Wahrheitswerte. Abbildungen auf Wahrheitswerten: 1.4. AUSSAGEN ALS BEISPIEL FÜR INFORMATIONEN 7 • not: B → B b not(b) L 0 0 L • id: B → B b id(b) L L 0 0 • or: B × B → B a b a and b 0 0 0 0 L L L 0 L L L L • and: a 0 0 L L B×B→B b a and b 0 0 L 0 0 0 L L • impl: a 0 0 L L B×B→B b a and b 0 L L L 0 0 L L • equiv: B × B → B a b a and b 0 0 L 0 L 0 L 0 0 L L L Die Festlegung einer Menge und von Abbildungen führt auf eine Algebra. impl(or(and(L, 0), L), not(0)) = L 1.4.3 Interpretation boolescher Terme Für elementare boolesche Terme definieren wir eine Interpretationsfunktion, die jedem elementaren booleschen Term einen Wahrheitswert zuordnet: • I[true] = L • I[f alse] = 0 • I[(¬t)] = not(I[t]) • I[(t1 ∨ t2 )] = or(I[t1 , I[t2 ) Wir belegen die freien Identifikatoren mit Wahrheitswerten durch Abbildung: β:D→B β heißt Belegung von D. Sei x ∈ D. 8 KAPITEL 1. INFORMATION, IHRE REPRÄSENTATION . . . • Iβ [x] = β(x) • Iβ [(t1 ∧ t2 )] = and(Iβ [t1 ], Ibeta [t2 ]) Mit WBool bezeichnen wir die Menge der elementaren booleschen Terme (d.h. die Terme ohne freie Identifikatoren), mit WBool (ID) die Menge der booleschen Terme mit Identifikatoren aus ID. I : WBool (ID) → (EN V → B) Sei dabei EN V die Menge der Belegungen von Identifikatoren aus ID. I[t](β) liefert Wahrheitswerte gegeben durch Iβ [t]. (B, WBool , Iβ und (EN V → B, WBool , I) sind Informationsrepräsentationssysteme. Damit ist auf booleschen Termen eine Relation semantisch äquivalent definiert. Beispiel: semantische Äquivalenz (f alse ⇒ x) true Sind diese Terme semantisch äquivalent? Ja, weil Iβ [f alse ⇒ x] = Iβ [true]∀β ∈ EN V . Beweis durch Tabelle: β(x) Iβ [f alse ⇒ x] Iβ [true] L L L 0 L L Sind (¬(x ∧ y)) und ((¬x) ∨ (¬y)) äquivalent? x y Iβ [(¬(x ∧ y))] Iβ [((¬x) ∨ (¬y))] L L 0 0 L 0 L L 0 L L L 0 0 L L 1.4.4 Gesetze der booleschen Algebra (i) Involutionsgesetz (ii) Kommutativgesetz (iii) Assoziativgesetz ¬¬x = x ( x∧y =y∧x x∨y =y∨x ( (x ∧ y) ∧ z = x ∧ (y ∧ z) (x ∨ y) ∨ z = x ∨ (y ∨ z) (iv) Idempotenzgesetz ( x∧x=x x∨x=x (v) Absorptionsgesetz ( x ∧ (x ∨ z) = x x ∨ (x ∧ z) = x (vi) Distributivitätsgesetz (vii) Gesetz von de Morgan ( x ∧ (y ∨ z) = (x ∧ y) ∨ (x ∧ z) x ∨ (y ∧ z) = (x ∨ y) ∧ (y ∨ z) ( ¬(x ∧ y) = (¬x) ∨ (¬y) ¬(x ∨ y) = (¬x) ∧ (¬y) 1.4. AUSSAGEN ALS BEISPIEL FÜR INFORMATIONEN (viii) Neutralitätsgesetz 9 ( x ∨ (y ∧ ¬y) = x x ∧ (y ∨ ¬y) = x (x ⇒ y) = (¬x) ∨ y Theorem: Die booleschen Terme auf der rechten und linken Seite der Gleichungen der Gesetze der booleschen Algebra sind jeweils semantisch äquivalent. Beweis: durch Tabellentechnik 1.4.5 Anwendung der Gesetze der booleschen Algebra Die Gleichheit • t=t = erfüllt folgende Gesetze einer Äquivalenzrelation: Reflexivität • t1 = t2 , dann gilt auch t2 = t1 Symmetrie • gilt t1 = t2 und t2 = t3 , dann gilt auch t1 = t3 Transitivität Definition: Substitution Die Substitution ist eine Operation auf Termen. Sie entspricht einer Abbildung: WBool (ID) × WBool (ID) × ID → WBool (ID) t[t0 /x] Substitution von t0 für x in t Beispiel: Substitution ((x ∧ x) ∨ ¬c)[(a ∨ b)/x] = (((a ∨ b) ∧ (a ∨ b)) ∨ ¬c) Ergebnis ist der Term, der aus t entsteht, indem jedes Vorkommen von x in t durch t0 ersetzt wird. Verallgemeinerung: simultane Substitution t[t1 /x1 , t2 /x2 , . . . , tn /xn ] Beispiel: Simultane und sequentielle Substitution (x ∧ y)[(y ∧ z)/x, z/y] = ((y ∧ z) ∧ z) ((x ∧ y)[(y ∧ z)/x])[z/y] = (z ∧ z ∧ z) simultan sequentiell Theroem: Verträglichkeit der Substitution mit der semantischen Äquivalenz: Falls (für x ∈ ID, t0 ∈ WBool (ID)) gilt β(x) = Iβ [t0 ], dann git (für t ∈ WBool (ID): Iβ [t] = Iβ [t[t0 /x]]. Durch die Substitution können wir genau definieren, was es heißt, ein Gesetz t1 = t2 anzuwenden: Instanz bilden (Spezialfall): t1 [t01 /x1 , . . . , t0n /xn ] = t2 [t01 /x1 , . . . , t0n /xn ] Will ich das Gesetz auf den Term t anwenden, so markiere ich die Anwendungsstelle: Ich suche Term t0 , t00 ∈ WBool (ID) und x ∈ ID, so dass gilt: t = t0 [t00 /x]. Wenn t00 genau mit t1 [[t01 /x1 , . . . , t0n /xn ] übereinstimmt, dann kann ich t00 durch t2 [t01 /x1 , . . . , t0n /xn ] ersetzen, d. h. t = t0 [t1 [t01 /x1 , . . . , t0n /xn ]/x] = t0 [t2 [t01 /x1 , . . . , t0n /xn ]/x] Die Anwendungsstelle des Gesetzes wird auch als Redex bezeichnet. 10 KAPITEL 1. INFORMATION, IHRE REPRÄSENTATION . . . 1.5 Information und Repräsentation: Normalform Da wir konkret auf Informationen (Semantik, Bedeutung) stats auf Repräsentationen (Syntax) arbeiten müssen, ist es sinnvoll, einen engen Bezug zwischen den beiden herzustellen. 1.5.1 Umformung von Repräsentation als Informationsverarbeitung Sei (A, R, I) ein Informationsrepräsentationssystem. Jede Abbildung ϕ0 : A → A auf Informationen muss konkret durch eine Abbildung ϕ : R → R dargestellt werden. Das erfordert folgende Beziehung: ϕ(r) = r0 ⇒ ϕ0 (I[r]) = I[r0 ]. Dies liefert das folgende kommutierende Diagramm: ϕ A −−−−→ x I A x I R −−−−→ R f Äquivalenztransformation: Gilt ∀r ∈ R : I[r] = I[ϕ(r)], so heißt ϕ Äquivalenzrelation, d.h. ϕ ist Identität auf dem Bild von I. Normalform N ⊆ R ausgezeichnete Teilmenge der Repräsentation. N heißt eindeutig, wenn ∀r, r0 ∈ N : I[r] = I[r0 ] ⇒ r = r0 . N heißt vollständig ⇔ ∀a ∈ A : ∃r ∈ N : I[r] = a. 1.5.2 Zeichenfolgen: Wörter über einem Alphabet Sei C eine Menge von Zeichen. Ein Wort der Länge n (oder eine Sequenz) über C ist gegeben durch hc1 . . . c[ n]i (Notation für Wörter), wobei c1 , . . . , cn ∈ C. Beispiel: c = {a, b, . . . , z} hheutei Wort der Länge 5 über C. Die Menge aller Wörter der Länge n bezeichnen wir mit C n (n-faches kartesisches Produkt). Die Elemente von C n heißen auch n-Tupel und werden oft auch durch (c1 , . . . , cn ) repräsentiert. Für n = 0 bekommen wir das leere Wort, darbestellt durch hi oder ε, d.h. C 0 = {ε}. C 1 = {hci, c ∈ C} Achtung: C 1 6= C! Die Menge aller Wörter über C ist durch C ∗ bezeichnet. C∗ = [ Cn n∈N0 Beispiel: ∗ • Strichzahlen: {I} = {ε, I, II, III, . . .} • Bytes: B8 ∗ • {0, 1, . . . , 9} 1.5. INFORMATION UND REPRÄSENTATION: NORMALFORM 11 Zur Darstellung von Texten verwenden wir spezielle Symbole (Leerzeichen, Zeilenvorschub, . . . ). (C ∗ )∗ = {hhc1 . . . cn ihc01 . . . c0n i . . .i, . . .} Wörter von Wörtern über C, Sequenzen von Sequenzen über C. ∅∗ = {ε} (∅∗ )∗ = {ε, hεi, hεεi, . . .} (Unendliche Menge aus nichts) Notation: C + = C ∗ \ {ε} Operationen auf Zeichenfolgen: • conc : C ∗ × C ∗ → C ∗ Konkatenation conc(hc1 . . . cn i, hc01 . . . c0n i) = hc1 . . . cn c01 . . . c0n i Häufig schreiben wir die Konkatenation infix: für w, v ∈ C ∗ : w ◦ v =def conc(w, v). Algebraische Gesetze: – w◦ε=w ε◦w =w ε ist ein neutrales Element bezüglic hder Konkatenation. – (w1 ◦ w2 ) ◦ w3 = w1 ◦ (w2 ◦ w3 ) Assoziativität • length : C ∗ → N, length(hc1 . . . cn i) = n ∗ Achtung: length auf {I} ist die klassische Interpretation. ∗ Für w, v ∈ {I} : length(w ◦ v) = length(w) + length(v). Schreibweise für w ∈ C ∗ : |w| = length(w). Definition: Formale Sprache Die formale Sprache S über C ist definiert als: S ⊆ C∗ 12 KAPITEL 1. INFORMATION, IHRE REPRÄSENTATION . . . Kapitel 2 Algorithmen und Rechenstrukturen Bestimmte Aufgabenstellungen lassen sich schematisch, durch Algorithmen lösen. Dabei betrachten wir in der Regel Aufgaben, die von gewissen Eingabegrößen abhängig sind (Eingabeparameter) und in der Regel werden bestimmte Ergebnisse durch den Algorithmus zurückgegeben (Resultate, Ausgaben). Beispiel: Multiplikation von Binärzahlen Eingabe: Zwei Binärzahlen b1 , b2 ∈ B∗ Ausgabe: Binärzahl r ∈ B∗ , wobei für I : B∗ → N (Interpretation) gilt: I[b1 ] · I[b2 ] = I[r]. 2.1 Der Begriff Algorithmus Definition: Algorithmus (informell) Ein Algorithmus ist ein schematisch anwendbares Verfahren [zur Lösung einer bestimmten Aufgabe] mit einer präzisen (d.h. in einer eindeutig verständlichen Sprache abgefassten) endlichen Beschreibung unter Verwendung effektiver (d.h. tatsächlich ausführbarer) Verarbeitungsschritte. Beispiel: (1) Multiplikation zweier Zahlen in Dezimaldarstellung (2) Euklids Algorithmus zur Berechnung des ggT zweier Zahlen a, b ∈ N (Eingabe). Der Algorithmus zur Berechnung des ggT(a, b) lautet wie folgt: (i) falls a = b, dann gilt ggT(a, b) = a (ii) falls a < b, dann gilt ggT(a, b) = ggT(a, b − a) (iii) falls a > b, dann gilt ggT(a, b) = ggT(a − b, b) Für Algorithmen unterscheiden wir fundamental zwei Sichten: (i) funktionale Sicht: Welche Aufgabenstellung bewältigt der Algorithmus? 13 14 KAPITEL 2. ALGORITHMEN UND RECHENSTRUKTUREN (ii) operationelle Sicht: Durch welche elementaren Verarbeitungsschritte wird die Aufgabe gelöst? (iii) (Warum?) Merkmale für Algorithmen Ein Algorithmus heißt: • terminierend, wenn er stets (für alle zulässigen Folgen von Schritten) nach endlich vielen Schritten endet. • deterministisch, wenn bei der Auswahl der Verarbeitungsschritte keine Freiheit besteht. • determiniert, wenn das Resultat (unabhängig von der Wahl der Verarbeitungsschritte im nicht-deterministischen Fall) eindeutig bestimmt ist. • sequentiell, wenn alle seine Verarbeitungsschritte stets strikt hintereinander ausgeführt werden. • parallel (nebenläufig), wenn gewisse Verarbeitungsschritte zeitlich (oder logisch) nebeneinander ausgeführt werden. Klassische Beschreibungselemente für Algorithmen: • Ausführung elementarer Schritte (Umformung gewisser Werte, Änderung gewisser Zustände) • Fallunterscheidung (Ausführen von Schritten abhängig von Bedingungen) • Wiederholung und Rekursion 2.1.1 Textersetzungsalgorithmen Wir betrachten zunächst nur Algorithmen auf Texten ∈ V ∗ (Wörter über einem endlichen Zeichenvorrat V). Textersetzungsregel: (v, w) ∈ V ∗ × V ∗ geschrieben v → w (*) Beispiel: textersetzungssystem → tes Konvention: In Textersetzungsregeln lassen wir die Klammern h. . .i weg. Für beliebige Wörter α, w ∈ V ∗ nennen wir α ◦ v ◦ ω → α ◦ w ◦ ω eine Anwendung der Regel (*). Ein Textersetzungssystem über einer Menge V von Zeichen ist eine endliche Menge von Textersetzungsregeln. Für ein Textersetzungssystem T nennen wir eine Folge von Wörtern t0 , . . . , tn ∈ V ∗ eine Berechnung, falls ∀i : ti → ti+1 die Anwendung einer Regel aus T ist. Achtung: Es existieren endliche und unendliche Berechnungen! Ein Wort t ∈ V ∗ heißt terminal bezüglich T, wenn keine Regel aus T auf t anwendbar ist, d. h. @t0 ∈ V ∗ , so dass t → t0 Anwendung einer Regel aus T ist. Eine endliche Berechnung t0 , . . . , tn heißt vollständig, wenn tn terminal ist. Hier heißt t0 Eingabe und tn Ausgabe. Der Textersetzungsalgorithmus für T erzeugt eine Berechnung für die Eingabe t0 ∈ V ∗ wie folgt: Solange eine Regel auf das letzte Wort in der Berechnungssequenz anwendbar ist, wende eine Regel an und füge die rechte Seite der Anwendung zur Berechnungssequenz hinzu. 2.1. DER BEGRIFF ALGORITHMUS 15 Beispiel: Addition von 1 zu einer Zahl in Binärdarstellung Aufgabe: x1 . . . xn Darstellung einer Zahl k in Binärschreibweise, y1 . . . ym Darstellung von k+1 in Binärschreibweise. Ax1 . . . xn SE → · · · → Ay1 . . . ym E (A, E und S sind Hilfszeichen) V = {L, 0, A, S, E} Textersetzungsregeln: • 0S → L • LS → S0 • AS → AL AL0LL0LLSE → AL0LL0LS0E → AL0LL0S00E → AL0LLL00E Beispiel: Revertieren einer Binärzahl Aufgabe: Ax1 . . . xn E → · · · → Axn . . . x1 E Algorithmus: • RL → SLR • R0 → S0R • SLL → LSL • S0L → LS0 • SL0 → 0SL • S00 → 0S0 • SLE → LE • S0E → 0E • RE → E AR00LE → AS0R0LE → AS0S0RLE → AS0S0SLRE → AS0S0SLE → AS0S0LE → AS0LS0E → AS0L0E → ALS00E → AL0S0E → AL00E Textersetzungsalgorithmen sind in aller Regel nicht deterministisch. In einem Wort gibt es in der Regel mehrere Anwendungsstellen für Regeln. 2.1.2 Deterministische Textersetzung Will man den Nichtdeterminismus vermeiden, dann können wir das durch Konventionen erreichen, die angeben, wann welche Regel zuerst angewendet werden soll / darf. Definition: Markov-Strategie zur Anwendung von Regeln Sind in einem Textersetzungssystem mehrere Regeln anwendbar, so wird immer diejenige Regel angewendet, die in der Aufschreibung der Regeln zuerst kommt. Ist die ermittelte Regel an mehreren Stellen anwendbar, so wählen wir die Anwendungsstelle, die am weitesten links liegt. Durch diese Strategie wird der Textersetzungsalgorithmus deterministisch. In der Menge der Berechnungen für ein Wort wird jeweils genau eine Berechnung 16 KAPITEL 2. ALGORITHMEN UND RECHENSTRUKTUREN und damit genau ein Resultat ausgezeichnet. Weitere Konvention: Terminierung über die Punktregel: Wir können zusätzlich gewisse Ersetzungsregeln auszeichnen, indem wir (für v, w ∈ V ∗ ) w → .v schreiben. Dann soll gelten, dass der Textersetzungsalgorithmus unmittelbar nach der Anwendung einer solchen Regel terminiert. Beispiel: Punktregel Durchschieben des Zeichens R in einem Binärwort bis hinter das erste L V = {R, 0, L} R0 → 0R RL → .LR 2.1.3 Durch Textersetzungssysteme induzierte Abbildungen Ein Textersetzungssystem T definert im deterministischen (auch im determinierten) Fall eine Zuordnung für jedes Wort w ∈ V ∗ vermöge w → · · · → v eine Berechnung (v ist terminal), oder wir erhalten für w eine unendliche Berechnung. In diesem Fall sagen wir, dass das Ergebnis undefiniert ist, dargestellt durch ⊥. Damit gilt: Jeder Textersetzungsalgorithmus T induziert eine Funktion fT : V ∗ → V ∗ ∪ {⊥} definiert durch fT (w) = v falls v terminal und w → . . . → v fT (w) = ⊥ falls die Berechnung für das Wort w nicht terminiert. Dies definiert die funktionale Sicht, die Berechnung selbst w → w1 → w2 → . . . → v die operationale Sicht. Textersetzungsalgorithmen arbeiten auf konkreten Darstellungen von Information. Betrachten wir eine konkrete Interpretation I : V ∗ → A (V ∗ Repräsentationen, A Informationen): fT V ∗ −−−−→ V ∗ ∪ {⊥} Iy yI A −−−−→ f 2.2 A0 Rechenstrukturen Algorithmen arbeiten über Datenstrukturen. Textersetzungsalgorithmen nutzen nur eine sehr elementare, uniforme Datenstruktur, nämlich Wörter (d.h. Zeichenfolgen). Wir betrachten nun Rechenstrukturen als eine strukturierte, für viele Aufgaben besser geeignete Darstellung von Information. 2.2.1 Funktionen und Mengen als Rechenstrukturen Zur Strukturierung und Modellierung umfangreicher Daten, Begriffe und Zusammenhänge ist es immer nützlich, zunächst eine Familie von Mengen zu identifizieren, die gewisse Informationen enthalten. Wir sprechen von Trägermengen. inhaber : Konto → Kunde 2.2. RECHENSTRUKTUREN 17 Definition: Rechenstrukturen Seien S und F Mengen von Bezeichnungen (Namen). S steht für die Namen für Trägermengen, F für die Menge der Namen von Funktionen. Eine Rechenstruktur A besteht aus je einer Trägermenge sA für jedes s ∈ S und A je einer Funktion f zwischenden Elementen der Trägermengen für jedes f ∈ F . A = ( sA ; s ∈ S , f A ; f ∈ F ) f A ist eine n-stellige Funktion auf gewissen Trägermengen. A A f A : sA 1 × . . . × sn → sn+1 Beispiel: Die Rechenstruktur der Wahrheitswerte BOOL bool S = {bool bool} F = {true, false, ¬, ∨, ∧, ⇒, . . .} bool BOOL = B ∪ {⊥} trueBOOL :→ B ∪ ⊥ Abkürzung: M ⊥ := M ∪ ⊥ falseBOOL : B⊥ → B⊥ ¬BOOL : B⊥ → B⊥ ∨BOOL , ∧BOOL , ⇒BOOL : B⊥ × B⊥ → B⊥ falseBOOL = 0 trueBOOL = 0 ¬BOOL (b) = not(b) BOOL ¬ fürb ∈ B (⊥) = ⊥ Beispiel: Die Rechenstruktur der Sequenzen SEQ bool S = {bool bool, nat nat, seqm seqm, m } F ={true, false, ¬, ∨, ∧, . . . , ? zero, succ, pred, add, mult, ≤, =, . . . , empty, make, conc, first, rest, last, lrest, length, . . .} 18 KAPITEL 2. ALGORITHMEN UND RECHENSTRUKTUREN Trägermengen: bool SEQ = B⊥ natSEQ = N⊥ seqm SEQ = (M ∗ )⊥ m SEQ = M ⊥ gegebene Trägermenge M ? = : N⊥ × N⊥ → B⊥ L falls n = m ⊥ ? = (n, m) = 0 falls n 6= m ⊥ falls n = ⊥ ∨ m = ⊥ für n, m ∈ N firstBOOL : (M ∗ )⊥ → M ⊥ ( m1 für k ≥ 1 BOOL first (hm1 m2 . . . mk i) = ⊥ für k = 0 ... Beispiel: Geldautomat als Rechenstruktur G S = {karte, pin, betrag, bool, zustand} F = {ein, pinein, betragein, pinok, ausgabe, aus} einG : zustandG × karteG → zustandG pineinG : zustandG × pinG → zustandG betrageinG : zustandG × betragG → zustandG pinokG : zustandG → boolG ausgabeG : zustandG → betragG ausG : zustandG → zustandG karteG : zustandG → boolG Wir verzichten auf die Angabe einer Trägermenge zustandG und betrachten nur eine wichtige Gleichung: Sei z ∈ zustandG und karteG (z) = 0 (d.h. keine Karte im Automaten). ausgabeG betrageinG pineinG einG (z, k) , p , b = ( b falls pinokG pineinG einG (z, k) , p = L = 0 sonst 2.2.2 Signaturen Die Namen (Bezeichnungen, Identifikatoren) für die Trägermengen und Funktionen einer Rechenstruktur bilden ihre Signatur. 2.2. RECHENSTRUKTUREN 19 pin karte aus ein pin_ein zustand karte betrag_ein betrag ausgabe pin_ok bool Abbildung 2.1: Signaturgraph Definition: Signatur Eine Signatur Σ = (S, F ) ist ein Paar von Mengen: • S ist die Menge der Bezeichnungen für Trägermengen. Die Elemente von S heißen Sorten oder Typen. • F ist eine Menge von Bezeichnungen für Funktionen (wir sprechen von Funktionssymbolen oder Konstanten). Dabei ist für jedes f ∈ F eine Funktionalität festgelegt: f ct ctf = (s1 , s2 , . . . , sn )sn+1 Im nullstelligen Fall: f ct ctf = s1 Beispiel: f ct pinein = (zustand, pin) zustand Oft wird eine grafische Angabe von Signaturen bevorzugt. Dann verwenden wir ein Signaturdiagramm (s. Abbildung 2.1). 2.2.3 Grundterme Für eine gegebene Signatur können wir Funktionsterme (auch Funktionsausdrücke), kurz Terme, schreiben. Jeder Term hat eine Sorte (einen Typ). Definition: Terme Die Menge der Terme der Sorte s ∈ S über einer Signatur Σ = (S, F ) ist wie folgt definiert: (i) Jedes nullstellige Funktionssymbol f ∈ F mit f ct ctf = s ist ein Term. (ii) Ist f ∈ F mit f ct ctf = (s1 , . . . , sn )s und sind t1 , . . . , tn Terme der entsprechenden Sorten s1 , . . . , sn , dann ist f (t1 , . . . , tn ) ein Term der Sorte s. Beispiel: Terme über der Signatur zur Rechenstruktur SEQ Terme der Sorte bool: {true, false, ¬(true), . . . , ∧(true, false), . . .} Terme der Sorte nat: {zero, succ(zero), +(zero, succ(zero)} ¬(zero) ist kein Term, weil f ct ct¬ = (bool) bool 20 KAPITEL 2. ALGORITHMEN UND RECHENSTRUKTUREN 1235 723 + 270 * Abbildung 2.2: Berechnungsvorgang für ∗(+(1235, 723), 270) Terme definieren für eine entsprechende Rechenstruktur Berechnungsvorgänge (s. Abbildung 2.2). ≤ (3, 7) geklammerte Präfixschreibweise 3≤7 Infixschreibweise Konvention: Wir schreiben gewisse Terme zur besseren Lesbarkeit statt in geklammerter Präfix- in Infix-, Postfix- oder Mixfix-Schreibweise. Beispiel: Schreibweisen von Termen √ Postfix-Schreibweise 10!, 78 , 12 3 < 7 < 20 Mixfix-Schreibweise Wir bezeichnen mit WΣs die Menge der Terme der Sorte s ∈ S zur Signatur Σ = (S, F ). Für jeden Term t ∈ WΣs existiert ein Wert für jede Σ-Rechenstruktur A. Dieser Wert liegt in der Trägermenge sA und wird durch die Interpretation IsA : WΣs → sA festgelegt. IsA [f ] = f A mitff ct ctf = s Beispiel: N AT [zero] = zeroN AT = 0 Inat A Is [f (t1 , . . . , tn )] = f A (IsA [t1 , . . . , IsA [tn ]) falls f ct ctf = (s1 , . . . sn )s SEQ Iseqnat [conc(emptyset, make(zero))] = SEQ SEQ concSEQ (Iseqnat [emptyset, Iseqnat [make(zero)]) = ε ◦ h0i = h0i 2.2.4 Terme mit Identifikatoren Typischerweise arbeiten wir beim Umgang mit Termen zusätzlich mit Identifikatoren (Bezeichnungen, Variablen). Die Identifikatoren stehen für frei wählbare Werte aus gewissen Trägermengen. ∗(+(x, y), z) 2.2. RECHENSTRUKTUREN 21 Den Identifikatoren sind Sorten zugeordnet. Damit kann bei der Termbildung auf die richtige Sortenangabe geachtet werden. ∧(≤ (x, y), x) kein Term Um einem Term t über der Signatur Σ mit freien Variablen aus X einen Wert in einer Algebra A zuordnen zu können, verwenden wir wieder die Idee der Belegung. X sei dabei eine Familie von sortierten Variablen. X := {Xs : s ∈ S} Xs . . . Menge der Identifikatoren der Sorte s. Eine Belegung von X ist eine Abbildung von der Menge aller Identifikatoren in die Menge aller Werte in A: β : {x ∈ Xs : s ∈ S} → a ∈ sA : s ∈ S wobei für x ∈ X : β(x) ∈ sA , d.h. Identifikatoren der Sorte s sind jeweils mit Werten der Sorte s belegt. Die Interpretation von Termen t für eine gegebene Belegung β bezeichnen wir mit IβA [t] Definition: Interpretation IβA [x] = β(x) IβA [f (t1 , . . . , tn )] =f A mitx ∈ Xs (IβA [t1 ], . . . , IβA [tn ])mitf ∈F Beispiel: Interpretation mult(ad(x, y), z) β x 2 y 3 z 4 IβN AT [mult(add(x, y), z)] = = multN AT (IβN AT [add(x, y)], IβN AT [z]) = = addN AT (IβN AT [x], IβN AT [y]) · 4 = = (2 + 3) · 4 = 5 · 5 = 20 Für das punktweise Ändern einer Belegung verwenden wir folgende Notation: β[m/x] x ∈ Xs , m ∈ sA bezeichnet wieder eine Belegung: ( m fallsx = y (β[m/x])(y) = β(y) sonst 22 KAPITEL 2. ALGORITHMEN UND RECHENSTRUKTUREN Beispiel: β 2 3 4 x y z 2.3 β[6/z] 2 3 6 Algorithmen als Termersetzungssysteme Termersetzungssysteme (TES) liefern übersichtlichere Algorithmen als Textersetzung. 2.3.1 Termersetzung Gegeben sei eine Signatur Σ, eine Familie X von Identifikatoren. Mit WΣs (x) bezeichnen wir die Menge der Terme über Σ und X der Sorte s. Termersetzungsregel: (t, r) ∈ WΣs (x) × WΣs (x), geschrieben t → r Beispiel: pred(succ(x)) = x Für beliebige Identifikatoren t1 , . . . , tn und Terme t1 , . . . , t2 der korrekten Sorten heißt t[t1 /x1 , . . . , tn /xn ] → r[t1 /x1 , . . . , tn /xn ] eine Anwendung. Sei c ein beliebiger Term, der den Identifikator v genau einmal enthält (v sei von der gleichen Sorte wie t und r). Anwendung c[t0 /v] → c[r0 /v], wobei t0 → r0 Instanz der Regel t → r sei. 2.3.2 Termersetzungssysteme Ein TES ist eine (endliche) Menge von Termersetzungsregeln. Eine Berechnung ist eine endliche Folge t0 → t1 → . . . → tn von Termen oder eine unendliche Folge t0 → t1 → . . . → . . . von Termen mit der Eigenschaft, dass für alle ti → ti+1 die Anwendung einer Regel ist. Eine endliche Berechnung heißt vollständig, wenn tn terminal ist, d.h. es existiert keine Regel mit tn → t. Die terminalen Terme bilden ein Normalformensystem. Beispiel: Termersetzung 2.3. ALGORITHMEN ALS TERMERSETZUNGSSYSTEME 23 nat S = {nat nat} F = {zero, pred, succ} pred(succ(x)) → x Berechnung: pred(succ(succ(pred(succ(x))))) → succ(pred(succ(x))) → succ(x) Terme in Normalform: zero, succ(zero), succ(. . . (zero) . . .), pred(zero), succ(. . . (pred(. . . (zero) . . .) . . .) Beispiel: Binäre Addition als Termersetzung bit S = {bit bit, seq bit bit} F = {stock, badd, empty} seq bit seq bit f ct stock = (seq bit, bit bit)seq f ct empty = seq bit seq bit seq bit f ct badd = (seq bit, seq bit bit)seq Zur Notation: Eine Binärzahl hL0Li stellen wir als stock(stock(stock(empty, L), 0), L) dar (Termnormalformdarstellung von Binärzahlen). • badd(empty, x) → x • badd(x, empty) → x • badd(stock(x, 0), stock(y, 0)) → stock(badd(x, y), 0) • badd(stock(x, 0), stock(y, L)) → stock(badd(x, y), L) • badd(stock(x, L), stock(y, 0)) → stock(badd(x, y), L) • badd(stock(x, L), stock(y, L)) → → stock(badd(badd(x, y), stock(empty, L)), 0) 2.3.3 Korrektheit von TES Sei Σ eine Signatur, X eine Familie von Identifikatoren, R ein TES. Sei A eine Rechenstruktur der Signatur Σ. Eine Termersetzungsregel t → r heißt korrekt (bezüglich A), wenn für alle Belegungen beta gilt: IβA [t] = IβA [r] Beispiel: IβN AT [empty] = 0 IβN AT [stock(t, b)] = IβN AT [t] · 2 + IβN AT [b] IβN AT [badd(t1 , t2 )] = IβN AT [t2 ] + IβN AT [t2 ] Ein TES heißt partiell korrekt, wenn jede seiner Regeln korrekt ist. 24 KAPITEL 2. ALGORITHMEN UND RECHENSTRUKTUREN Beispiel: Korrektheit der obigen dritten Regel IβN AT [badd(stock(x, 0), stock(y, 0))] = β(x) · 2 + β(y) · 2 IβN AT [stock(badd(x, y), 0)] = (β(x) + β(y)) · 2 Dies ist für jede Regel zu machen, um die partielle Korrektheit zu zeigen. Neben der Frage, ob die Umformungen durch Termersetzung die Werte der Terme unverändert lassen, interessiert uns auch die Frage, ob jede Berechnung terminiert. Ein TES heißt (total) korrekt, falls es partiell korrekt ist und jede Berechnung terminiert. Der Beweis der Terminierung erfolgt über eine Gewichtsfunktion: G : WΣ (X) → N Für eine geeignete Gewichtsfunktion zeigen wir, dass für jede Regel t → r gilt: G[t] > G[r]. Berechnung: t 0 → t 1 → t2 → . . . G[t0 ] > G[t1 ] > G[t2 ] > . . . 2.4 Aussagenlogik und Prädikatenlogik Eine der ältesten Ideen zur Führung unangreifbarer Argumentation ist die Verwendung formaler Beweissysteme. Idee: Verwendung festgelegter Regeln zur Argumentation. Wir betrachten dazu Terme der Sorte bool bool, die Aussagen darstellen. Problem: In Rechenstrukturen können Terme der Sorte bool auch den Wert undefiniert haben. Wir schränken uns auf sogenannte zweiwertige Logik ein, d.h. auf Terme der Sorte bool bool, deren Werte für jede Belegung entweder L oder 0 sind. Wichtige Aussagen, die wir häufig betrachten, sind Gleichungen: t1 = t2 (Gleichung zwischen Termen gleicher Sorte). Starke Gleichheit: ( L fallsIβA [t1 ] = IβA [t1 ] A Iβ [t1 = t2 ] = 0 sonst Schwache Gleichheit: A A L falls⊥ 6= Iβ [t1 ] = Iβ [t1 ] IβA [t1 = t2 ] = 0 falls⊥ 6= IβA [t1 ] 6= IβA [t1 ] 6= ⊥ ⊥ falls⊥ = IβA [t1 ] ∨ IβA [t1 ] = ⊥ Beispiel: Gleichheit Starke Gleichheit: (predN AT (0) = succN AT (predN AT (0))) → (⊥ = ⊥) → L Schwache Gleichheit: (predN AT (0) = 0) → ⊥ Die starke Gleichheit verwenden wir in logischen Aussagen (zweiwertige Logik), die schwache Gleichheit in Programme / Algorithmen, da die Gleichung nicht terminiert, falls die Berechnung eines der Werte der Terme t1 oder t2 nicht terminiert. Achtung: t1 = t2 ? t1 = t2 ist zweiwertig ist dreiwertig 2.4. AUSSAGENLOGIK UND PRÄDIKATENLOGIK 2.4.1 25 Aussagenlogik Im Zentrum der Logik steht der Ableitungsbegriff: Aus einer Menge H gegebener Aussagen werden nach festgelegten Regeln weitere Aussagen t abgeleitet. Wir schreiben H`t um auszudrücken, dass sich aus der Menge der Aussagen H die Aussage t ableiten lässt. Beispiel: {x ∧ y} ` x Wir suchen ein Regelsystem, in dem die Ableitung korrekt vorgenommen werden kann. Notation: ` t, falls ∅ ` t. Ableitungsregeln der Aussagenlogik (Schlussregeln) Seine t, t1 , t2 beliebige (zweiwertige) boolesche Terme. • ` t ∨ ¬t tertium non datur • {t1 , ¬t2 ∨ t2} ` t2 modus poneus • Ist t1 = t2 Anwendung einer Regel der booleschen Algebra (oder der eingeführten Abkürzungen, dann gilt {t1 } ` t2 Mehr Schlussregeln werden nicht benötigt. Sei H eine Menge von Aussagen (zweiwertige boolesche Terme); wir sagen: die Aussage t ist direkt aus H ableitbar und schreiben H ` t, falls t mit einer der Schlussregeln aus H ableitbar ist, d.h. falls h0 ⊆ H existiert und H 0 ` t entspricht einer der drei Schlussregeln. Konsequenz: Unser Ableitungsbegriff ist monoton: Gilt H ` t und ferner H ⊆ H 0 , so gilt H 0 ` t. Manchmal schreibt man statt {t1 , . . . , tn } ` t auch t1 . . . t n t gelesen aus t1 , . . . , tn lässt sich t ableiten. Format eines Beweises, ausgehend von einer Menge von Annahmen (Axiomen) H (Beweis für: H ` tn ): H ` t0 H ∪ {t0 } ` t1 H ∪ {t0 , t1 } ` t2 .. . H ∪ {t0 , . . . , tn−1 } ` tn Lemma: Gilt H ` t1 ⇒ t2 , so gilt auch H ∪ {t1 } ` t2 . Theorem: {false} ` t, d.h. jede Aussage t lässt sich aus false ableiten. 26 KAPITEL 2. ALGORITHMEN UND RECHENSTRUKTUREN Terminologie: Die Menge der Annahmen H heißt auch Axiome. Eine Menge H von Axiomen und eine Menge von Ableitungsregeln bilden ein Ableitungssystem, auch formales System oder Theorie genannt. Die ableitbaren Aussagen nennen wir Theoreme. Eine Theorie heißt inkonsistent, falls jede Aussage ableitbar ist. In unserem Fall ist die Theorie bereits inkonsistent, wenn für eine Aussage t gilt: H ` t und H ` ¬t. Begründung: H ` (t ∧ ¬t) = false. Theorem: Korrektheit von Ableitungen Gilt H ` t, dann gilt für jede Belegung β der Identifikatoren in H bzw. t: Sind alle Aussagen in H für β wahr, d.h. gilt ∀t ∈ H : Iβ [t0 ] = L, dann gilt auch Iβ [t] = L. Eine Theorie ist also korrekt, wenn aus wahren Annahmen die Wahrheit der abgeleiteten Aussagen folgt. 2.4.2 Prädikatenlogik In der Aussagenlogik werden nur Aussagen betrachtet und die Frage, ob diese den Wert L oder 0 besitzen. Der innere Aufbau atomarer Aussagen ist nicht von Interesse. Beispiel: Atomare Aussagen • Hilde studiert Informatik. • Klaus studiert BWL. Ein Prädikat ist eine Abbildung p : M1 × M2 × . . . × M n → B Beispiel: Sei M die Menge der Personen, G die Menge der Studienfächer. studiert : M × G → B studiert(Hilde, Informatik) studiert(Klaus, BWL) Mit Hilfe von Prädikaten können wir Terme schreiben, die Aussagen bilden. Auf diese können alle aussagenlogischen Verknüpfungen angewandt werden. Beispiel: Wie können wir die Aussage Hans ist Student formulieren? ∃x ∈ G : studiert(Hans, x) Um bestimmte allgemeine Aussagen formulieren zu können, werden Quantoren verwedet: • Allquantor: ∀mx : t oder ∀x ∈ M : t • Existenzquantor: ∃mx : t oder ∃x ∈ M : t Beispiel: Nicht alle Personen studieren Informatik: ¬∀y ∈ M : studiert (y, Informatik) Durch die Verwendung von Quantoren bekommen wir wieder Aussagen. Der Identifikator x in ∀x . . . bzw. ∃x . . . heißt durch den Quantor gebunden. (∀x ∈ M : t) ≡ (∀y ∈ M : t[y/x]), falls y in t nicht frei vorkommt. 2.4. AUSSAGENLOGIK UND PRÄDIKATENLOGIK 27 Beispiel: Namenskonflikt ∀x ∈ G : ∃y ∈ M : studiert(y, x) Hier ist x nicht durch y ersetzbar. Regeln für die Substitution mit gebundenen Bezeichnungen: Seien x, y Bezeichnungen. (∀x ∈ M : t)[t0 /y] = ∀x ∈ M : (t[t0 /y]), falls x nicht frei in t0 vorkommt. Kommt x frei in t0 vor, muss die gebundene Bezeichnung x vor der Substitution umbenannt werden. Umbenennungsregeln: (∀x ∈ M : t) = (∀y ∈ M : t[y/x]), falls y nicht frei in t vorkommt. Gesetze: • ∀x ∈ M : t = ¬∃x ∈ M : ¬t • ∃x ∈ M : t = ¬∀x ∈ M : ¬t Auch für Quantoren lassen sich Ableitungsregeln angeben. Wir schreiben (Σ, H) ` t falls für den booleschen Term (zweiwertig!) t gilt: t ist Term über der Signatur Σ und lässt sich aus der Menge der Aussagen H ableiten. Alle Ableitungsregeln für Aussagenlogik gelten auch für die so erweiterte Sprache der Aussagen. Ableitungsregeln für die Prädikatenlogik Sei M Trägermenge zur Sorte m , sei M 6= ∅, M \ {⊥} = 6 ∅, sei x von der Sorte m : (Σ, H) ` t x ist nicht frei in H und kein Element von Σ mx : t ∀m mx : t sei t1 Term der Sorte m , t1 6= ⊥ (Σ, H) ` ∀m (Σ, H) ` t[t1 /x] (Σ, H) ` t[t1 /x] sei t1 Term der Sorte m , t1 6= ⊥ mx : t (Σ, H) ` ∃m mx : t sei f ct (Σ0 , H) ` ∃m ctx = m in Σ und Σ0 = Σ ohne x (Σ, H) ` t Wieder gilt eine Monotonie: Gilt (Σ, H) ` t und H ⊆ H 0 , Σ ⊆ Σ0 mit Σ = (S, F ), Σ0 = (S 0 , F 0 ), S ⊆ S 0 , F ⊆ F 0 , so gilt (Σ0 , H 0 ) ` t. Enthält die Signatur Σ0 Sorten und Funktionen, die nicht in Σ vorkommen, aber auch nicht in H oder t, so gilt ((Σ, H) ` t) ⇔ ((Σ0 , H) ` t). Interpretation von Quantoren über einer Algebra A der Signatur Σ (β Belegung, t zweiwertiger boolescher Term): 0 A 0 L falls für die Belegung β : Iβ [t] = L mit β = β[w/x] A A mx : t] = Iβ [∀m mit w ∈ m \ {⊥} 0 sonst 0 A 0 L falls eine Belegung β existiert Iβ 0 [t] = L mit β = β[w/x] mx : t] = IβA [∃m und w ∈ mA \ {⊥} 0 sonst 28 KAPITEL 2. ALGORITHMEN UND RECHENSTRUKTUREN Kapitel 3 Programmiersprachen Komplexe Algorithmen lassen sich kaum lesbar und handhabbar durch Text- oder Termersetzung beschreiben. Benötigt wird eine angepasste Notation: wir sprechen von Programmiersprachen. Definition: Programmiersprache Eine Programmiersprache ist eine formale Sprache über einer endlichen Zeichenmenge V (d.h. die Programmiersprache ist ⊆ V ∗ ) zur Beschreibung von Algorithmen, Datenformaten und Anweisungen / Ausdrücken bezogen auf eine konkrete oder abstrakte Maschine. Eine Programmiersprache ist dafür konstruiert, auf einem Rechner verarbeitet zu werden. Genau: Die Programme (Wörter der formalen Sprache) können auf dem Rechner ausgeführt werden. Es gibt eine große Zahl von Programmiersprachen - jeder Rechner hat eine eigene Programmiersprache; wir sprechen von Maschinensprachen. Daneben existieren rechnerunabhängige Sprachen (höhere oder problemorientierte Programmiersprachen), die auf vielen Rechnern realisiert werden können. Diese haben folgendes Ziel: • Formulierung von Programmen, die auf vielen unterschiedlichen Rechnern einsetzbar sind (Portabilität). • Formulierung der Programme mit Mitteln, die eher dem zu lösenden Problem dienen, als sich an der Struktur des Rechners zu orientieren (Lesbarkeit). Eine Programmiersprache hat eine äußere Form (die formale Sprache) - wir sprechen von Syntax. Durch die Syntax wird festgelegt, wie die Programme aufgebaut sind. Zusätzlich hat jedes Programm einer Programmiersprache eine Bedeutung (Semantik). Dadurch ist festgelegt, was das Programm leistet, d.h. was passiert, wenn das Programm ausgeführt wird. Dabei unterscheiden wir: • funktionale Semantik: Welche Ergebnisse / Effekte werden durch das Programm erzielt? • operationale Sematik: Durch welche Einzelschritte werden die Ergebnisse / Effekte erzielt? 3.1 Syntax - BNF Eine Programmiersprache ist - syntaktisch gesehen - eine formale Sprache. Allerdings sind Programmiersprachen oft sehr kompliziert in ihrer syntaktischen Struktur. Deshalb setzen wir eine bestimmte Notation ein (Backus - Normalform, BNF), 29 30 KAPITEL 3. PROGRAMMIERSPRACHEN um Programmiersprachen syntaktisch zu beschreiben. (Die BNF ist nach John Backus benannt, einem amerikanischen Informatiker, der Fortran schuf und BNF zur Beschreibung von Algol entwickelte.) 3.1.1 BNF Ein Teil der BNF - Notation sind reguläre Ausdrücke. Sie dienen zur Beschreibung einfacher formaler Sprachen. Beispiel: E-Mail-Adresse [a|b| . . .]∗ .[a|b| . . .]∗ @[a|b| . . .]∗ [.[a|b| . . .]∗ ]∗ BNF-Notation ist selbst eine Sprache zur Beschreibung von Sprachen. Sei ein Zeichensatz V = {v1 , . . . , vn } gegeben. regulärer Ausdruck Bedeutung: ⊆ V ∗ Terminalzeichen v ∈ V v {hvi} ⊆ V ∗ Vereinigung (R, Q reguläre R—Q L[R] ∪ L[Q] Ausdrücke mit Sprachen L[R], L[Q] ⊆ V ∗ ) Konkatenation RQ {x ◦ y : x ∈ L[R], y ∈ L[Q]} Option {R} ε ∪ L[R] ∗ Iteration {R} , R∗ {x1 ◦ . . . ◦ xn : n ∈ N∧ x1 , . . . , xn ∈ L[R]} + {R} , R+ {x1 ◦ . . . ◦ xn : n ∈ N \ {0} ∧ x1 , . . . , xn ∈ L[R]} Klammern [R] L[[R]] = L[R] leere Sprache {} , ε L[{}] = 0, L[ε] = {ε} Die Schreibweise der regulären Ausdruücke ist dann umständlich, wenn Abkürzungen hilfreich wären. Beispiel: Aufbau eines Buches hBuchi ::=hTitelihVorwortihInhaltsverzeichnisi hKapitelihKapiteli+ hIndexi hTiteli ::=hTitelbezeichnungihAutorihVerlagi hKapiteli ::=hKapitelüberschriftihVorspannihAbschnittihAbschnitti+ ... Idee: Jedes Wort h. . .i ist Bezeichnung für eine formale Sprache. Wir sprechen von Nichtterminalen. Eine BNF-Beschreibung hat allgemein die Form: he1 i ::= R1 he2 i ::= R2 .. . hen i ::= Rn 3.1. SYNTAX - BNF 31 (Definitionssystem). Dabei sind he1 i, . . . , hen i Nichtterminalzeichen und R1 , . . . , Rn reguläre Ausdrücke, in denen auch Nichtterminalzeichen auftreten dürfen. Jedes Nichtterminal bezeichnet eine formale Sprache. Beispiel: Klammergebirge hklammergebirgei ::= (hklammergebirgei) |hklammergebirgeihklammergebirgei | () L[hklammergebirgei] = {h()i, h(())i, h()()i, . . .} In BNF dürfen wir auch rekursive Definitionen für Sprachen schreiben, also Definitionssysteme, bei denen die definierten Nichtterminale beliebig auf der rechten Seite in regulären Ausdrücken verwendet werden. Beispiel: Aussagenlogik hAussagei ::= true | false |hatomareAussagei |hIdentifikatori |(¬hAussagei) |hAussagei[∧| ∨ | ⇒ | ⇐ | ⇔ | . . .]hAussagei Die formalen Sprachen L[he1 i], . . . , L[hen i], die wir mit einem BNF-Definitionssystem verbinden, definieren wir wie folgt: Wir konstruieren eine Folge von formalen Sprachen E10 , E20 , . . . , En0 E11 , E21 , . . . , En1 ... wie folgt: • Ei0 = ∅ für i = 1, . . . , n sei die formale Sprache, die wir erhalten, indem wir für he1 i, . . . , hen i in • ej+1 i Ri die formalen Sprachen E1j , . . . , Enj verwenden. Beispiel: Klammergebirge Folge der formalen Sprachen E 0 , E 1 , . . . für hklammergebirgei: E0 = ∅ E 1 = {h()i} E 2 = {h(())i, h()()i, h()i} E3 = . . . 32 KAPITEL 3. PROGRAMMIERSPRACHEN PROGRAM identifier ( identifier ) ; block . ; Abbildung 3.1: Syntax-Diagramm eines Pascal-Programms Beobachung: Eij ⊆ Eij+1 S j Wir definieren Ei = Ei als die formale Sprache zu hei i. Wir erhalten dadurch j∈N eine induktive Definition für die formale Sprache Ei für hei i. Ferner gilt Ei = L[Ri ], wobei wir in Ri für die he1 i, . . . , hen i jeweils die formalen Sprachen E1 , . . . , En verwenden. Beispiel: Klammergebirge E = {h(i ◦ e ◦ h)i : e ∈ E} ∪ {e1 ◦ e2 : e1 , e2 ∈ E} ∪ {h()i} Fixpunktgleichung für die Sprache E Können wir die Sprachen E1 , . . . , En ausschließlich durch diese Fixpunktgleichungen charakterisieren? Ja, wir müssen, falls verschiedene Lösungen existieren (d.h. verschiedene Sprachen E10 , . . . , En0 ), jeweils die Sprachen wählen, die in der ⊆ Ordnung am kleinsten sind. 3.1.2 Syntaxdiagramme Statt BNF-Notation werden auch Syntax-Diagramme verwendet, um formale Sprachen zu beschreiben. Pascal-Programm: s. Abbildung 3.1 in BNF: hprogrammi ::= PROGRAM hidentifieri (hidentifieri {, hidentifieri}∗ ; hblocki. 3.1.3 Kontextbedingungen Durch die BNF-Notation können wir formale Sprachen definieren. Allerdings gibt es in Programmiersprachen in der Regel zusätzliche Forderungen an die Elemente dieser formalen Sprache, die erfüllt sein müssen, damit wir von wohlgeformten Programmen sprechen. 3.2 Semantik Liegt die Syntax einer Sprache fest: L ⊆ V ∗ , wobei in der Regel L ⊆ L0 , L0 durch BNF beschrieben und p ∈ L0 ist in L, wenn p alle Kontextbedingungen erfüllt, so können wir für die Elemente in L die Semantik definieren. Im Idealfall geben wir drei Varianten von Semantik: (i) funktional (Welche Ergebnisse liefert ein Programm?) (ii) operational (Durch welche Schritte wird das Ergebnis erzielt?) (iii) axiomatisch (Wie können wir beweisen, dass das Programm das gewünschte leistet?) 3.3. IMPLEMENTIERUNG VON PROGRAMMIERSPRACHEN 3.3 33 Implementierung von Programmiersprachen Im einfachsten Fall entspricht ein Programm direkt einer Folge von Schritten, die in einem Rechner ausgeführt werden. Entspricht ein Programm p ∈ L nicht genau den Schritten, die ein Rechner zur Verfügung stellt, so kennen wir zwei Verfahren zur Implementierung des Programms auf einem Rechner: (i) Interpretation: Wir realisieren auf unserem Rechner ein Programm (in der Maschinensprache), das das Programm p ∈ L als Eingabe nimmt und diese interpretiert. (ii) Übersetzung: Wir übersetzen p ∈ L in die Maschinensprache. In der Regel wird diese Übersetzung wieder durch ein Programm vorgenommen (Compiler). 3.4 Methodik der Programmierung Die Erstellung (Entwicklung) umfangreicher Programme (Software) ist schwierig und aufwändig. Deshalb ist ein systematisches, wohlüberlegtes Vorgehen ratsam. Grundprinzip: Niemals ein Programm schreiben, bevor man genau verstanden hat, was es leisten soll. Erst spezifizieren, dann programmieren, zuletz verifizieren. 34 KAPITEL 3. PROGRAMMIERSPRACHEN Kapitel 4 Applikative / funktionale Sprachen Applikative oder funktionale Programmiersprachen zeichnen sich dadurch aus, dass man Algorithmen in der Form von Funktionsdefinitionen beschreibt, die so aufgebaut sind, dass damit die Funktionswerte für beliebige Parameter algorithmisch ermittelt werden können. Beherrschendes Element dieser Sprachen: • Funktionsdefinition • Funktionsanwendung (Applikation) 4.1 Rein applikative Sprachen Beispiel: Fibonacci 0 fib(n) = 1 fib(n − 2) + fib(n − 1) n 0 1 2 3 fib(n) 0 1 1 2 als Pascal-Funktion: 1 2 4 6 8 10 4 3 5 5 6 8 7 13 für n = 0 für n = 1 für n > 1 ... ... function fib (n: integer): integer; begin if n = 0 then fib := 0 else if n = 1 then fib := 1 else fib := fib (n-1) + fib (n-2) end; 4.1.1 Syntax von Ausdrücken In BNF-Schreibweise definieren wir die Syntax von Ausdrücken als formale Sprache für hexpi: 35 36 KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN hexpi ::= hidi |⊥ |hfunctionapplicationi |hconditionalexpi |(hexpi) |hmonadopihexpi |ihexpihdyadopihexpi |hsectioni hmonadopi ::= −|¬ · ? hdyadopi ::= +| − | · | = | < | ≤ | = | ≥ | > | ∧ | ∨ | ⇒ | ⇐ | ⇔ |◦ · 4.1.2 Bedeutung von Ausdrücken Grundsätzlich sind wir an drei Formen der Beschreibung von Ausdrücken interessiert: (i) operational: Angabe von Termersetzungsregeln (ii) funktional: Angabe einer Interpretationsfunktion (iii) axiomatisch: Angabe von Umformungsregeln Um Programme schreiben zu können, setzen wir eine gegebene Signatur Σ = (S, F ) voraus, sowie eine Rechenstruktur A zu dieser Signatur. Wir verwenden eine Reihe von Mengen, auf die wir die Bedeutung von Ausdrücken stützen. ID Menge von Identifikatoren D = a ∈ sA : s ∈ S Menge der Datenelemente A A F CT ={f : sA 1 × . . . × sn → sn+1 : n ∈ N0 ∧ f strikt ∧ ∀i : 1 ≤ i ≤ n + 1 : si ∈ S} Eine Funktion heißt strikt, wenn: A A f : sA 1 × . . . × sn → sn+1 f (a1 , . . . , an ) = ⊥ ⇐ ai = ⊥ für ein i, 1 ≤ i ≤ n H = D ∪ F CT Menge der semantischen Elemente Beobachtung: Ist ein Term t über Σ mit Funktionen gebildet, die alle strikt sind (in A), so gilt: Enthält t einen Teilterm t0 mit I[t0 ] = ⊥, so gilt I[t] = ⊥. In Programmiersprachen verwenden wir in der Regel Sprachkonstrukte, die nicht strikt sind, d.h. deren Wert ungleich ⊥ ist, auch wenn Teilterme auftreten, deren Wert ⊥ ist. Beispiel: Nicht strikte Funktion ; Sequentielles Oder ∨ cor : B⊥ × B⊥ → B⊥ 4.1. REIN APPLIKATIVE SPRACHEN 37 cor 0 L ⊥ 0 0 L ⊥ L L L L ⊥ ⊥ ⊥ ⊥ cor(t1 , t2 ) wird ausgewertet, indem wir zunächst t1 auswerten. Falls t1 den Wert L liefert, setzen wir den Wert von cor(t1 , t2 ) = L. Sonst t2 auswerten und dann den Wert von cor ermitteln. Falls die Auswertung von t1 nicht terminiert, terminiert insgesamt die Auswertung von cor nicht. cor ist nicht strikt. Damit gilt die obige Regeln Ausdrücke mit Teilausdrücken mit Wert ⊥ haben den Wert ⊥ nicht für Ausdrücke, die mit nicht strikten Funktionen gebildet werden. Uneingeschränkte Termersetzung führt auf unendliche Berechnungen, falls Teilausdrücke Berechnungen haben. Beispiel: cor cor(true, x) → true cor(false, x) → x Sei t0 → t1 → . . . unendliche Berechnung für t0 . cor(true, t0 ) → true cor(false, t0 ) → cor(false, t1 ) → . . . Fazit: Für nicht strikte Funktionen benötigen wir ein Termersetzungskonzept, für das wir die Anwendung der Regeln genauer steuern können. Wir verwenden bedingte Termersetzungsregeln. Beispiel: (Fortsetzung: cor) cor(false, x) → x t1 → t2 ⇒ cor(t1 , t3 ) → cor(t2 , t3 ) Dabei dürfen Termersetzungsregeln nicht mehr beliebig im Inneren von Termen angewendet werden, sondern werden von Bedingungen gesteuert. Beispiel: (Fortsetzung: cor) cor(true, x) → true dadurch verhindert: cor(true, t0 ) → cor(true, t1 ) → . . . 38 KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN 4.1.3 Konstanten und Identifikatoren Alle Programmiersprachen verwenden Konstanten oder Identifikatoren. Syntax: ∗ hidi ::= hletteri {hcharacteri} hletteri ::= a|b| . . . |z hcharacteri ::= hletteri|0| . . . |9 Sei ID die Menge der Identifikatoren, d.h. der Wörter zur formalen Sprache hidi. Um Identifikatoren Werte zuzuordnen, verwenden wir Belegungen: ENV =def {β : ID → H} Interpretation von Ausdrücken: I : hexpi → (ENV → H) Für x ∈ ID: Iβ [x] = β(x) Iβ [x] ist dabei die Interpretation des Ausdrucks x (in unserem Fall des Identifikators x unter der Belegung β). 4.1.4 Bedingte Ausdrücke Ein bedingter Ausdruck hat die folgende Form: if B then E else E 0 f i B: Bedingung then E: then-Zweig else E’: else-Zweig Nebenbedingungen: • B Ausdruck der Sorte bool • E, E’ Ausdrücke der gleichen Sorte • In B, E, E’ werden Identifikatoren im Hinblick auf ihre Sorten einheitlich verwendet. hconditionalexpressioni ::= ifhexpi thenhexpi ∗ {elifhexpi thenhexpi} elsehexpi fi Beispiel: Ausdrücke, die gegen Nebenbedingungen verstoßen • if 1+2 then 5 else 7 fi (Die Bedingung muss vom Typ bool sein.) • if 1¿2 then 5 else true fi (E und E’ müssen gleiche Sorten haben.) • if x then x+1 else x+2 fi (Der Identifikator x muss einheitlich mit einer festgelegten Sorte verwendet werden.) 4.1. REIN APPLIKATIVE SPRACHEN 39 Beispiel: • if x ≥ y then x else y fi (max (x, y)) · • if x = 0 then 0 else y = x fi (Abfangen eines Sonderfalls) · • if x then y else true fi (nicht strikte Erweiterung der Implikation) Achtung: if . . . fi ist eine nicht strikte Konstruktion: Iβ [E] fallsIβ [B] = L Iβ [if B then E else E’ fi] = Iβ [E 0 ] fallsIβ [B] = 0 ⊥ fallsIβ [B] = ⊥ Gleichungen: Es gilt if true then if false then if ¬B then if ⊥then E E E E else else else else E’ E’ E’ E’ fi = E fi = E 0 fi = if B then E’ else E fi fi = ⊥ Termersetzungsregeln für if . . . fi: B → B 0 ⇒ if B then E else E’ fi → if B’ then E else E’ fi if true then E else E’ fi → E if false then E else E’ fi → E 0 Ein Ausdruck if B then E else E’ fi wird ausgewertet, indem wir zunächst B auswerten und dann, abhängig vom Ergebnis, E bzw. E’ auswerten. Warum fi? Algol-Syntax: • if B then E else E 0 • if B then E • if B1 then if B2 then E1 else E2 wie klammern? fi dient als schließende Klammer, verbessert die Lesbarkeit. Geschachtelter if-Ausdruck: 1 2 4 if x=y then E1 else if x<y then E2 else E3 fi fi Oder kürzer: 1 2 4 if x=y then E1 elif x<y then E2 else E3 fi 40 KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN 4.1.5 Funktionsapplikation Sei F ein Ausdruck, der eine Funktion bezeichnet - ein Funktionsausdruck der Sorte (S1 , . . . , sn )sn+1 . Dann ist F (E1 , . . . , En ) für beliebige Ausdrücke E1 , . . . , En der Sorten s1 , . . . , sn ein Ausdruck der Sorte Sn+1 (Funktionsapplikation). Einzige zusätzliche Nebenbedingung: Die Sorten der Identifikatoren in E1 , . . . , En und F sind konsistent. Zur Terminologie: F (E1 , . . . , En ) E1 , . . . , E n aktuelle Parameterausdrücke Werte von E1 , . . . , En aktuelle Parameter BNF: ∗ hfunction applicationi ::= hfunctioni (hexpi {, hexpi} ) Interpretation: ( f (Iβ [E1 ], . . . , Iβ [En ]) falls ∀i, 1 ≤ i ≤ n : Iβ [Ei ] 6= ⊥ Iβ [F (E1 , . . . , En )] = ⊥ sonst (strikte Funktionsanwendung) Dabei ist f die Funktion, die durch F bezeichnet wird: f = Iβ [F ]. Regel für die Funktionsauswertung (sei 1 ≤ i ≤ n): Ei → Ei0 ⇒ F (E1 , . . . , En ) → F (E1 , . . . , Ei−1 , Ei0 , Ei+1 , . . . , En ) Dies heißt: F (E1 , . . . , En ) wird ausgewertet, indem wir E1 , . . . , En auswerten. Achtung: Terminiert die Auswertung von Ei für ein i, 1 ≤ i ≤ n nicht, so terminiert die Auswertung von F (E1 , . . . , En ) nicht. Prinzip: Zuerst E1 , . . . , En auswerten, dann die Funktionsapplikation auswerten (Call-by-value, Wertaufruf von f ). Dies entspricht der Annahme, dass alle Funktionen strikt sind. 4.1.6 Funktionsabstraktion Typischerweise arbeiten wir mit Ausdrücken mit frei belegbaren Identifikatoren. Beispiel: x2 + ax + b x2 + 5x + 7 y 2 + 5y + 7 (Letztere beide sind in Darstellung und Interpretation verschieden.) Falls x bzw. y nur Platzhalter für beliebige Werte sind, ist es besser, dies kenntlich zu machen: nat nat : x2 + 5x + 7 (nat natx)nat Funktionsabstraktion (4.1) Der Ausdruck bezeichnet eine Funktion, d.h. die Abbildung, die durch x 7→ x2 + 5x + 7 ausgedrückt wird, d.h. x wird abgebildet auf x2 + 5x + 7. nat nat 4.1 ist Ausdruck für eine Funktion mit Funktionalität (nat nat)nat nat. nat nat : y 2 + 5y + 7 (nat naty)nat (4.2) 4.1. REIN APPLIKATIVE SPRACHEN 41 Die Interpretationen von 4.1 und 4.2 sind gleich. Dies erkennt man, wenn man eine Hilfsbezeichnung f für 4.1 bzw. 4.2 einführt: nat ∀nat natx : f (x) = x2 + 5x + 7 nat ∀nat naty : f (y) = y 2 + 5y + 7 Allgemeine Form der Funktionsabstraktion: (s1 x1 , . . . , sn , xn )sn+1 : E Bezeichnungen: (s1 x1 , . . . , sn , xn )sn+1 x1 , . . . , xn s1 , . . . , sn sn+1 E Kopfzeile formale Parameter Sorten der formalen Parameter Ergebnissorte Ergebnisausdruck, Rumpf Beispiel: nat nat : x · x + a · x + b (nat natx, nat nata, nat natb)nat Applikation: nat nat : x · x + a · x + b)(7, 3, 10) ((nat natx, nat nata, nat natb)nat Interpretation: Iβ [s1 x1 , . . . , sn xn )sn+1 : E] = f wobei f eine Abbildung A A f : sA 1 × . . . × sn → sn+1 A ist, mit (für a1 ∈ sA 1 , . . . , an ∈ sn ): ( Iβ 0 [E] fallsai 6= ⊥∀i, 1 ≤ i ≤ n f (a1 , . . . , an ) = ⊥ fallsai = ⊥für eini, 1 ≤ i ≤ n wobei β 0 = η[a1 /x1 , . . . , an /xn ] (ergibt die Belegung, die wir aus β erhalten, indem wir die Belegungen von x1 , . . . , xn durch a1 , . . . , an ersetzen). Achtung: Wir geben keine Auswertungsregeln für die Funktionsabstraktion, sondern nur für die Funktionsapplikation von Funktionsabstraktionen. ((s1 x1 , . . . , sn xn )sn+1 : E)(E1 , . . . , En ) → E[E1 /x1 , . . . , En /xn ] falls alle Ei terminal, d.h. in Normalform sind. Beispiel: (Fortsetzung) (x2 + ax + b)[7/x, 3/a, 10/b] = (72 + 3 · 7 + 10) Achtung: Falls wir die obige Regel in Fällen anwenden, in denen ein Argument nicht in terminaler Form ist, d.h. nicht auf seinen Wert reduziert ist, bekommen wir eine Diskrepanz zu unserer Striktheitsfestlegung. 42 KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN Beispiel: · nat nat : ifx = 0then0elsexyfi)(0, 1 − 0) ((nat natx, nat naty)nat · Sofortiges Ensetzen der Argumentausdrücke liefert: · if0 = 0then0else0 · 1 − 0fi · Dies entspricht der Call-by-name - Auffassung: Die Auswertungsregel wird angewendet, ohne dass die Parameterausdrücke ausgewertet sind. Dies ist im Gegensatz zu unserer Festlegung: Wir arbeiten stets mit Call-by-value. In einer Funktionsabstraktion (s1 x1 , . . . , sn xn )sn+1 : E sind die Identifikatoren (wie bei der Quantifizierung) gebunden. Regel für Substitution: ((s1 x1 , . . . , sn xn )sn+1 : E)[E 0 /y] = (s1 x1 , . . . , sn xn )sn+1 : (E[E 0 /y]) falls y verschieden von x1 , . . . , xn und in E 0 kommen x1 , . . . , xn nicht frei vor. ((s1 x1 , . . . , sn xn )sn+1 : E)[E 0 /xi ] = (s1 x1 , . . . , sn xn )sn+1 : E Beispiel: nat nat : x + x)[5/x] = ((nat nat nat : x + x) ((nat natx)nat natx)nat α - Konversion: Umbenennung formaler Parameter ((s1 x1 , . . . sn xn )sn+1 : E) = ((s1 y1 , . . . sn yn )sn+1 : E[y1 /x1 , . . . , yn /xn ]) falls y1 , . . . , yn in E nicht frei vorkommen. Analyse der Bezeichnerstruktur eines applikativen Programms: (1) Kennzeichnen der freien Identifikatoren (2) Zuordnen der gebundenen Identifikatoren zu ihren Bindungen (3) Zuordnung der aktuellen Parameterausdrücke zu den formalen Parametern BNF: Funktionsabstraktion ∗ hfunctioni ::= hidentifier | (hsortihidentifieri {, hsortihidentifieri} )hsorti : hexpi Nebenbedingung: Die formalen Parameter werden sortenkonform im Rumpf verwendet. Bemerkung: Die Funktionsabstraktion orientiert sich am λ-Kalkül von Alonso Church: Es werden keine Sorten geschrieben und nur einstellige Funktionen betrachtet. Statt nat nat : x + 1 schreibt man im λ-Kalkül: λx : x + 1. (nat natx)nat Funktionsabstraktion (bzw. λ-Notation) bietet uns die Möglichkeit, Ausdrücke zu schreiben, deren Werte Funktionen sind. Bemerkung: Es gibt einen engen Zusammenhang zur Schreibweise der Mengenlehre · · · · nat bool : x > 6 ∧ x − 8 < 2000. x ∈ N : x > 6 ∧ x − 8 < 2000 : (nat natx)bool Durch die Kopfzeile einer Funktionsabstraktion entsteht eine Bindung für formale Parameter, der Rumpf entspricht dem Bindungsbereich, wir sprechen von der 4.2. DEKLARATIONEN IN APPLIKATIVEN SPRACHEN 43 Lebensdauer der Bindung (bzw. der Bezeichnung). Die Bindung ist für die Lebensdauer gültig, unterbrochen nur durch weitere Bindungen des gleichen Identifikators. nat nat : (nat natx)nat ? bool nat : if x then y · y else y + y fi) if x = 0 then ((bool boolx, nat naty)nat (0 < x, 2x) else 3xfi)(5) Stets ist höchstens eine Bindung gültig. Die Lebensdauern können sich überlagern. 4.2 Deklarationen in applikativen / funktionalen Sprachen Beispiel: nat nat : 3x3 + 5x2 + 7x + 3)((a + b) − c) ((nat natx)nat bequemer: nat pnat natx = a + b − c 3x3 + 5x2 + 7x + 3y Deklarationen dienen einer Bindung eines Identifikators mit gleichzeitiger Wertbelegung. Dies ist oft lesbarer als die Funktionsabstraktion und -applikation. Deklarationen verwenden wir in Abschnitten (Blöcken); die Abschnittsklammern begrenzen den Bindungsbereich. Syntax: hsectioni ::=phinneriy ∗ |ifhinnerithenhinneri {elifhinnerithenhinneri} elsehinnerifi hinneri ::=hsort declarationi; hexpi |helement declarationi; hexpi ∗ | {hfunction declarationi} ; hexpi |hprocedural inneri 4.2.1 Elementdeklaration Allgemeine Form: sx = E Elementdeklaration / -vereinbarung Nebenbedingungen: (1) Der Identifikator x kommt im Ausdruck E nicht frei vor. (2) E hat die Sorte s . Beispiel: Elementdeklaration seqnat pseqnat seqnatx = h1 3 7i; x ◦ xy hat Wert h1 3 7 1 3 7i if bool boolb = true; b ⇒ ¬b then nat natz = 7; z · z else 23 fi Auch Schachtelungen sind erlaubt: nat nat nat pnat natz = pnat natz = 3; 3 · z − zy; pnat natz = 5; z · zy + zy 44 KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN Interpretation: I[ssx = E] : EN V → EN V ( β[Iβ [E]/x] falls Iβ [E] 6= ⊥ I[ssx = E](β) = Ω sonst Ω ∈ EN V : Ω(z) = ⊥ ∀z ( II[D](β) [E] falls I[D](β) 6= Ω Iβ [pD; Ey] = ⊥ sonst Auswertung von Abschnitten: pssx = E; E 0 y → ((ssx)ss0 : E 0 )(E) 4.2.2 Funktionsdeklaration Ebenso wie Elemente deklarieren wir Funktionen: nat nat : if x ≥ y then y else x fi f ct ctmin = (nat natx, nat naty)nat Allgemeine Form: f ct ctf = (s1 x1 , . . . , sn xn )sn+1 : E (4.3) Syntax: hfunction declarationi ::= f ct cthidi = hfunction abstractioni Achtung: In (4.3) darf f frei vorkommen, als Funktionsidentifikator für eine Funktion der Sorte (s1 x1 , . . . , sn xn )sn+1 . In diesem Fall heißt die Deklaration rekursiv. Regel zur Auswertung: pff ct ctf = F ; Ey → E[F/f ] Regel für den rekursiven Fall: siehe später. 4.3 Rekursive Funktionsdeklarationen Rekursive Funktionsdeklarationen bilden das Kernstück der funktionalen Programmierung. Typischerweise werden Programmieraufgaben wie folgt formuliert: Gib eine Funktion(sdeklaration) an, die für folgende Argumente . . . einen Wert berechnet, der folgende Eigenschaften hat: . . . . Beispiel: Rekursive Funktionen (1) Umrechnen einer Binärzahl in ihren Wert (Binärzahldarstellung: seqbit seqbit): 1 2 4 fct calcval = (seq bit x) nat: if x ?= empty then 0 elif last(x) ?= L then 1 + 2*calcval(lrest(x)) else 2*calcval(lrest(x)) fi (2) Revertieren einer Sequenz: Eine gegebene Sequenz soll in umgekehrte Reihenfolge gebracht werden. 4.3. REKURSIVE FUNKTIONSDEKLARATIONEN 1 2 4 45 fct revertiere = (seq data s) seq data: if s ?= empty then empty else conc(<last(s)>, revertiere(lrest(s))) fi (3) Feststellen, ob zwei Sequenzen über einer linear geordneten Menge in der alphabetischen Ordnung stehen. (Sei data eine Menge von Elementen mit linearer Ordnung ≤.) 1 2 4 6 fct isalpha = (seq data s, seq data t) bool: if s ?= empty then true elif t ?= empty then false elif first(s) < first(t) then true elif first(s) > first(t) then false else isalpha (rest(s), rest(t)) fi (4) Sortieren einer Sequenz durch Mischen: Wir betrachten Sequenzen von natürlichen Zahlen. Eine Sequenz heißt absteigend sortiert, wenn sorted(s) gilt, wobei: 1 2 4 fct sorted = (seq nat s) bool: if length(s) <= 1 then true else first(s) >= first(rest(s)) and sorted (rest(s)) fi Mischen zweier sortierter Sequenzen: 1 2 4 6 fct merge = (seq nat s, seq nat t) seq nat: if s ?= empty then t elif t ?= empty then s elif first(s) >= first(t) then conc(<first(s)>, merge(rest(s), t)) else conc (<first(t)>, merge(s, rest(t))) fi Behauptung: sorted(s) ∧ sorted(t) ⇒ sorted(merge(s, t)) Sortieren durch Mischen: 1 2 4 6 fct mergesort = (seq nat s) seq nat: if length(s) <= 1 then s else nat h = length(s) / 2; seq nat lpart = part(s, 1, h); seq nat rpart = part(s, h+1, length(s)); merge (mergesort(lpart), mergesort(rpart)) fi 8 10 12 fct part = (seq nat s, nat a, nat b) seq nat: if a > 1 then part (rest(s), a-1, b-1) elif a ?= b then <first(s)> else conc (<first(s)>, part(rest(s), 1, b-1)) fi Bemerkungen: 46 KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN (1) Es gilt sorted(mergesort(s)) für jede Sequenz s von natürlichen Zahlen. (2) Rufen wir part(s, a, b) mit Werten für s, a, b auf, so dass 1 ≤ a ≤ b ≤ length(s) nicht gilt, dann wird bei der Auswertung des Aufrufs irgendwann eine Operation auf ein Argument angewendet, bei der das Ergebnis bot, d.h. nicht definiert ist (Bsp. 0 − 1, rest(empty)). DAnn ist das Ergebnis von part(s, a, b) ebenfalls undefiniert. (Laufzeitfehler) Wir wenden uns nun der genaueren Deutung rekursiver Deklarationen zu. 1 2 4 fct mod = (nat a, nat b) nat: if a < b then a else nod (a-b, b) fi Auswertung durch Termersetzung: unfold if-Regeln unfold if-Regeln unfold if-Regeln mod(29, 9) −−−−→ if 29 < 9 then 29 else mod(29 − 9, 9) fi −−−−−−→ mod(20, 9) −−−−→ if 20 < 9 then 20 else mod(20 − 9, 9) fi −−−−−−→ mod(11, 9) −−−−→ if 11 < 9 then 11 else mod(11 − 9, 9) fi −−−−−−→ unfold mod(2, 9) −−−−→ if 2 < 9 then 2 else mod(2, 9) fi if-Regeln −−−−−−→ 2 Neben der Auswertung durch Termersetzung können wir die Bedeutung rekursiver Funktionsdeklarationen auch durch • Induktion • Fixpunktfestlegung beschreiben. mx)n n:E f ct ctf = (m (4.4) Idee: Dies entspricht einer Gleichung f (x) = E (Fixpunktgleichung). Wir verbinden mit der Deklaration (4.4) eine (die?) Funktion, die die Fixpunktgleichung erfüllt. Offene Fragen: (1) Gibt es immer eine Lösung für die Fixpunktgleichung? (2) Gibt es manchmal mehrere Lösungen, und wenn ja, welche verbinden wir dann mit (4.4)? Beispiel: (paradox) nat nat : f1 (x) f ct ctf1 = (nat natx)nat nat nat : f2 (x) + 1 f ct ctf2 = (nat natx)nat Gleichungen: f1 (x) = f1 (x) f2 (x) = f2 (x) + 1 4.3. REKURSIVE FUNKTIONSDEKLARATIONEN 4.3.1 47 Induktive Deutung rekursiver Funktionsdeklarationen Idee: Wir approximieren für die rekursive Funktionsdeklaration mx)n n:E f ct ctf = (m (4.5) die deklarierte Funktion f induktiv, d.h. wir konstruieren eine Folge von Funktionen f0 , f1 , f2 , . . ., deren Grenzwert f darstellt. Mit (4.5) verbinden wir ein Funktional: Sei M die Trägermenge zur Sorte m , N die Trägermenge zur Sorte n (jeweils inklusive ⊥). τ : (M → N ) → (M → N ) mit (für g : M → N ): τ [g] : M → N definiert durch (sei a ∈ M ) ( Iβ 0 [E] falls a 6= ⊥ τ [g](a) = ⊥ sonst Sei dabei β eine gegebene Belegung (die Belegung, in deren Kontext wir (4.5) betrachten) und β 0 = β[g/f, a/x] Beispiel: 1 2 4 fct fac = (nat x) nat: if x ?= 0 then 1 else x * fac (x-1) fi Wir definieren nun induktiv: f0 (a) = ⊥ fi+1 = τ [fi ] Beispiel: (Fortsetzung) a 0 1 2 3 4 5 fac0 (a) ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ fac1 (a) 1 ⊥ ⊥ ⊥ ⊥ ⊥ fac2 (a) 1 1 ⊥ ⊥ ⊥ ⊥ fac3 (a) 1 1 2 ⊥ ⊥ ⊥ fac4 (a) 1 1 2 6 ⊥ ⊥ ( a! falls a < i faci (a) = ⊥ sonst (Induktive Folge) 48 KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN Beobachtung: Gilt fi (a) = b, b 6= ⊥ ⇒ ∀j ∈ N : j ≥ i : fj (a) = b. Grenzwert: ( fi (a) falls ∃i ∈ N : fi (a) 6= ⊥ f∞ (a) = ⊥ sonst Wir können auch eine partielle Ordnung auf der Menge der Funktionen einführen: f v g ⇔ ∀a ∈ M : f (a) = g(a) ∨ f (a) = ⊥ Idee: f v g heißt f approximiert g , d.h. hat für a ∈ M f (a) einen Wert b = 6 ⊥, so gilt g(a) = b, d.h. g hat für a den gleichen Wert. Gild f (a) = ⊥, dann kann g(a) = ⊥ gelten, oder auch nicht. f approximiert die Information, die g trägt. Aufgrund unserer Konstruktion gilt fi v fi+1 ∀i ∈ N Die überall undefinierte Funktion Ω:M →N mit Ω(a) = ⊥∀a ∈ M ist kleinstes Element, d.h. Ω v g∀g : M → N Allgemeine Definition zur Ordnungstheorie: Sei (M, v) eine partiell geordnete Menge. Für jede Teilmenge M 0 ⊆ M heißt ein Element x ∈ M kleinste obere Schranke von M 0 , falls gilt: (1) x ist obere Schranke von M 0 , d.h. ∀y ∈ M 0 : y v x (2) x ist kleinste obere Schranke, d.h. für jede obere Schranke z ∈ M gilt x ⊆ z. Achtung: Nicht für jede Menge M 0 ⊆ M existiert solch eine kleinste obere Schranke. Falls die kleinste obere Schranke existiert, schreiben wir lub(M 0 ) (least upper bound) Eine Kette x0 v x1 v x2 v . . . ist eine Menge {xi ∈ M : i ∈ N} mit xi v xi+1 ∀i ∈ N Eine Menge heißt kettenvollständig (auch vollständig partiell geordnet), wenn jede Kette eine kleinste obere Schranke besitzt. Anmerkung: Die oben definierten Funktionen fi bilden eine Kette mit f∞ = lub {fi : i ∈ N} Sei tau eine Abbildung auf geordneten Mengen. τ heißt monoton, wenn gilt: x1 v x2 ⇒ τ (x1 ) v τ (x2 ) Anmerkung: In der obigen Konstruktion f0 = Ω fi+1 = τ (fi ) ist τ monoton: fi v fi+1 Beweis durch Induktion und Monotonie: fi v fi+1 f0 = Ω v f1 ⇒ τ [fi ] v τ [fi+1 ] = fi+1 v fi+2 4.4. REKURSIONSFORMEN 4.3.2 49 Deutung rekursiv deklarierter Funktionen durch kleinste Fixpunkte Seien alle Definitionen wie oben. Satz von Knaster-Tarski: Sei (M, v) partiell geordnete Menge, kettenvollständig mit kleinstem Element; sei τ : M → M monoton. Dann existiert ein kleinster Fixpunkt, d.h. eine kleinste Lösung der Gleichung x = τ [x]. Genauer: (1) Es existiert eine Lösung der Gleichung x = τ [x]. (2) Es existiert eine kleinste Lösung x0 , d.h. x0 = τ [x0 ] und x = τ [x] ⇒ x0 v x. x0 heißt kleinster Fixpunkt von τ . Wir bezeichnen x0 auch mit fix τ . Im Fall rekursiv definierter Funktionen gilt: fix τ = lub {fi : i ∈ N} Beispiel: Die Fakultätsfunktion ist Fixpunkt von τ für ( 1 fallx a = 0 τ [g](x) = g(a − 1) · a sonst d.h. a! = if a = 0 then 1 else (a − 1)!a fi 4.4 Rekursionsformen Die Art und Weise, wie im Rumpf rekursiv definierter Funktionen rekursive Aufrufe auftreten, ist für die Klassifizierung von Rekursion von Bedeutung. 4.4.1 Lineare Rekursion Wir sprechen von linearer Rekursion, wenn jeder Aufruf im Rumpf zu höchstens einem weiteren rekursiven Aufruf führt. Dies gilt insbesondere, wenn im Rumpf genau ein Aufruf der rekursiv definierten Funktion auftritt, bzw. wenn in jedem Zweig der if - Anweisung im Rumpf höchstens ein Aufruf vorkommt. Wir erhalten eine lineare Folge von Aufrufen. Beispiel: • Fakultät fib(4) | fib(3) | fib(2) | fib(1) | fib(0) 50 KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN • Aussortieren von Elementen aus einer Sequenz (von natürlichen Zahlen), die kleiner sind als ein gegebenes Element: 1 2 4 fct lp if s elif else fi = (seq nat s, nat a) seq nat: ?= empty then empty first(s) < a then conc (<first(s)>, lp(rest(s))) lp(rest(s)) (Analog: hp (alle Elemente, die größer sind als das gegebene Element) und ep (Elemente gleich dem gegebenen Element)) 4.4.2 Repetitive Rekursion Eine lineare Rekursion heißt repetitiv, wenn der Rumpf folgende Form hat: 1 2 4 6 fct f = (m1 x1, ..., mn xn) m: if B1 then ... elif B2 then f(E1, ..., En) ... else ... fi Dabei stellen E1, . . . , En keine rekursiven Aufrufe dar. Genauer: Alle rekursiven Aufrufe sind von einer Form, dass auf das Ergebnis keine weiteren Operationen mehr angewendet werdne. Beispiel: Repetitive Fakultätsberechnung 1 2 4 fct repfac = (nat x, nat y) nat: if x ?= 0 then y else repfac (x-1, y*x) fi Wichtig: Repetitive Rekursion benötigt keine Stapel von Zwischenergebnissen. 4.4.3 Kaskadenartige Rekursion Bei nichtlinearer Rekursion tritt in gewissen Zweigen mehr als ein rekursiver Aufruf auf. 1. Fall: Die Aufrufe sind unabhängig voneinander, d.h. keiner der Aufrufe tritt in den aktuellen Parameterausdrücken eines anderen rekursiven Aufrufs auf (→ Kaskadenrekursion). 2. Fall: Ein rekursiver Aufruf ist im aktuellen Parameterausdruck eines anderen rekursiven Aufrufs. Beispiel: Quicksort Idee: absteigend sortieren durch Aufspalten 4.4. REKURSIONSFORMEN 1 2 4 51 fct qs = (seq nat s) seq nat: if length(s) <= 1 then s else nat x = first (s); conc (qs(hp(s, x)), conc (ep(s, x), qs(lp(s, x)))) fi Dieser Algorithmus erzeugt eine Kaskade von Aufrufen (einen sogenannqs ten Aufrufbaum). qs(hp. . . ) qs(lp. . . ) qs(hp. . . ) qs(lp. . . ) qs(hp. . . ) qs(lp. . . ) 4.4.4 Vernestete Rekursion Beispiel: Ackermann 1 2 4 fct ackermann = (nat m, nat n) nat: if m ?= 0 then n+1 elif n ?= 0 then ackermann(m-1, 1) else ackermann(m-1, ackermann(m, n-1)) fi ack : N × N → N falls m = 0 n + 1 ack(m, n) = ack(m − 1, n) falls n = 0 ∧ m 6= 0 ack(m − 1, ack(m, n − 1)) sonst Die Anzahl der rekursiven Aufrufe hängt empfindlich von m ab. Bemerkung: Die Ackermannfunktion ist ein wichtiges Beispiel bei der Diskussion der Rekursion. Spezialfälle Bm (n) : N → N Bm (n) = ack(m, n) Es gilt: B0 (n) = n + 1 B1 (n) = n + 2 B2 (n) = 2n + 3 (Nachfolgerfunktion) (Addition von 2) (Multiplikation) B3 (n) = 2n+3 − 3 B4 (n) = . . . (Exponentialfunktion) (keine einfache math. Beschreibung) Bemerkung: Aus praktischer Sicht ist die Bedeutung der vernesteten Rekursion gering. 52 KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN 4.4.5 Verschränkte Rekursion Gegeben sei ein System rekursiver Funktionsdeklarationen: f ct ctf1 = (m1 x1 )n1 : E1 .. . f ct ctfk = (mk xk )nk : Ek Treten in E1 , . . . , Ek die Funktionsbezeichner f1 , . . . , fk (k ≥ 2) auf, so spricht mann von verschränkter Rekursion. Beispiel: Teilbarkeit einer Zahl durch 3 Funktionsaufruf: rest0(n). 1 2 4 6 8 fct rest0 = (nat x) bool: if n ?= 0 then true else rest1(n-1) fi fct rest1 = (nat x) bool: if n ?= 0 then false else rest2(n-1) fi 10 12 14 fct rest2 = (nat x) bool: if n ?= 0 then false else rest0(n-1) fi Stützgraph: graphisches Hilfsmittel zur Beschreibung der im Rumpf einer rekursiven Funktion auftretenden Funktionen. 4.5 4.5.1 Techniken applikativer Programmierung Spezifikation der Aufgabenstellung Erstes Prinzip der disziplinierten Programmierung Niemals sollte mit der eigentlichen Programmierung begonnen werden, bevor die Aufgabenstellung genau verstanden und beschrieben ist. Erläuterung: Eine formale, mathematische Beschreibung einer Aufgabenstellung nennt man formale Spezifikation. Diese Anforderungsspezifikation ist im Idealfall mit einer formalen Sprache formuliert. Vorgehensweise bei formaler Spezifikation (1) Spezifikation des Funktionsraumes und Parameterbeschränkung. • mathematisch: f : M1 × . . . × Mn → Mn+1 ∀x1 ∈ M1 , . . . , xn ∈ Mn : ¬Q (x1 , . . . , xn ) ⇒ f (x1 , . . . , xn ) = ⊥ 4.5. TECHNIKEN APPLIKATIVER PROGRAMMIERUNG 53 • in Pseudocode: f ct f = (m1 x1 , . . . , mn xn : Q(x1 , . . . , xn ))mn+1 (2) Spezifikation des Werteverlaufs. • mathematisch: f : M1 × . . . × Mn → Mn+1 ∀x1 ∈ M1 , . . . , xn ∈ Mn : ¬Q(x1 , . . . , xn ) ⇒ f (x1 , . . . , xn ) = ⊥ ∀x1 ∈ M1 , . . . , xn ∈ Mn : Q(x1 , . . . , xn ) ⇒ R(x1 , . . . , xn , f (x1 , . . . , xn )) • in Pseudocode: f ct f = (m1 x1 , . . . , mn xn : Q(x1 , . . . , xn ))mn+1 : some mn+1 y : R(x1 , . . . , xn , y) Beispiel: ganzzahlige Division (1) Spezifikation des Funktionsraumes und Beschränkung der Parameter. • mathematisch: div : N × N → N ∀x ∈ N, y ∈ N : ¬(y > 0) ⇒ div(x, y) = ⊥ • in Pseudocode: nat f ct div = (nat natx, nat naty : y > 0)nat (2) Spezifikation des Werteverlaufs. • mathematisch: ∀x ∈ N, y ∈ N : (y > 0) ⇒ ∃r ∈ N : x = y · div(x, y) + r ∧ y > r ≥ 0 • in Pseudocode: nat f ct div = (nat natx, nat naty : y > 0)nat nat r : x = y · z + r ∧ y > r ≥ 0 some nat z : ∃nat Definition: Korrektheit Eine Rechenvorschrift heißt partiell korrekt bezüglich einer Aufgabenstellung, wenn sie für alle zulässigen Argumente, für die sie terminiert, ein bezüglich der Aufgabenstellung korrektes Resultat liefert. Eine Rechenvorschrift heißt total korrekt, wenn sie partiell korrekt ist und zusätzlich in allen durch die Aufgabenstellung geforderten Fälle terminiert. Bemerkungen: • Die i-te Iterierte der Fakultätsfunktion ist partiell korrekt, denn faci berechnet die Fakultätsfunktion für n < i; für n ≥ i ist faci (n) = ⊥. • faci ist nicht total korrekt. • Eine total korrekte Rechenvorschrift kann für Argumente, die bezüglich der Aufgabenstellung nicht terminieren, dennoch ein definiertes Resultat liefern. 54 KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN • Zusammenfassung: fpart v fspec v ftot Wichtig: Systematisches Vorgehen bei der Entwicklung von Programmen! Fragen Entwicklungsphase Welche Aufgabe ist zu lösen? Anforderungsspezifikation Welche Lösungsidee, wie strukturieren? Entwurf / Design Wie im Detail ausprogrammieren? Implementierung Löst das Programm die Aufgabe? Verifikation der Korrektheit Wie effizient ist das Programm? Performance / Komplexität 4.5.2 Strukturierung und Entwurf Liegt eine Spezifikation vor, d.h. eine hinreichend genaue Beschreibung einer zu implementierenden Funktion, so ist der nächste Schritt, zu überlegen, wie die Aufgabe implementiert werden soll: (1) Direkte Angabe einer Funktionsdeklaration (u.U. mit Rekursion). (Suche nach Algorithmus) (2) Zergliederung der Aufgabe in Teilaufgaben. (divide and conquer) (z.B. Quicksort) (3) Verallgemeinerung der Aufgabenstellung. Grund: (a) verallgemeinerte Aufgabe einfacher oder effizienter zu lösen (b) Ergebnis öfter einsetzbar Beispiel: Fibonacci 1 2 4 fct fib = (nat n) nat: if n <= 1 then n else fib (n-2) + fib (n-1) fi Aufrufbaum von fib: fib(n) fib(n − 2) fib(n − 1) fib(n − 3) fib(n − 2) fib(n − 5) fib(n − 4) fib(n − 4) fib(n − 3) Der selbe Funktionsaufruf kommt öfter vor. Verallgemeinerung: fibe(n, 1, 0) = fib(n + 1) 1 2 4 fct fibe = (nat n, nat a, nat b) nat: if n ?= 0 then a else fibe (n-1, a+b, a) fi offen: Beweis von (4.6). (4.6) 4.5. TECHNIKEN APPLIKATIVER PROGRAMMIERUNG 55 Beispiel: Sortieren durch Einbettung Sortieren duch Einsortieren: 1 2 4 6 8 10 12 fct insertsort = (seq nat s) seq nat: insertseq (empty, s) fct insertseq = (seq nat s, seq nat r: sorted(s)): seq nat: if r ?= empty then s else insertseq (insert (s, first(r)), rest(r)) fi fct insert = (seq nat s, nat x) seq nat: if s ?= empty then <x> elif x >= first(s) then conc (<x>, s) else conc (first(s), insert (rest(s), x)) fi Sortieren durch Auswählen: 1 2 4 6 fct selectsort = (seq nat s) seq nat: appsortseq (empty, s) fct appsortseq = (seq nat s, seq nat r: sortes(s)) seq nat: if r ?= empty then s else nat max = selmax(r); appsortseq (conc (s, <max>), del(r, max)) fi 8 10 12 14 fct selmax = (seq nat s: s <> empty) nat: if length(s) = 1 then first(s) else nat max = selmax(rest(s)); if max >= first(s) then max else first(s) fi fi 16 18 20 fct del = (seq nat r, nat x) seq nat: if r ?= empty then empty elif first(r) = x then rest(r) else conc (<first(r)>, del (rest, r)) fi 4.5.3 Hinweise zur Strukturierung der Aufgabenstellung und der Lösung Prinzipien: (1) Einführung geeigneter Hilfsfunktionen und Begriffe (auch schon in der Spezifikation). (2) Hilfsfunktionen sollten für sich eigenständig zu verstehen sein. (3) Die Rümpfe der Funktionen sollen von überschaubarer Größe sein (max Seiten). (4) Klare Gesamtstruktur des Programms (Architektur). 1 2 −1 56 KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN Beispiel: Hilfsfunktionen und Begriffe sort(s) = r ⇔ s ∼ r ∧ sorted(r) s ∼ r: sorted(r): s und r haben die gleichen Elemente r ist sortiert Ziele: • Lesbarkeit • Dokumentation (Erläuterungen einfügen) • Unabhängigkeit der Lösung der Teilaufgaben (Schnittstellen-Beschreibung der Teilaufgaben) • Performance • Sicherstellung der Korrektheit 4.5.4 Rekursion und Spezifikation Neben der Zerlegung von Aufgaben in Teilaufgaben ist die Ableitung von rekursiven Deklarationen die Hauptaufgabe in der Implementierung. Hier hilft es oft, geeignete Gleichungen abzuleiten, die dann die Grundlage für die Rekursion bilden. 4.5.5 Parameterunterdrückung Verändern sich bestimmte Parameter in der Rekursion nicht, so kann ich eine Hilfsfunktion angeben, die diese Parameter nicht enthält, sondern als globale Identifikatoren nutzt. Ausbesserung der Funktion del: 1 2 4 6 fct del = (seq nat r, nat x) seq nat: | fct h = (seq nat r) seq nat: if r ?= empty then empty elif first(r) ?= x then rest(r) else conc (<first(r)>, h (rest(r))) fi h(r) _| 4.5.6 Effizienz Definition: Effizienz eines Programms / Algorithmus Unter der Effizienz eines Programms verstehen wir den Aufwand, der bei der Ausführung des Programms erforderlich ist, im Verhältnis zur Aufgabenstellung. Wir messen für funktionale Programme den Aufwand wie folgt: • Speicheraufwand: wie viele Hilfsgrößen müssen gleichzeitig gespeichert werden? • Rechenaufwand: Anzahl der erforderlichen Rechenoperationen • Rechenzeit (Antwortzeit) Um diese allgemeinen Definitionen besser greifen zu können, führen wir Funktionen 4.5. TECHNIKEN APPLIKATIVER PROGRAMMIERUNG 57 ein, die den jeweiligen Aufwand messen. Sei die Aufgabe berechne fs : N → N gegeben. Lösung (Rekursion): nat x)nat nat : . . . f ct f = (nat (4.7) Wir definieren drei Gewichtsfunktionen: (1) fa : N → N (Berechnungsaufwand) fa (n) entspricht der Anzahl der Rechenoperationen bei der Auswertung des Aufrufs f (n) nach (4.7). (2) fb : N → N (Speicheraufwand) fb (n) bezeichnet die maximale Anzahl geschachtelter Bindungen bei der Auswertung des Aufrufs f (n) nach (4.7). (3) fc : N → N fc (n) bezeichnet die Rekursionstiefe, d.h. die Höhe des Aufrufbaums für f (n) nach (4.7). Beispiel: fibe(10, 1, 0) Rechenaufwand: Rechenoperationen: ? n=0 n−1 n+b fibea (10, 1, 0) ≈ 30 fibea (n, a, b) ≈ 3 · n Seien f1 und f2 rekursiv definierte Funktionen, die die selbe Aufgabenstellung lösen. Wir sagen f1 ist effizienter alls f2 , wenn gilt: f1 a (x) f2 a (x) ∧ f1 b (x) f2 b (x) Beispiel: (Fortsetzung) 1 2 4 fct fib_a = (nat n) nat: if n <= 1 then 1 else 1 + fib_a (n-1) + fib_b (n-2) fi Frage: Von welcher Ordnung ist der Berechnungsaufwand? Wie stark wächst der Aufwand mit wachsendem Parameter n? Wir sagen, eine Funktion f : N → N wächst mit Ordnung g : N → N, falls ∃k, i, j ∈ N, i > 0, k > 0: ∀n ∈ N : j ≤ n ⇒ g(n) ≤ i · f (n) ≤ k · g(n) Notation: f (n) ≈ O(g(x)). 58 KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN Beispiel: fiba (n) = O(2n ) fibea (n) = O(n) exponentiell linear Speicheraufwand: 1 2 4 fct fib_b = (nat n) nat: if n <= 1 then 1 else 1 + max (fib_b (n-2), fib_b (n-1)) fi Es gilt fibb (n) = O(n). Rekursionstiefe: fibc = fibb . Die Effizienz von Algorithmen wird in der Informatik unter den Stichworten Komplexität und Komplexitätstheorie analysiert. 4.5.7 Dokumentation Die Qualität eines Programms wird nicht nur durch seine Korrektheit (Programm liefert korrekte Ergebnisse) und seine Effizienz bestimmt, sondern auch durch seine Lesbarkeit, Verständlichkeit und Änderbarkeit. Entscheidend dabei: Dokumentation 4.5.8 Test / Integration von Programmen Programmieren ist fehleranfällig. Folglich sind fertiggestellte Programme sorgfältig auf Fehler zu überprüfen. Dazu bieten sich folgende Verfahren an: • Inspektion: Durchsicht von Programmen auf Fehler • Review: Durchsprache von Programmen im Team • Testen: Probeläufe von Programmen für ausgewählte Eingabewerte (sog. Testfälle) • Verifikation: Nachweis der Korrektheit mit logischen Mitteln Integration: Die Aufgabe des Zusammensetzens größerer Programme aus kleineren Programmeinheiten. 4.6 Korrektheitsbeweise Funktionale Programme entsprechen mathematischen Deklarationen und können direkt mit Mitteln der Logik und Mathematik analysiert werden. Beispiel: Fibonacci n ≤ 1 ⇒ fib(n) = n n > 1 ⇒ fib(n) = fib(n − 1) + fib(n − 2) 4.6. KORREKTHEITSBEWEISE 59 m x)n n : E entspricht der Gleichung f (x) = E Eine rekursive Deklaration f ct f = (m (Fixpunktgleichung) für alle x der Sorte m , x 6= ⊥. Ferner: mit kleinste Fixpunkt - Eigenschaft f0 (x) = ⊥ fi+1 (x) = E[fi /f ] gilt f (x) = ⊥ ⇔ ∀i ∈ N : fi (x) = ⊥ 4.6.1 Induktion und Rekursion Induktion und Rekursion sind eng verwandt. Viele Rekursionen entsprechen einem induktiven Prinzip. Um Beweise über rekursiv definierte Funktionen führen zu können, verwenden wir oft erfolgreich Induktion. Beispiel: Beweis: 2n ≤ fib(2n + 1) ≤ 22n+1 Induktionsanfang: n = 0 20 ≤ fib(1) ≤ 21 1≤1≤2 Induktionsannahme: 2n ≤ fib(2n + 1) ≤ 22n+1 Induktionsschritt (exemplarisch nur für 2n ≤ fib(2n + 1)) 2n+1 ≤ 2 · fib(2n + 1) ≤ fib(2n + 1) + fib(2n + 2) = fib(2n + 3) = fib (2 (n + 1) + 1) Mit Hilfe von Rekursion können wir insbesondere beweisen, dass (1) die rekursiv definierte Funktion f einer spezifizierten Funktion fs entspricht, d.h. f = fs bzw. ∀x : f (x) = fs (x) (Korrektheit). (2) Partielle Korrektheit: f v fs bzw. ∀x : f (x) v fs (x) Beispiel: 1 2 4 fct rev = (seq m x) seq m: if length(x) <= 1 then x else conc (<last(x)>, conc (rest(lrest(x)), <first(x)>)) fi Behauptung: rev(hx1 . . . xn i) = hxn . . . x1 i Beweis: Die Rekursion liefert length(x) ≤ 1 ⇒ rev(x) = x length(x) > 1 ⇒ rev(x) = hlast(x)i ◦ rev(rest(lrest(x))) ◦ hfirst(x)i n ≥ 2 ⇒ rev(hx1 . . . xn i) = hxn i ◦ rev(hx2 . . . xn−1 i) ◦ hx1 i 60 KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN Induktion über n = length(x): n ≤ 1: trivial Induktionshypothese: rev(hx1 . . . x0n i) = hx0n . . . x1 i Induktionsschritt: rev(hz1 . . . zn+1 i) = hzn+1 i ◦ rev(hz2 . . . zn i) ◦ hz1 i = hzn+1 i ◦ hzn . . . z2 i ◦ hz1 i = hzn+1 zn . . . z2 z1 i Zur Effizienz von rev: (Achtung: rev arbeitet auf Sequenzen, nicht auf Zahlen!) 1 2 4 fct rev_a = (seq m x) nat: if length(x) <= 1 then 1 else 6 + rev_a (rest (lrest(x))) fi reva (x) ≈ 3 length(x) Rechenaufwand: Linear in Länge der Sequenz Beispiel: log 1 2 4 fct log = (nat n) nat: if n ?= 1 then 0 else log (n / 2) + 1 fi n = 1 ⇒ log(n) = 0 n 6= 1 ⇒ log(n) = log(n/2) + 1 Behauptung: ⊥ falls n = 0 log(n) = 0 falls n = 1 k für 2k ≤ n < 2k+1 Beweis: Induktion ∀i ∈ N : logi (n) = ⊥, wobei log0 (n) = ⊥ logi+1 (n) = if n = 1 then 0 else logi (n/2) + 1 f i Wir zeigen: logi (0) = ⊥ durch Induktion über i ∈ N. Beweis: trivial Der Rest der Behauptung folgt durch Induktion über die Fixpunktgleichung 4.6.2 Terminierungsbeweise Gegeben: Rekursive Deklaration m x)n n:E f ct f = (m Sei P ein Prädikat auf den Elementen der Sorte m . Wir wollen folgende Aussage beweisen: P (x) ⇒ f (x) 6= ⊥ d.h. für alle x, für die P (x) gilt, terminiert die Rekursion für f . 4.6. KORREKTHEITSBEWEISE 61 Beispiel: 1 2 4 fct f = (nat a, nat b) nat: if a < b then b else f (a, 2b) fi Frage: Für welche Parameterwerte terminiert f ? f terminiert für Werte b > 0. Gesucht: Beweis von b > 0 ⇒ f (a, b) 6= ⊥ Prinzip: Verwendung einer Abstiegsfunktion Wir suchen eine Funktion h : M− → N Abstiegsfunktion wobei M − die Menge der Elemente zur Sorte m ist (ohne ⊥). h sei eine Abschätzung für die Höhe des Aufrufbaums. Wir beweisen durch Induktion über k ∈ N: P (x) ∧ h(x) ≤ k ⇒ f (x) 6= ⊥ Induktionsanfang (k = 0): P (x) ∧ h(x) = 0 ⇒ f (x) 6= ⊥ Induktionsvoraussetzung: P (x) ∧ h(x) ≤ k ⇒ f (x) 6= ⊥ Induktionsschritt: zeige P (x) ∧ h(x) ≤ k + 1 ⇒ f (x) 6= ⊥ (4.8) Durchführung des Beweises: Wir wissen: f (x) = E. Um (4.8) zu zeigen, zeigen wir P (x) ∧ h(x) ≤ k + 1 ⇒ E 6= ⊥ Wenn wir h so wählen, dass alle Aufrufe in E eine Bewertung haben, die kleiner als k + 1 ist, so können wir die Induktionsannahme einsetzen, d.h. ist f (E0 ) ein Aufruf in E, der unter der Bedingung C stattfindet, so ist zu zeigen: C ⇒ h(E0 ) < h(x) h(E0 ) < h(x) ≤ k + 1 h(E0 ) ≤ k Beispiel: (Fortsetzung) Gesucht: h : N × N → N mit b > 0 ∧ a ≥ b ⇒ h(a, 2b) < h(a, b) a < b ⇐ h(a, b) = 0 ( 0 falls a < b h(a, b) = a + 1 − b falls a ≥ b a + 1 − 2b < a + 1 − b ⇐ b > 0 ∧ a ≥ b Der entscheidende Schritt beim Terminierungsbeweis ist die Angabe einer geeigneten Abstiegsfunktion. 62 KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN Kapitel 5 Zuweisungsorientierte Programmierung Grundidee: Ein Programm besteht aus einer Folge von Anweisungen, die maschinell (d.h. durch eine Rechenanlage) ausführbar sind. Frage: Was ist eine Anweisung (ein Befehl)? Eine Anweisung entspricht einer Aufforderung zur Änderung eines Zustands. Damit ist die Idee der Anweisung eng verbunden mit der Idee des Zustands. Auf Rechnern entspricht dem Zustand die Belegung der Speicherzellen und der Register. Dabei entspricht jede Ausführung einer Anweisung der Änderung gewisser Speicherzellen und Register (von Neumann - Maschine). 5.1 Anweisungen Ein Programm besteht in der zuweisungsorientierten Programmierung aus Anweisungen. Diese lassen sich auch zu größeren Programmen zusammensetzen. 5.1.1 Syntax Syntax der Anweisungen (engl. statements): hstatementi ::= nop | abort |hassignment statementi |hsequential compositioni |hconditional statementi |hwhile statementi |hblocki |hprocedure calli 63 64 5.1.2 KAPITEL 5. ZUWEISUNGSORIENTIERTE PROGRAMMIERUNG Programmvariable und Zuweisungen In der funktionalen Programmierung verwenden wir Bezeichner / Identifikatoren zusammen mit Bindungen: nat x = 3; pnat .. . . . .x . . . .. . y Grundidee der Programmvariablen: Die Bindung kann (dynamisch) geändert werden. var nat natv := 3; .. . v := v + 1 .. . Zuweisung Bedeutung der Zuweisung: vneu = valt + 1 valt Wert von v im Zustand vor Ausführung der Zuweisung vneu Wert von v im Zustand nach Ausführung der Zuweisung andere Notation: v 0 = v + 1. 5.1.3 Zustände Ein Zustand eines Programms ist durch die Belegung seiner Programmvariablen gegeben. Sei ID die Menge der Programmvariablen, W die Menge der Werte. EN V = {σ : ID → W } EN V : Belegungen ST AT E = EN V ∪ {⊥} Zustand ∼ Belegung Anweisung ∼ erzeugt neuen Zustand 5.1.4 Funktionale Bedeutung von Anweisungen Jede Anweisung entspricht einer Abbildung ST AT E → ST AT E Interpretation für Anweisungen (hstatementi . . . Menge der Anweisungen): I : hstatementi → (ST AT E → ST AT E) 5.1.5 Operationelle Bedeutung von Anweisungen Operationell entspricht jede Anweisung einer Folge von Operationen einer Rechenanlage (s. später). 5.2. EINFACHE ANWEISUNGEN 5.2 65 Einfache Anweisungen Wir benutzen drei einfache Anweisungen: nop abort tue nichts werde nie fertig I[nop](σ) = σ I[abort](σ) = ⊥ Wichtiger: die Zuweisung v := v + 1 ( σ[σ[v] + 1/v] für σ = 6 ⊥ I[v := v + 1](σ) = ⊥ für σ = ⊥ Allgemeine Form der Zuweisung: v1 , . . . , vn := E1 , . . . , En Kollektivzuweisung In BNF: ∗ ∗ hassignment statementi ::= hidi {, hidi} := hexpi {, hexpi} Kontextbedingungen: • Anzahl und Sorte der Identifikatoren links von Sorte der Ausdrücke rechts := entspricht Anzahl und • Identifikatoren werden konsistent bezüglich ihrer Sorten verwendet Beispiel: x, y x, y x, y x, y := x − 1, y + 1 := x − 1, y + 1, z ungültig := x − 1, x ∨ y ungültig := y, x Vertauschung der Belegungen I[v1 , . . . , vn := E1 , . . . , En ](σ) = ( σ[Iσ [E1 ]/v1 , . . . , Iσ [En ]/vn ] falls σ 6= ⊥ ∧ ∀i : 1 ≤ i ≤ n : Iσ [Ei ] 6= ⊥ = ⊥ sonst 5.3 Zusammengesetzte Anweisungen Aus den Einzelanweisungen können wir Anweisungen zusammensetzen. 5.3.1 Sequentielle Komposition Seien S1, S2 gegebene Anweisungen. Dann ist S1; S2 eine zusammengesetzte Anweisung: Führe zuerst S1 und danach S2 aus. hsequential compositioni ::= hstatementi; hstatementi 66 KAPITEL 5. ZUWEISUNGSORIENTIERTE PROGRAMMIERUNG Beispiel: z := x; x := y; y := z (Vertauschen der Belegung von x und y; z ist Hilfsvariable und erhält den Wert von x) x := x + 1; x := x + 1 (Entspricht x := x + 2) Kontextbedingungen: • Sorten der Bezeichner in S1 und S2 sind konsistent I[S1; S2](σ) = I[S2] (I[S1] (σ)) Regeln: S; nop nop; S abort; S S; abort (S1; S2); S3 5.3.2 entspricht entspricht entspricht entspricht entspricht (Funktionskomposition) S S abort abort S1; (S2; S3) (⇒ Klammern unnötig) Bedingte Anweisung Gegeben: Anweisungen S1, S2, boolescher Ausdruck C if C then S1 else S2 f i Zusammenhang zwischen bedingtem Ausdruck und bedingter Anweisung: if C then x := E1 else x := E2 f i entspricht x := if C then E1 else E2 f i Syntax: if hexpithen then hconditional statementi ::=if thenhstatementi ∗ elif hexpithen then {elif thenhstatementi} else {else elsehstatementi} fi Nebenbedingungen: • Der Ausdruck C hat Sorte bool bool. • Die in C, S1 und S2 auftretenden Identifikatoren besitzen konsistente Sorten. Semantik: I[S1](σ) falls Iσ [C] = L ∧ σ 6= ⊥ if C then S1 else S2 f ii](σ) = I[S2](σ) falls Iσ [C] = 0 ∧ σ 6= ⊥ I[if ⊥ falls σ = ⊥ ∨ Iσ [C] = ⊥ Spezialfälle (notationelle Vereinfachungen): 5.3. ZUSAMMENGESETZTE ANWEISUNGEN (1) if C then S f i steht für if C then S else nop f i (2) if C then S elif C 0 then S 0 . . . f i steht für if C then S else if C 0 then S 0 . . . f i f i (3) Case - Anweisung: In manchen Programmiersprachen schreibt man case E of E1 : S1 .. . En : Sn else : Sn+1 esac ? ? für if E = E1 then S elif . . . elif E = En then Sn else Sn+1 f i 5.3.3 Wiederholungsanweisung Sei C ein boolescher Ausdruck, S eine Anweisung. Dann ist while C do S od eine Anweisung, genannt Wiederholungsanweisung. Idee: Solange C true ist, wird S ausgeführt. Nebenbedingungen: • C hat Sorte bool bool. • Identifikatoren in C und S besitzen konsistente Sorten. Beispiel: Seien x, y Variable der Sorte nat nat. 1 2 while x >= y do r, x := r+1, x-y od Diese Programm berechnet auf r das Resultat der ganzzahligen Division von x und y (genauer: von den Werten, die x und y besitzen, bevor wir das Programm ausführen). Falls y = 0 gilt: Die Wiederholung terminiert nicht! Bedeutung der Wiederholungsanweisung: Es gilt die Fixpunktgleichung while C do S od ≡ if C then S; while C do S od else nop f i Wir definieren die Bedeutung von while durch den schwächsten Fixpunkt von τ : (ST AT E → ST AT E) → (ST AT E → ST AT E) 67 68 KAPITEL 5. ZUWEISUNGSORIENTIERTE PROGRAMMIERUNG wobei (für g : ST AT E → ST AT E): 6 ⊥ ∧ Iσ [C] = L g (I[S](σ)) falls σ = τ [g](σ) = σ falls σ = 6 ⊥ ∧ Iσ [C] = 0 ⊥ sonst while C do S od I[while od] = f ix ixτ Bemerkung: Zusammenhang Wiederholung - Rekursion while C do x := E od kann durch x := f (x) ersetzt werden, wobei fir f wie folgt deklarieren: 1 2 4 fct f = (m x) m: if C then f(E) else x fi (repetitive Rekursion) Beispiel: Suchen eines Elements in einer Sequenz Gegeben: Sequenz s durch var seq m m, x der Sorte m . Gesucht: Zahl k, var bool die der Position von x in s entspricht. found (var bool) gibt an, ob x in s vorkommt. 1 2 4 6 found, k := false, 1; while (length(s) > 0) and (not found) do if first(s) ?= x then found := true else k, s := k+1, rest(s) fi od Falls vor Ausführung des Programms s die Sequenz t enthält, dann gilt nach Ausführung des Programms (found ∧ t.k = x) ∨ (¬found ∧ x kommt in t nicht vor). Varianten (Spielarten) zu Wiederholungsanweisungen: • nichtabweisende Wiederholung: repeat S until C steht für S; while ¬C do S od • gezählte Wiederholung: f or i := E1 to E2 do S od steht für i := E1; while i ≤ E2 do S; i := i + 1 od Achtung: In S sollten keine Zuweisungen an i erfolgen. 5.4 Variablendeklarationen und Blöcke Auch Programmvariablen sind zu deklarieren; dies geschieht in Blöcken: var m x := E; Sy pvar In S kann x als lokale Variable verwendet werden. Allgemeine Form der Variablendeklaration: var m1 x1 , . . . , varmn xn := E1 , . . . , En 5.5. PROZEDUREN 69 Syntax: var hsortihidi hvariable declarationi ::=var ∗ {, var hsortihidi} ∗ := hexpi {, hexpi} Nebenbedingungen: • x1 , . . . , xn treten nicht in E1 , . . . , En auf. • Sorten und Anzahl stimmen überein. Beispiel: var nat h := x; x := y; y := hy ≡ x, y := y, x pvar hblocki ::=phinner blockiy while hinneri do hinner blocki od |while if hinneri then hinner blocki else hinner blocki f i |if ∗ hinner blocki ::= {hdeclarationi; } hstatementi hdeclarationi ::=hvariable declarationi |hprocedure declarationi |helement declarationi |hfunction declarationi |hsort declarationi Nebenbedingungen: • Sortenkonsistenz • Jeder Identifikator wird in einem Block höchstens ein Mal deklariert (aber geschachtelte Deklarationen sind erlaubt). 5.5 Prozeduren Sollen gewisse Anweisungen an verschiedenen Stellen im Programm genutzt werden, so führen wir dafür (besonders bei umfangreichen Anweisungen) besser Abkürzungen ein. Dies spart nicht nur Schreib- und Leseaufwand, sondern erlaubt uns auch, in sich geschlossene Programmteile zu eigenständigen Modulen zusammenzufassen. 5.5.1 Prozedurdeklaration Beispiel: var m x, var m y) : proc vertausche = (var m h = x; x := y; y := hy pm (x, y: formale Parameter) Sind v und w Programmvariablen der Sorte m , so können wir die Prozedur mit vertausche (v, w) aufrufen (v, w: aktuelle Parameter). m h = v; v := w; w := hy. Dies hat die gleiche Wirkung wie pm 70 KAPITEL 5. ZUWEISUNGSORIENTIERTE PROGRAMMIERUNG Syntax: proc hidi = hpari {, hpari}∗ : hstatementi hprocedure declarationi ::=proc var hpari ::= {var var} hsortihidi m1 x1 , . . . , mn xn ) : S Allgemeine Form: proc p = (m Nebenbedingungen: • Sorten m1 , . . . , mn von x1 , . . . , xn müssen dem Gebrauch in S entsprechen. • x1 , . . . , xn paarweise verschieden In Prozeduren können Parameter sehr unterschiedliche Rollen übernehmen. Beispiel: proc p = (var var nat natv, var nat natw, var nat natu, nat natx) : w, v := x + v, u + w Wir unterscheiden: (1) Eingabeparameter: Programmvariable oder Konstante in der Parameterliste, die sich im Rumpf nicht ändern (keine Zuweisung erfahren). (2) Ausgabeparameter: Programmvariable in der Parameterliste, an die Zuweisungen vorgenommen werden, deren Wert jedoch nicht abgefragt wird. (3) Transienter Parameter: Programmvariable dient gleichzeitig zur Einund Ausgabe Achtung: • Die Rolle eines Parameters sieht man aus dem Prozedurkopf nicht. Bei umfangreichen Prozeduren ist die Rolle zu dokumentieren. • Neben Programmvariablen als Parameter und lokalen Programmvariablen im Rumpf können in Prozeduren auch globale Variablen auftreten. Auch globale Variablen sind zu dokumentieren. 5.5.2 Prozeduraufruf Für eine Prozedur m1 x1 , . . . , mn xn ) : S proc p = (m schreiben wir einen Aufruf wie folgt: p(E1 , . . . , En ) Jeder Aufruf ist eine Anweisung. Wir haben zwei Arten von formalen Parametern: • Konstante (d.h. die Sorte mi ist keine Variablensorte); dann muss der aktuelle Parameter Ei ein Ausdruck de rSorte mi sein. • Variable (d.h. die Sortenangabe mi ist von der Bauart var si ); dann muss der aktuelle Parameter eine Programmvariable der Sorte si sein. 5.5. PROZEDUREN 71 Beispiel: var nat v, nat x) : S proc q = (var Aufrufe (sei var nat w im Umfeld deklariert): q(w, 27) gültiger Aufruf q(w, w) gültiger Aufruf q(27, 3) ungültiger Aufruf q(w + 1, w + 2) ungültiger Aufruf Bedeutung des Prozeduraufrufs: nat x = E; S[w/v]y (wenn x in E Der Aufruf q(w, E) ist äquivalent zu pnat nicht vorkommt). Schwierigkeiten beim Prozeduraufruf: var nat x, var nat v) : Spvar var nat w := 5; proc p = (var p(w, w); .. . y Besetzung verschiedener formaler Parameter durch die gleichen globalen Parameter ist unbedingt zu vermeiden (Aliastabu). Achtung: Neben dem Wertaufruf für Konstante und dem Call - by - name / variable / reference, die wir verwenden, gibt es noch viele Prozeduraufrufmechanismen. Beispiel: Call-by-value-result nat x := E; var nat v := w; S; w := vy. q(w, E) entspricht hier pnat 5.5.3 Globale Programmvariable in Prozeduren Im Rumpf von Prozeduren dürfen auch Programmvariablen auftreten, die nicht lokal und keine formalen Parameter sind: var nat x, y := . . . ; proc vertausche =: x, y := y, x; vertausche; vertausche Globale Variable sind sorgfältig zu dokumentieren. Aliasing ist zu vermeiden: var nat x := . . . ; var nat v) : x, v := v, x + 1; proc p = (var p(x) 5.5.4 Rekursion Auch Prozeduren dürfen rekursiv sein. 1 2 4 var nat x, y, r := a, b, 0; proc pdiv =: if x < y then nop else r, x := r+1, x-y; pdiv fi 72 KAPITEL 5. ZUWEISUNGSORIENTIERTE PROGRAMMIERUNG 5.6 Block und Abschnitt, Bindung: Gültigkeit / Lebensdauer Wir benutzen Identifikatoren in folgenden vier Rollen: • Konstante • Programmvariable • Funktionen • Prozeduren In allen Fällen setzen wir voraus, dass die Identifikatoren gebunden sind. Arten der Bindung: • vordefiniert (z.B. Funktion head in Gofer) • Deklaration • formale Parameter Zu jeder Bindung gibt es einen Bindungsbereich: • Deklaration: Block / Abschnitt, in dem die Bindung steht • formale Parameter: Rumpf der Funktion / Prozedur Bei der Ausführung von Programmen sprechen wir auch von der Lebensdauer einer Bindung (Zeitdauer, für die die Bindung besteht). Bindungen können überlagert werden. 1 2 4 var nat x := 1; proc p = (var nat v, nat x): ... v := v + x; ... 6 8 | var bool x := true; x := x or not x; _| x := 5 Hier ist x drei Mal gebunden. Die Lebensdauer entspricht dem Bindungsbereich. Die Gültigkeit ergibt sich aus der Lebensdauer minus der Lebensdauer von Bindungen an den gleichen Identifikator in inneren Blöcken / Abschnitten oder Funktions- / Prozedurdeklarationen. 1 2 4 var nat v := 1; proc p = (var nat w): w := v + 1 | var nat v, var nat u := 2, 3; p(u) _| Frage: Welchen Wert hat u nach dem Aufruf p(u)? 2? → statische Bindung (static scoping) 3? → dynamische Bindung (dynamic scoping) Statische Bindung: Globale Identifikatoren richten sich nach der Deklarationsstelle. Dynamische Bindung: Globale Identifikatoren werden nach Aufrufstelle gebunden. Wir verwenden statische Bindung. Grund: Lesbarkeit - Zuordnung nach Aufschreibung 5.7. PROGRAMMTECHNIKEN FÜR ZUWEISUNGEN 5.7 73 Programmtechniken für Zuweisungen Im Gegensatz zur funktionalen Programmierung treten in Anweisungen eine Reihe von technischen Aspekten auf, die mit der Art und Weise zu tun haben, wie Anweisungen auf Rechnern abgearbeitet werden. Zusätzlich zu der Kenntnis dieser Aspekte brauchen wir Methoden, um anweisungsorientierte Programme • zu spezifizieren • zu analysieren • als korrekt nachzuweisen Beispiel: (sei x, y var nat nat) 1 2 4 6 {x = x := {x = y := {x = x := {x = a AND y = b} x + y; a + b AND y = b} x - y; a + b AND y = a} y - x; -b AND y = a} Idee der Zusicherung: Sei S eine Anweisung und Q sowie R boolesche Ausdrücke (prädikatenlogische Ausdrücke) über den Programmvariablen in S. Wir schreiben {Q} S {R} für die folgende Aussage: Gilt im Zustand vor Ausführung der Anweisung S die Aussage Q und terminiert S, so gilt im Zustand nach Ausführung von S die Aussage R. Beispiel: {x ≥ 0} x := x + 1 {x > 0} Um zu zeigen, dass Aussagen mit Zusicherungen (formalen Kommentaren) für ein Programm (eine Anweisung) tatsächlich gelten, verwenden wir folgende Regeln: Axiome des Zusicherungskalküls (Hoare - Kalkül) • Axiom für nop nop: {Q} nop {Q} • Axiom für abort abort: {Q} abort {ff alse alse} • Zuweisungsaxiom: {R[E/x]} x := E {R} • Regel für das Abschwächen von Zusicherungen (rule of consequence): Q0 ⇒ Q {Q} S {R} {Q0 } S {R0 } R ⇒ R0 74 KAPITEL 5. ZUWEISUNGSORIENTIERTE PROGRAMMIERUNG • Regel für sequentielle Komposition: {Q} S1 {R0 } {R0 } S2 {R} {Q} S1 ; S2 {R} • Regel für die bedingte Anweisung: {Q ∧ C} S1 {R} {Q ∧ ¬C} S2 {R} {Q} if C then S1 else S2 f i {R} • Regel für die Wiederholungsanweisung {R ∧ C} S {R} {R} while C do S od {¬C ∧ R} R heißt Invariante. Beispiel: 1 2 {rest(s) = empty} s := rest(s) {s = empty} 4 6 8 {x >= 0 AND y >= 0} x := x + 1 {x > 0 AND y >= 0} y := y + 1 {x > 0 AND y > 0} 10 12 14 16 {not found AND length(s) > 0 AND k + length(s) = n+1} if first(s) ?= x then found := true else k, s := k+1, rest(s) fi {((first(s) = x AND found) OR (not found)) AND (k + length(s) = n+1)} Beispiel: Hoare - Kalkül 1 2 4 6 8 10 {s = t} found, k := false, 1; {k + length(s) = n+1} while length(s) > 0 AND not found do {length(s) > 0 AND not found AND k + length(s) = n+1} if first(s) = x then found := true else k, s := k+1, rest(s) fi od {(length(s) = 0 OR found) AND (k + length(s) = n+1)} Um Zusicherungen über Wiederholungsanweisungen zeigen zu können, müssen wir geeignete Invarianten finden. Diese helfen, die Logik eines Programms mit Wiederholungsanweisungen zu verstehen. 5.7. PROGRAMMTECHNIKEN FÜR ZUWEISUNGEN 75 Beispiel: (Fortsetzung) • Invariante des Beispielprogramms: s = t[k : n] ∧ (x ∈ t ⇔ x ∈ s) • weitere Invariante: found ⇒ first(s) = x. Durch die Regeln, die wir bisher betrachtet haben, können wir allerdings nicht nachweisen, dass eine Wiederholungsanweisung terminiert. Um die Terminierung von Wiederholungsanweisungen zu zeigen, verwenden wir eine Regel, die sich auf die Idee der Zusicherungen abstützt. Um zu zeigen, dass while C do S od (5.1) unter der Vorbedingung Q terminiert, brauchen wir: • eine Invariante R • einen Terminierungsausdruck E (E bezeichnet eine ganze Zahl und darf auf Programmvariablen Bezug nehmen) • einen Hilfsidentifikator i, der in (5.1), in R und E nicht vorkommt Beweisbedingungen: (1) Q ⇒ R: die Invariante gilt am Anfang (2) E ≤ 0 ∧ R ⇒ ¬C: das Programm terminiert, falls E ≤ 0 (3) {E = i + 1 ∧ C ∧ R} S {E ≤ i ∧ R}: führung von S echt kleiner R ist invariant und E wird durch Aus- Bemerkung: E entspricht der Terminierungsfunktion in den Terminierungsbeweisen der funktionalen Programmierung. Beispiel: (Fortsetzung) Wähle für E: ( length(s) falls¬ found ∧ length(s) > 0 E= 0 falls found Wir können Q und R trivial als true wählen. Bemerkung: Zusicherungen können auch zur Spezifikation von Anweisungen verwendet werden. Wir erhalten folgendes Schema für die Spezifikation von Anweisungen: (1) Angabe der zu verwendenden Programmvariablen und ihrer Sorten und der Eingabewerte (2) Angabe der Vorbedingungen (3) Angabe der Nachbedingungen Beispiel: (Fortsetzung) Eingabewerte: Programmvariablen: Vorbedingung: Nachbedingung: t: seq m x: m var bool found var seq m s var nat k s=t (found ∧t[k] = x) ∨ (¬ found ∧x ∈ / t) 76 KAPITEL 5. ZUWEISUNGSORIENTIERTE PROGRAMMIERUNG Kapitel 6 Sortendeklarationen bool Bisher haben wir mit einer kleinen Anzahl vorgegebener Sorten gearbeitet (bool bool, nat seq m nat, m). Nun betrachten wir Möglichkeiten, weitere Sorten zu deklarieren. 6.1 Deklaration von Sorten BNF-Syntax: sort hsorti = hsort constructi hsort declarationi ::=sort hsort constructi ::=henumeration sorti |hproduct sorti |hsum sorti |hsubrange sorti |harray sorti |hset sorti |hfile sorti |hsorti 6.1.1 Skalare durch Enumeration Wir können Sorten mit endlich vielen Elementen einfach durch Aufzählung angeben: Beispiel: sort color = {blue, yellow, red} Damit haben wir eine Sorte deklariert, die (außer ⊥) genau drei Elementen besitzt. 1 2 4 fct f = (color x) color: if x ?= yellow then red else blue fi Wichtig: Auf den Elementen einer Enumerationssorte ist nur die Vergleichsoperation verfügbar. Allgemeine Form: sort enum = {e1 , . . . , en } 77 78 KAPITEL 6. SORTENDEKLARATIONEN Vergleichsoperatoren: ? ? ei = ej ≡ i = j ei ≤ ej ≡ i ≤ j Syntax: ∗ henumeration sorti ::= { hidi {, hidi} } Beispiel: sort currency = {euro, dollar, yen, . . .} Grundsätzliches zur Sortendeklaration: • eine Sortenbezeichnung • eine Menge (Trägermenge) von Sortenelementen • eine Menge von Funktionen – zur Konstruktion von Elementen der Sorte (Konstruktoren) – zum Abfragen von Eigenschaften (Selektoren etc.) Beispiel: sort obst = {banane, apfel, birne, . . .} Sortenbezeichnung: obst Konstruktoren: banane, apfel, birne 6.1.2 Direktes Produkt / Tupelsorten Beispiel: string name, nat alter, sex geschlecht) sort person = makeperson(string sort sex = {mann, frau} Tupelsorten dienen dazu, unterschiedliche Informationen zu einem Paket (Tupel) zusammenzufassen. Allgemeine Form: sort product = construct(ss1 sel1 , . . . , sn seln ) Trägermenge zur Sorte product product: M1− × . . . × Mn− ∪ {⊥} direktes Produkt wobei Mi− Träger zur Sorte si (ohne ⊥) sind. Funktionen: product f ct construct = (ss1 , . . . , sn )product product f ct seli = (product product)ssi für 1 ≤ i ≤ n Es gilt (für ai ∈ Mi− und 1 ≤ i ≤ n): seli (construct(a1 , . . . , an )) = ai 6.1. DEKLARATION VON SORTEN 79 Beispiel: 1 2 4 alter (makeperson ("Heidi", 22, frau)) = 22 fct geburtstag = (person x) person: makeperson (name(x), alter(x)+1, sex(x)) Syntax: hproduct sorti ::= hidi ∗ hsorti {hidi} {, hsortihidi} Nebenbedingung: • Selektorfunktionen paarweise verschieden Achtung: In vielen Programmiersprachen können Produktsorten selektiv geändert werden (sei v vom Typ var person person): statt v := makeperson(name(v), alter(v) + 1, sex(v)) schreiben wir alter(v) := alter(v) + 1 Beispiel: sort empty = empty() 6.1.3 Direkte Summe / Varianten Oft sind wir interessiert, aus einer Anzahl von Sorten eine Vereinigung zu bilden. nat seuro)|dollar(nat nat sdollar) sort currency = euro(nat Die direkte Summe entspricht der disjunkten Vereinigung von Mengen. ({euro} × N) ∪ ({dollar} × N) Beispiel: nat smeter)|feet(nat nat sfeet) sort laenge = meter(nat Allgemeine Form: sort sum = product1 | . . . |productn Hierbei steht productj für die Sorte constructj = sj1 selj1 , . . . , sjnj seljnj Funktionen: • Konstruktoren: sum f ct constructj = (ssj1 , . . . , sjnj )sum • Abfrage: Sei y von der Sorte sum sum. y in constructj Diskriminator beantwortet die Frage, ob y von der Variante productj ist (boolesches Resultat). 80 KAPITEL 6. SORTENDEKLARATIONEN Beispiel: var currency c := euro(25) c in euro true c in dollar false Selektion (Projektion): selij ( xji = ⊥ fallsy = constructj (xj1 , . . . , xnnj ) sonst Beispiel: (Fortsetzung) seuro(c) = 25 sdollar(c) = ⊥ 1 2 4 fct eurowert = (currency x) nat: if x in euro then seuro (x) else sdollar (x) * 0.98 fi Syntax: 6.1.4 n o∗ hsum sorti ::= hproduct sorti |hproduct sorti Teilbereichsorten Manchman möchte man von Sorten m , auf denen eine lineare Ordnung ≤ definiert ist, eine Teilbereichsorte bilden. Seien E1 , E2 Ausdrücke der Sorte m , dann bezeichnet E1 : E2 die Teilbereichsorte (sei M − Trägermenge zu der Sorte m ohne ⊥) mit Trägermenge x ∈ M − : E 1 ≤ x ≤ E2 Beispiel: sort bnat = 0 : 248 − 1 sort tag = {mo, di, mi, do, fr, sa, so} sort werktag = mo : fr Syntax: hsubrange sorti ::= hexpi : hexpi 6.2 Felder Felder dienen dazu, eine große Zahl von Elementen gleicher Sorte darzustellen und auf diese durch Indexwerte zuzugreifen. 6.2. FELDER 6.2.1 81 Einstufige Felder Seien i, j ganze Zahlen mit i ≤ j. Dann ist array m [i : j]array die Sorte eines Feldes der Länge j − i + 1 für Elemente der Sorte m . array m kann man sich wie folgt vorstellen: [i : j]array i i + 1 ... j ai ai+1 . . . aj d.h. a entspricht einer Abbildung a : {i, . . . , j} → M − wobei M − die Menge der Elemente der Sorte m ohne ⊥ bezeichnet. Beispiel: array person [1 : 10]array Funktionen auf Feldern: array s f ct init = [i : j]array array ss, int f ct get = ([i : j]array int)ss array ss, int array s f ct update = ([i : j]array int, s )[i : j]array init liefert ein Feld, in dem alle Einträge undefiniert sind: get (init, k) = ⊥ falls k = k 0 , k = 6 ⊥, 1 ≤ k ≤ j x 0 0 0 get (update (a, k, x) , k ) = get (a, k ) falls k 6= k , k = 6 ⊥ 6= k 0 , i ≤ k, k 0 ≤ j ⊥ sonst Notation: Wir schreiben a[k] für get (a, k), a[k] := E für a := update (a, k, E) (selektives Ändern). Die Menge aller Felder ist gegeben durch (sei M = M − ∪ {⊥}) ({i, . . . , j} → M ) ∪ {⊥} 6.2.2 Felder und selektives Ändern array int a mit a[k] 6= Sortieren durch Auswählen in einem Feld: Gegeben sei [1 : n]array array int va := a. Gesucht ist ein Programm, das ⊥∀1 ≤ k ≤ n und var var[1 : n]array auf va die (absteigend) sortierte Sequenz zu a erzeugt (In - Situ - Sortieren, d.h. Sortieren auf dem Platz). Lösung: Suchen des jeweils maximalen Elements. 1 2 4 6 8 10 for i := 1 to n do var int max := i; for j := i+1 to n do if va[max] < va[j] then max := j fi od if not (max ?= i) then va[i], va[max] := va[max], va[i] fi od 82 KAPITEL 6. SORTENDEKLARATIONEN Achtung: Durch Schreibweisen wie a[i] := a[i]+1 erhalten wir Bezeichnungen a[i] für Programmvariable. var [1 : n] array nat a wird gleichsam als [1 : n] array var nat a verstanden. Wir erhalten errechnete Programmvariable a[i + j]. Es gilt auch die Zuweisungsregel nach Hoare nicht mehr. Die Aussage {x = a[j]} a[i] := a[i] + 1 {x = a[j]} ist für den Fall i = j falsch. Das Hoare - Kalkül ist für Felder nur korrekt, wenn wir die abkürzende Schreibweise durch die ausführliche ersetzen: 1 2 {x = get (update (a, i, get (a, i) + 1), j)} a := update (a, i, get (a, i) + 1) {x = get (a, j)} 6.2.3 Mehrstufige Felder Allgemeine Sorte: [n1 : m1 , . . . , nk : mk ] array m ist Sorte eines k - stufigen Feldes. Beispiel: sort matrix = [1 : n, 1 : m] array m Syntax: ∗ harray sorti ::=[hindexi {, hindexi} ] array hsorti hindexi ::=hsubrange sorti |hsorti Beispiel: Multiplikation Vektor - Matrix 1 2 4 6 8 proc vectmatmult = ([1:n, 1:n] array nat a, [1:n] array nat x, var [1:n] array nat y): for i := 1 to n do y[i] := 0; for j := 1 to n do y[i] := y[i] + a[i,j] * x[j] od od Kritischer Punkt: Festlegung der Feldgrenzen • statisch: Die Feldgrenzen sind durch Zahlenwerte (Konstante) vor Ausführung des Programms festgelegt. • dynamsich: Die Feldgrenzen werden zum Deklarationszeitpunkt des Feldes festgelegt. • flexibel: Feldgrenzen können während der Lebensdauer des Feldes verändert werden. 6.3. ENDLICHE MENGEN 6.3 83 Endliche Mengen Sei m eine beliebige Sorte; mit set m bezeichnen wir die Sorte aller Mengen, die endliche Teilmengen der Trägermenge zur Sorte m (ohne ⊥) sind. Trägermenge zu set m ist {s ⊆ M \ {⊥} : s endlich} ∪ {⊥}, wobei M Trägermenge zur Sorte m sei. Operationen auf Mengen: ∅ leere Menge (emptyset) ∪ Vereinigung (union) ∩ Durchschnitt (average) {x1 , . . . , xn } bilden endlicher Mengen \ Mengendifferenz (diff) ∈ Elementrelation (elem) Beispiel: Generieren der Menge aller Teilmengen von {1, . . . , n} für ein gegebenes n ∈ N: nat n) set set nat f ct gen = (nat gen(n) = {s ⊂ {1, . . . , n}} Gesucht ist ein induktives Prinzip. Für n ≥ 1 sei x = gen(n−1) gegeben. Es ist x ⊆ gen(n) (x enthält alle Mengen, in denen n nicht vorkommt). Dann ist gen(n) = x ∪ {s ∪ {n} : s ∈ x}. 1 2 4 fct gen = (nat n) set set nat: if n ?= 0 then {emptyset} else set set nat x = get (n-1); union (x, include (x, n)) fi 6 8 10 fct include = (set set nat x, nat n) set set nat: if x ?= emptyset then emptyset else set nat s = any (x); union ({union (s, {n})}, include (diff (x, {s}), n)) set m m benutzt, die für beliebige Wir haben hier die Auswahlfunktion f ct any = (set m)m Elemente s der Sorte setm setm(6= ⊥) folgender Regel genügt: s 6= ∅ ⇒ any(s) ∈ s (Hilberts Auswahloperator) Mengen können auch verwendet werden, um gerichtete Graphen darzustellen. Gegeben sei sort node = {a, b, . . . , z}. Ein gerichteter Graph wie in Abbildung 6.1 kann dadurch beschrieben werden, dass wir für jeden Knoten x die Menge g(x) der Knoten angeben, zu denen von x aus eine Kante führt: node set node Graph der Funktion f ct g = (node node)set Beispiel: Für einen durch g gegebenen Graphen können wir nun die Menge aller von einem gegebenen Knoten x über Folgen von Knoten erreichtbaren Knoten berechnen. x → x1 → x3 → . . . → xn Kantenzug (Pfad) in g Naiver Ansatz zur Menge der erreichbaren Knoten: 84 KAPITEL 6. SORTENDEKLARATIONEN a b e d c Abbildung 6.1: Gerichteter Graph 1 2 4 fct reachable = (set node x) set node: if x ?= emptyset then emptyset else union (x, {reachable (g (z)): elem (z, x)}) fi Problem: Der Algorithmus terminiert nicht im Falle von Zyklen! Frage: Wir brechen wir Zyklen auf? tc (transitive closure) berechnet alle von x aus erreichbaren Punkte, ohne auf Wegen zu gehen, die Knoten aus y enthalten, vereinigt mit den in y enthaltenen Punkten. 1 2 4 6 fct tc = (set node if x ?= emptyset else z = any(x); if elem (z, y) else tc (union fi fi x, set node y) set node: then y then tc (diff (x, {z}), y) (x, g(z)), union (y, {z})) Durch den Aufruf tc ({a} , ∅) erhalten wir die Menge aller von a aus erreichbaren Knoten. Kapitel 7 Sprünge und Referenzen Heutige Rechenmaschinen verwenden einen Speicher, der im Wesentlichen ein riearray siges Feld (array array) ist. Der Index des Feldes ist die Speicheradresse. Sowohl die Daten als auch die Programme sind im Speicher abgelegt. Jedes im Speicher abgelegte Programmstück (jeder Befehl) und jedes Datenelement hat somit eine Adresse. Wir besprechen nun zwei Ideen für Programmierelemente, die auf diese Beobachtung zurückzuführen sind: • Sprünge • Referenzen 7.1 Kontrollfluss In einem zuweisungsorientierten (prozeduralen) Programm besteht die Ausführung aus einer Folge von Befehlen, die ausgeführt werden. Welche Befehle wann ausgeführt werden, wird über Bedingungen gesteuert; wir sprechen vom Kontrollfluss. 7.1.1 Marken und Sprünge Will man den Kontrollfluss explizit steuern, so bietet sich an: (1) gewisse Anweisungen S mit Namen m zu versehen (zu markieren) [m: S (Marke)] (2) einen Befehl zu verwenden, der den Kontrollfluss zu einer markierten Anweisung steuert [goto m (Sprungbefehl)] Beispiel: Programm mit Sprungbefehl 1 2 4 6 8 y := 1; m: if y <= n then if a[y] ?= x then nop else y := y + 1; goto m fi else y := 0 fi 85 86 KAPITEL 7. SPRÜNGE UND REFERENZEN Nachbedingung: {(y = 0 ∧ ∀y ∈ {1, . . . , n} : a[j] 6= y) ∨ (y ∈ {1, . . . , n} ∧ a[y] = x)} Durch Marken (label) können wir Anweisungen in Programmen eindeutig kennzeichnen. Durch m : S wird eine Marke m deklariert. Für die Deklaration von Marken gelten die gleichen Regeln wie für andere Deklarationen: (1) Innerhalb eines Bindungsbereichs darf eine Marke höchstens ein Mal deklariert werden. (2) Deklarationen dürfen sich überlagern. Wir können jedes zuweisungsorientierte Programm (abgesehen von rekursiven Prozeduren) in eine Form bringen, bei der wir nur zwei Arten von Befehlen benötigen: m1 : x := E Datenfluss m2 : if C then goto m3 f i Kontrollfluss Diese Befehlsstruktur ist sehr ähnlich dem Aufbau von maschinennahen Sprachen. Achtung: goto - Befehle sind mit Vorsicht zu genießen, da sie zu sehr fehleranfälligen Programmen führen. Erkenntnis (1970): Die Anzahl der goto - Befehle in einem Programm ist direkt proportional zur Anzahl der Fehler (Grund: Ablaufstruktur ist implizit). Lösung: Strukturiertes Programmieren - goto vermeiden. Beispiel: while - Anweisung übersetzt in goto gotos while C do S od ist gleichwertig zu mwhile: if ¬C then goto whileende f ii; S; goto mwhile; whileende: nop 7.1.2 Kontrollflussdiagramme Die graphische Darstellung von Programmen wird häufig als ein Mittel angesehen, Programme einfacher verständlich zu machen. Abbildung 7.1 stellt den Kontrollfluss des Beispielprogramms aus 7.1.1 dar. 7.2 Referenzen und Zeiger Die Verwendung von Bezeichnern (Identifikatoren) ist eine der grundlegenden Ideen der Mathematik, Logik, Informatik. Arten der Verwendung von Identifikatoren: • gebundene Identifikatoren • freie Identifikatoren • Programmvariable 7.2. REFERENZEN UND ZEIGER 87 y := 1 true false false y <= n true a[y] = x y := 0 y := y + 1 Abbildung 7.1: Kontrollflussdiagramm Nun gehen wir den Schritt vom Bezeichner zu Werten, die als Stellvertreter für andere Werte stehen. Die Menge der Verweise auf Elemente einer Sorte m bezeichnen wir mit der Sorte ref m m. Wichtig ist für die Elemente der Sorte ref m nicht, wie sie aussehen oder dargestellt werden, sondern welche Operationen für sie verfügbar sind: • Dereferenzieren: Übergang von einem Verweis / einer Referenz auf den bezoref m m genen Wert: f ct deref = (ref m)m ? ? • Identitätsvergleich: ref m x, y: x = y, deref(x) = deref(y) (x = y ⇒ deref(x) = deref(y)) • Generieren (erzeugen) von Referenzen funktional: nil leere Referenz nil deref(nil nil) = ⊥ ref m ref m f ct gen = (ref m, m )ref deref (gen (r, a)) = a Beispiel: nil ref nat x = gen(nil nil, 7) Wir können natürlich auch Referenzen mit Variablen kombinieren: var ref m ref var m mx dadurch werden Zuweisungen wie deref(x) := wert möglich var ref var m Pointer Zum Erzeugen von Referenzen werden oft spezielle Prozeduren verwendet: var ref var m v) : . . . proc new = (var Sei var ref var m v deklariert. Dann entspricht new(v) einer Zuweisung an v: Dabei bekommt v als neuen Wert eine Referenz (Zeiger, Pointer) auf eine neue Programmvariable. Auf diese Weise kann die Anzahl der Programmvariablen dynamisch (zur Ausführungszeit des Programms) erhöht werden. Idee: Aufbau von verzweigten Strukturen. 88 KAPITEL 7. SPRÜNGE UND REFERENZEN Kapitel 8 Rekursive Sorten Bisher haben wir (abgesehen von Sequenzen und dynamischen Feldern) nur statische Datenstrukturen behandelt. Dies sind Datenstrukturen, bei denen der Umfang der zu speichernden Informationen beschränkt ist. Dabei ist zu berücksichtigen, dass in vielen Implementierungen von Programmiersprachen auch die Größe der natürlichen Zahlen beschränkt ist. Im Folgenden studieren wir Datenstrukturen, die geeignet sind, eine unbeschränkte Größenordnung von Information zu speichern. 8.1 Sequenzartige Rechenstrukturen Die Struktur der endlichen Sequenzen über einer gegebenen Menge M ist für viele Algorithmen und Anwendungen hilfreich. Beispiel 8.1.1 Strings, Pfade in einem Graphen etc. Rechenstruktur der Sequenzen Sorte seq m m: Sequenz von Elementen der Sorte m . Sei M − die Menge der Elemente der Sorte m (ohne ⊥). Trägermenge zu seq m ist ∗ dann: (M − ) ∪ {⊥}. Operationen auf Sequenzen: empty, make, conc, first, last, rest, lrest. Beobachtung: Viele Programmiersprachen stellen keine Sorte von Sequenzen allgemein zur Verfügung (höchstens string mit kleinen Längen). Stattdessen werden Unterstrukturen zur Verfügung gestellt, d.h. Sequenzen, für die nicht alle Operationen zur Verfügung stehen (insbesondere die Konkatenation nicht). 8.1.2 Stack stackm sei die Sorte der Stapel von Elementen der Sorte m . Operationen auf Stacks: empty, append, first, rest. 1 2 fct append = (m x, stack m s) stack m: conc (<x>, s) Wichtige Gleichungen: rest (append (x, s)) = s first (append (x, s)) = x Prinzip: Last - in - first - out (LIFO) 89 90 KAPITEL 8. REKURSIVE SORTEN pegel 1 2 3 4 5 6 s1 s2 s3 s4 s5 s6 7 Abbildung 8.1: Stack als Feld dargestellt Beispiel Typischerweise verwenden wir in der zuweisungsorientierten Programmierung Programmvariablen vom Typ var stack m zur Zwischenspeicherung von Teilergebnissen. Gegeben sei ein Feld a[1:n] mit Elementen der Sorte nat nat. Aufgabe: Das Feld ist aufsteigend zu sortieren. 1 2 4 6 8 10 12 proc partition = (var [1:n] array nat a, nat min, nat max, var nat r): | var nat i, j, x := min+1, max, a[min]; while i <= j do if a[i] <= x then i := i + 1 elif x <= a[j] then j := j - 1 else a[i], a[j] := a[j], a[i] fi; if min < j then a[min], a[j], r := a[j], a[min], j else r := min fi | 14 16 18 20 22 24 proc quicksort = (var [1:n] array nat a): | var nat r, min, max; var stack nat s := append (1, append (n, empty)); while s <> empty do s, min, max := rest(rest(s)), first(s), first(rest(s)); partition (a, min, max, r); if min < r-1 then s := append (min, append (r-1, s)) fi; if r+1 < max then s := append (r+1, append (max, s)) fi od | Dieses Beispiel zeigt, wie durch die Verwendung von Stapeln Rekursion vermieden werden kann. Fakt: Jede Rekursion kann durch Stapel ersetzt werden! Stapel lassen sich rekursiv deklarieren: m first, stack m rest) sort stack m = empty empty|append (m Solange die Größe von Stapeln beschränkt ist und wir die Schranke kennen, können wir Stapel einfach durch Felder darstellen (gegeben sei var array [1 : n] m feld und varnat pegel) [vgl. Abb. 8.1]. Dabei gilt: • feld[pegel] liefert das erste Element • rest bedeutet Dekrementieren des Pegels • append durch Inkrementieren des Pegels und Schreiben in das Feld 8.2. BAUMARTIGE RECHENSTRUKTUREN 91 fe 1 2 le 3 4 5 6 s1 s2 s3 s4 7 Abbildung 8.2: Queue als Feld • leerer Stapel: pegel = 0 8.1.3 Warteschlangen (Queue) queue m sei die Sorte der Warteschlangen über der Sorte m . Operationen auf Warteschlangen: empty, first, rest, stock. Wichtigste Operation: 1 2 fct stock = (queue m q, m x) queue m: conc (q, <x>) Wichtige Gleichungen: first (stock (empty, x)) = x rest (stock (empty, x)) = empty first (stock (stock (q, y) , x)) = first (stock (q, y)) rest (stock (stock (q, y) , x)) = stock (rest (stock (q, y) , x)) Prinzip: First - in - first - out (FIFO) Darstellung in einem Feld (gegeben seien var array [1 : n] m feld und var nat le, fe) [vgl. Abb. 8.2]: Zwei Pegel: • fe ∼ first element • le ∼ last element 8.1.4 Dateien Auch Dateien sind sequenzartige Rechenstrukturen. seq m old, seq m new) sort band = datei (seq 8.2 Baumartige Rechenstrukturen Bäume sind für die Informatik zentrale Datenstrukturen. Definition: Baum über einer gegebenen Menge M (informell) Ein Baum ist: (1) leer oder (2) besteht aus einem Element aus M (genannt die Wurzel des Baums) und einer endlichen Menge von Bäumen 92 KAPITEL 8. REKURSIVE SORTEN 13 22 12 13 7 22 7 12 3 Beispiel: Bäume über N 4 4 28 7 3 ε 5 5 11 22 23 2 ε 13 Es gibt viele Möglichkeiten, Bäume darzustellen: 22 12 7 ε {13, {{22, {{7} , ε}} , {12}}} (((7) , 22, ε) , 13, (12)) Beispiel: arithmetische Ausdrücke als Baum [(3 + 7) ∗ (8 + 12)] − 22 22 * + + 3 7 8 12 Definition: Binärbäume über M (formal) Wir definieren die Menge der Binärbäume über M induktiv: T0 = {ε} leerer Baum Ti+1 = Ti ∪ (Ti × M × Ti ) Wir definieren Tree(M ) = [ i∈N Ti 8.2. BAUMARTIGE RECHENSTRUKTUREN 93 Bäume als Rechenstruktur Sei tree m die Sorte der Binärbäume über der Sorte m . f ct f ct f ct f ct f ct f ct emptytree = tree m tree m cons = (tree m, m , tree m m) tree m tree m left = (tree m) tree m tree m right = (tree m) tree m tree m root = (tree m) m tree m isempty = (tree m) bool isempty (emptytree) = true isempty (cons (t, x, t0 )) = false left (cons (t, x, t0 )) = t root (cons (t, x, t0 )) = x right (cons (t, x, t0 )) = t0 left (emptytree) = ⊥ root (emptytree) = ⊥ right (emptytree) = ⊥ Beispiele: Programme über Bäumen (1) Umformen von Bäumen in Sequenzen: Vorordnung (preorder): Erst die Wurzel, dann links, dann rechts. 1 2 4 fct pre = (tree m t) seq m: if isempty(t) then empty else conc (<root(t)>, conc (pre(left(t)), pre(right(t)))) fi (2) Inordnung (inorder): Erst links, dann die Wurzel, dann rechts. 1 2 4 fct in = (tree m t) seq m: if isempty(t) then empty else conc (in(left(t)), conc (<root(t)>, in(right(t)))) fi (3) Postordnung (postorder) 1 2 4 fct post = (tree m t) seq m: if isemtpy(t) then emtpy else conc(post(left(t)), conc(post(right(t)), <root(t)>)) fi Achtung: Diese Abbildungen sind nicht injektiv, d.h. verschiedene Bäume werden auf die selbe Sequenz abgebildet. Mögliche Lösung (für Pre- und Postordnung): ε mit in die Sequenz aufnehmen. 94 KAPITEL 8. REKURSIVE SORTEN 22 Auswahlbäume 22 4 3 12 22 12 1 4 10 22 7 12 ε ε εε εε ε ε εε ε ε εε Idee: Die Wurzel ist das Maximum der Wurzeln der Teilbäume (falls diese nicht leer sind). Die Teilbäume sind entweder beide leer oder beide nicht leer. Programm, das entscheidet, ob ein Baum ein Auswahlbaum ist: 1 2 4 6 8 fct istct = (tree nat t) bool: if isempty(t) then true elif isempty(left(t)) then isempty(right(t)) elif isempty(right(t)) then false else isct(left(t)) and isct(right(t)) and root(t) ?= max (root(left(t)), root(right(t))) fi Die folgende Rechenvorschrift wandelt eine Sequenz in einen Auswahlbaum um: 1 2 4 6 fct mktree = (seq nat s) tree nat: if s ?= empty then emptytree elif length(s) = 1 then cons(emptytree, first(s), emptytree) else nat k = length(s) / 2; cctree (mktree (part(s, 1, k)), mktree (part (s, k+1, length(s)))) fi 8 10 12 14 fct cctree = (tree nat l, tree nat r) tree nat: if isempty(r) then l elif isempty(l) then r elif root(l) > root(r) then cons (l, root(l), r) else cons (l, root(r), r) fi Löschen des maximalen Elements im Auswahlbaum: 1 2 4 6 8 fct deltree = (tree nat t) tree nat: if isempty(t) then emptytree elif isempty(right(t)) then deltree(left(t)) elif isempty(left(t)) then deltree(right(t)) elif root(t) ?= root(left(t)) then cctree (deltree(left(t)), right(t)) else cctree (left(t), deltree(right(t))) fi Sortieren durch Auswahlbäume (Heapsort) 1 2 fct heapsort = (seq nat s) seq nat: treeinsort (mktree(s)) 8.3. REKURSIVE SORTENVEREINBARUNGEN 4 6 95 fct treeinsort = (tree nat t) seq nat: if isemtpy(t) then empty else conc (treeinsort(deltree(t)), <root(t)>) fi Arithmetische Ausdrücke als Bäume sort op = (0 +0 ,0 −0 ,0 ·0 ,0 /0 ) nat n)|operator(op op sort entry = operand(nat opo) Prüfen, ob ein Baum einen korrekten arithmetischen Ausdruck darstellt: 1 2 4 fct iae = (tree entry t) bool: if isemtpy(t) then false elif root(t) in operator then iae(left(t)) and iae(right(t)) else isempty(left(t)) and isempty(right(t)) fi Auswerten eines arithmetischen Ausdrucks gegeben als Baum: 1 2 4 6 fct eval = (tree entry t: iae(t)) nat: if root(t) in operand then n(root(t)) elif root(t) ?= operator(’+’) then eval (left(t)) + eval (right(t)) ... fi Ein arithmetischer Ausdruck, dargestellt durch einen Baum t, kann durch post(t) in eine Sequenz umgewandelt werden. Sei s diese Sequenz, als Stack dargestellt. Wir werten den Ausdruck in dieser Form wie folgt aus: 1 2 4 6 8 fct steval = (stack entry e, stack nat k) nat: if isempty(e) then first(k) elif first(e) in operand then steval(rest(e), append(n(first(e)), k)) elif first(e) ?= operator(’+’) then steval(rest(e), append(first(rest(k))+first(k), rest(rest(k)))) ... fi 8.3 Rekursive Sortenvereinbarungen Beispiel tree m left, m root, tree m right) sort tree m = emptytree|cons (tree sort arithe xp = operand (nat nat n) |ausdruck (arith arithe xp l, op o, arithe xp r) Frage: Welche Sorte, d.h. welche Trägermenge wird durch eine rekursive Sortenvereinbarung festgelegt? Sei D die Menge aller Datenelemente. Mit der rechten Seite einer rekursiven Sortenvereinbarung verbinden wir eine Abbildung ∆ : 2D → 2D 96 KAPITEL 8. REKURSIVE SORTEN ∆ am Beispiel der rekursiven Sortendeklaration für Bäume Sei K die Trägermenge zur Sorte k und M die Trägermenge zur Sorte m . Der Sortenaustree m left, m root, tree m right) definiert eine Trägermendruck emptytree|cons (tree ge, die wir als ∆(K) definieren: ∆(K) = {⊥} ∪ {ε} ∪ {(x, y, z) : x, z ∈ K \ {⊥} , y ∈ M \ {⊥}} Mit ∆ können wir eine Folge von Mengen Ki , i ∈ N definieren: K0 = {⊥} Ki+1 = ∆(Ki ) im Beispiel: K0 = {⊥} K1 = {⊥, ε} K2 = {⊥, ε} ∪ {(ε, y, ε) : y ∈ M \ {⊥}} Für die Konstruktion gilt: ∀i ∈ N : Ki ⊆ Ki+1 K∞ = [ Ki Trägermenge der rekursiv definierten Sorte i∈N K = ∆(K) Fixpunkt kleinster Fixpunkt in Mengeninklusion: K∞ 8.3.1 Verwendung von Rekursion für Sortenvereinbarungen Beispiel: Bäume mit beliebigen Verzweigungen m root, children ch) sort bvtree = bvcons (m bvtree first, children rest) sort children = emptytree|app (bvtree 8.4 Geflechte Oft werden in rekursiven Sortendeklarationen Referenzen mit der Rekursion verbunden. Beispiele ref tree textlef t, m root, ref rtree right) sort rtree = emptytree|cons (ref sort ref tree = ref retree ref tree left, m root, ref tree right) sort retree = cons (ref Hier wird der leere Baum mit nil dargestellt. Durch rekursive Sorten und Referenzen können wir Sorten einführen, die es uns erlauben, Datenstrukturen zu behandeln, die gerichteten Graphen entsprechen. Sehen wir als Knoten in diesen Graphen Programmvariable vor, so können wir die Graphen selektiv verändern. 8.4. GEFLECHTE 97 Abbildung 8.3: Geflecht Person 3 Person 4 Person 2 Person 1 Abbildung 8.4: Geflecht über der Sorte person 8.4.1 Einfache Geflechte Wir können auch mit Geflechten arbeiten, ohne Rekursion bei den Sorten zu verwenden. Beispiel: Bibliothek sort buch = cbuch (string string titel, ref var regal platz) nat nummer, ref var saal ort) sort regal = cregal (nat nat nummer, ref var stockwerk w) sort saal = csaal (nat ... Um ein Buch in ein anderes Regal zu stellen, schreiben wir (sei buch b gegeben): deref (platz (b)) := neuesregal; Solange wir keine rekursiven Sorten verwenden, sind die Geflechte zyklenfrei. 8.4.2 Geflechte über rekursiv vereinbarten Sorten Beispiel string name, ref var person mutter) sort person = persdaten (string Führen mehrfach Verweise aus unterschiedlichen Geflechtsteilen auf den gleichen Record, dann sprechen wir von gemeinsamen Teilstrukturen. Wir können mit dieser Technik Informationen genau an einer Stelle im Programm verwalten und von vielen Punkten über Verweise darauf zugreifen. 98 KAPITEL 8. REKURSIVE SORTEN (a) Einfach verkettete Liste (b) Einfach verkettete Ringliste Abbildung 8.5: Verkettete Listen Abbildung 8.6: Invertierter Baum 8.4.3 Verkettete Listen Um Sequenzen durch Geflechte darzustellen, verwenden wir verkettete Listen. sort revl = ref var pevl m first, var revl rest) sort pevl = pair (m Beispiel: Einfach verkettete Listen in Pascal 1 2 4 6 type ref_elist = ^elist; elist = record first: integer; rest: ref_elist end; Rechenvorschrift zum Aufbau eines Listenelements: 1 2 4 6 8 procedure push (n: integer; var v: ref_elist); var h: ref_elist; begin new (h); h^.first := n; h^.rest := v; v := h end; 8.4.4 Zweifach verkettete Listen Will man verkettete Listen in zwei Richtungen durchlaufen, so bieten sich zweifach verkettete Listen an. Beispiel: Zweifach verkettete Liste in Pascal 1 2 type rhead = ^head; 8.4. GEFLECHTE 99 Abbildung 8.7: Zweifach verkettete Liste 4 6 8 10 rzvl = ^zvl; head = record first, last: rzvl end; zvl = record vor, nach: rzvl; elem: integer end; Die folgende Rechenvorschrift erzeugt für eine gegebene Zahl n ∈ N, n ≥ 1 eine zweifach verkettete Liste, die alle Zahlen von 1 bis n enthält. 1 2 4 6 8 10 12 14 16 18 20 22 procedure build (n: integer; var r: rhead); var h, a: rzvl; k: integer; begin new (a); new (r); r^.first := a; a^.elem := 1; k := 2; while k <= n do begin new (h); h^.vor := a; a^.nach := h; h^.elem := k; inc (k); a := h end; r^.last := a; a^.nach := nil; r^.first^.vor := nil end; Geflechte treten in einer großen Variantenzahl auf. 100 KAPITEL 8. REKURSIVE SORTEN Kapitel 9 Objektorientiertes Programmieren Idee: Beim Aufbau eines Programmes verwenden wir gleichartig strukturierte Bausteine, genannt Objekte. Zuammenhang zum Record: Objekt ∼ Verweis (Objektidentifikator) auf Programmvariable der Sorte Record + Prozeduren (Methoden), die auf den Elementen (Attributen) des Records arbeiten. Klasse ∼ Beschreibung von Objekten gleicher Verhaltensstruktur Wichtige Idee: Daten / Programmvariablen und der Zugriff darauf (durch Methoden) werden zusammengefasst (Verkapselung). Damit werden die Möglichkeiten, Daten zu manipulieren, bewusst eingeschränkt. Es entsteht die Idee der Schnittstelle (engl. interface). Beispiel 1 2 4 6 8 class Point =: var real x1, x2, x3; method create = (real y1, y2, y3): x1, x2, x3 := y1, y2, y3; method shift = (real y1, y2, y3): x1, x2, x3 := x1+y1, x2+y2, x3+y3; method dist = (var real d): d := sqrt (x1*x1 + x2*x2 + x3*x3); endclass 9.1 Klassen und Objekte Im Zentrum der objektorientierten Programmierung stehen die Begriffe se und Objekt. Objekt: Zusammenfassung von Klas- • Variablen und Konstantenbezeichnungen (genannt Attribute) • Prozeduren (genannt Methoden) Idee: Auf die Attribute kann von außen nur über die Methoden zugegriffen werden. Jedes Objekt besitzt einen eindeutigen Identifikator (vergleichbar mit einer Referenz). 101 102 KAPITEL 9. OBJEKTORIENTIERTES PROGRAMMIEREN 9.1.1 Klassen Klassen dienen der Beschreibung von Objekten. Klasse: • Eine Klasse besitzt eine Klassenbezeichnung. • Eine Klasse definiert eine Menge von Attributen und Methoden. • Die Attributnamen sind nur innerhalb der Klassendefinition gültig. • Die Methoden sind außerhalb der Klassendefinition gültig und dienen dem Zugriff auf die Attribute. • Klassen beschreiben Objekte. • Mit Hilfe von Klassen können wir Objekte kreieren (instanziieren). Beispiel 1 2 4 6 8 10 12 14 16 18 20 Klasse zur Beschreibung eines Kontos: class account =: var nat kontonr; var string inhaber; var nat stand; method create = (nat nr, string inh): kontonr, inhaber, stand := nr, inh, 0; method buchen = (nat nr, string x, int betrag, var string report): if nr <> kontonr then report := "falsche Kontonummer" elif x <> inhaber then report := "falsche Inhaberangaben" elif betrag+stand < 0 then report := "Kontostand ungenuegend" else | report := "Buchung erfolgreich"; stand := stand + betrag; | fi method abfragen = ... method aenderung = ... ... endclass Warteschlange: 1 2 4 6 8 10 class Queue =: var seq data s; method create =: s := empty; method enq = (data x): s := conc (s, <x>); method deq = (var data x): s, x := rest(s), first(s); method isempty = bool: s ?= empty; endclass 9.1. KLASSEN UND OBJEKTE 9.1.2 103 Objekte Objekte werden durch die Nutzung der Klassen und die Methode create erzeugt. Ein Objekt besitzt einen eindeutigen Identifikator in Form einer Referenz. Wir erzeugen ein Objekt durch q := Queue.create (9.1) Dabei ist q eine Variable der Sorte Queue. Die Klassennamen werden also als Sorten für die Objektidentifikatoren der Objekte der entsprechenden Klasse benutzt. Durch (9.1) wird ein neues Objekt (wie in Pascal durch new) der entsprechenden Klasse erzeugt (eine Familie von Programmvariablen gemäß der Attribute) sowie eine Referenz darauf (der Objektidentifikator). Diese Referenz wird an q zugewiesen. Beispiel 1 Aufruf der Methode enq mit dem Parameter 27 für das Objekt q: q.enq(27) Zur Laufzeit eines objektorientierten Programms entspricht jede Klasse einer Menge von Objekten, die für diese Klasse generiert wurden. Historisch Die objektorientierte Programmierung entstand Ende der 60-er Jahre aus Simula 67. → O. J. Dahl, Nygaard, Oslo Smalltalk in den 70-er Jahren, Xerox Parc, C++, Eiffel, Java, ... Gründe für die Objektorientierung: • Graphische Benutzerschnittstellen sind so viel einfacher zu programmieren. • Viele Programmieraufgaben (Steuerung von Geräten) lassein sich objektorientiert sehr einfach und anschaulich programmieren. Wichtige Begriffe zur objektorientierten Programmierung: Persistenz: Die Attribute eines Objekts leben so lange wie ds Objekt, sind also beständig - persistent. • klassiche prozedurale Programmierung: 1 2 sort account = construct (nat ktonr, ...) var account x := ... proc buchen = (...)... x wird als globale Variable genutzt. Unterschied: In der objektorientierten Programmierung ist die Gültigkeit der Attribute eingeschränkt, der Zugriff nur über Methoden möglich, beliebig viele Exemplare (Objekte) erzeugbar. Schnittstelle (interface): Übergangspunkt zwischen zwei Systemteilen. Idee: System wird in Teile strukturiert (in der Objektorientierung: Klassen und Objekte). Schnittstelle eines Objekts: Die Methoden, die das Objekt zur Verfügung stellt (vgl. Signatur) Idee: Mehrfachverwendung von Klassen durch Klassenbibliotheken (framework), d.h. vorgefertigte Klassen, beschrieben durch Schnittstellen. 104 KAPITEL 9. OBJEKTORIENTIERTES PROGRAMMIEREN 9.2 Vererbung Ein weiteres wesentliches Element der Objektorientierung ist die Vererbung. Für Klassen können wir Klassifikationen angeben (Taxonomien). Fahrzeuge Beispiel Kettenfahrzeuge Räderfahrzeuge Einräder Vierräder PKW Pickup 3 Türen 5 Türen Die Objektorientierung nutzt Taxonomien, um Klassen zueinander in Beziehung zu setzen. Container SeqCont SetCont Stack Queue Set Multiset Dabei folgen die syntaktischen Schnittstellen und die vorhandenen Attribute der Taxonomie. 9.2.1 Vererbungsbeziehung Durch die Taxonomie entstehen Beziehungen zwischen Klassen. Wir sprechen von einer Vererbungsbeziehung. Im Beispiel: SeqCont erbt von Container. Sprechweise: SeqCont ist Unterklasse von Container, Container ist Oberklasse von SeqCont. Technisch heißt das: Eine Unterklasse erhält alle Attribute der Oberklasse und alle ihre Methoden, aber n. K. noch mehr. Idee: In einer Unterklasse werden die Methoden und Attribute der Oberklasse nicht erneut aufgeschrieben, sonder nur die Methoden / Attribute, die neu hinzukommen. 1 2 4 6 class Deque =: extends Queue method enql = (data x): s = conc (<x>, s) method deqr = (var data x): s, x := lrest(s), last(s) endclass Wichtige Beobachtung: Wir können Objekte der Sorte Deque nutzen, wenn wir Objekte der Sorte Queue benötigen. Dabei lässt sich kein Unterschied feststellen, solange wir die neu eingeführten Methoden nicht verwenden. Prinzip: Ersetzbarkeit der Oberklasse durch Unterklassen. Das bedeutet, dass wir an allen Stellen, wo wir Objekte der Oberklasse verwenden, auch Objekte der Unterklasse verwenden dürfen. 1 2 Deque x = Deque.create; ... Queue y = x; 9.2. VERERBUNG 105 Jeder Objektidentifikator einer Unterklasse ist auch Objektidentifikator der Oberklasse. Einfachster Fall: alle Attribute / Methoden der Oberklasse werden unverändert von der Unterklasse übernommen (vererbt). Überschreiben von Methoden: Bei der Vererbung können auch Methoden nicht unverändert übernommen, sondern überschrieben werden, d.h. in der Unterklasse wird für eine Methode der Oberklasse ein neuer Rumpf angegeben. Idee: Methoden werden den Spezifika der Unterklasse nach Bedarf angepasst. Problem: Nun muss festgelegt werden, welche Methode tatsächlich aufgerufen wird, wenn eine Programmvariable der Sorte Oberklasse mit einem Wert der Sorte Unterklasse belegt ist und eine Methode aufgerufen wird, die in der Oberklasse deklariert und in der Unterklasse überschrieben wird. Regel: Es wird immer die Methode angewandt, die dem Wert des Objekts entspricht, d.h. im obigen Fall die Methode der Unterklasse. Folge: Es kann i.a. erst zum Aufrufzeitpunkt (nicht aus der Aufschreibung) festgestellt werden, welche Methode ausgeführt wird, da dies von der aktuellen Sorte des Werts der Variablen abhängt. Wir sprechen von später Bindung (late binding). Achtung: I.a. arbeiten wir mit Vererbungshierarchien. Regel: Um die Methode zu identifizieren, die schließlich ausgeführt wird, gehen wir von der Klasse des Objekts aus, für das die Methode aufgerufen wird, und steigen im Vererbungsbaum nach oben, bis wir eine explizite Methodendeklaration finden. Polymorphie (Mehrgestaltigkeit) bezeichnet den Umstand, dass Variablen der Sorte Oberklasse durch Werte der Sorte Unterklasse belegt werden können. Einfachvererbung: Jede Klasse besitzt höchstens eine unmittelbare Oberklasse; die Vererbungsrelation stellt einen Baum dar. Mehrfachvererbung: Unterklassen können von mehreren Oberklassen unmittelbar erben. Beispiel 1 2 4 6 8 10 12 14 16 18 20 22 24 class Stack =: var m first; var Stack rest; var bool isempty; method create =: isempty := true; method push = (m x): if isempty then first := x; isempty := false; rest := nil; elif rest ?= nil then rest := Stack.create; rest.push (first); first := x; else rest.push (first); first := x; fi method pop = (var m x): if isempty then --> error elif (rest ?= nil) or rest.stackempty then x, isempty := first, true else 106 26 28 30 32 34 36 38 40 KAPITEL 9. OBJEKTORIENTIERTES PROGRAMMIEREN x := first; rest.pop (first); fi method stackempty = bool: isempty; endclass class StackCount =: extends Stack var nat c; method create =: c, isempty := 0, true; method push = (m x): c = c + 1; super.push(x); ... endclass Abschreckendes Beispiel: 1 2 4 6 8 10 12 14 class Queue =: extends Stack method push = (m x): if isempty then rest := nil; first := x; isempty := false; elif rest ?= nil then rest := Queue.create; rest.push (x); else rest.push(x); fi endclass Achtung: Das Beispiel zeigt einen Missbrauch der Idee der Vererbung: Warteschlangen können nicht Stacks ersetzen, ohne dass sich die Programme unterschiedlich verhalten. 9.2.2 Bemerkungen zur Objektorientierung In objektorientierten Sprachen gibt es eine Vielzahl von notationellen Besonderheiten und weiteren Konzepten, die bestimmte Programmierprobleme speziell unterstützen. • Exceptions Oft werden Zugriffsrechte speziell geregelt (public / private / final). Weitere Beispiele: this, super Abstrakte Klassen: Klassen, für die keine Objekte instanziiert werden können. Analog: abstrakte Methoden. Neben den bisher betrachteten Methoden / Attributen (die in Java Instanzmethoden / -attribute heißen), gibt es in einigen objektorientierten Sprachen (z.B. Java) auch Klassenmethoden / -attribute. Diese sind nicht Bestandteil eines einzelnen Objekts, sondern Bestandteil der Klasse. Beispiel Zähler, der die Anzahl der instanziierten Objekte der Klasse zählt. Teil II Rechnerstruktur, Hardware, Maschinennahe Programmierung 107 109 Stichworte: • Zeichenweise Darstellung von Information → Codierung • Schaltungen / Schaltwerke - Schaltlogik • Rechnerstrukturen: Aufbau von Rechneranlagen • Maschinennahe Programmierung 110 Kapitel 1 Codierung / Informationstheorie In der Codierung studieren wir die Darstellung von Information durch Zeichenfolgen (vgl. formale Sprache). 1.1 Codes / Codierung Wir konzentrieren uns stark auf eine Darstellung durch zwei Zeichen (Bits - Binary Digits). B = {L, 0} Bn Menge der Binärwörter der Länge n (n - Bit - Wörter) Sei A ein Alphabet (linear geordneter endlicher Zeichensatz), B ein Alphabet. c : A → B Code / Codierung c : A∗ → B ∗ Wortcodierung Wichtig auf einer Codierung ist die Umkehrbarkeit: d : {c(a) : c ∈ A} → A mit d(c(a)) = a. Voraussetzung: c injektiv. Binärcodierung: c : A → B∗ Achtung: B∗ ist lexikographisch linear geordnet (0 < L). 1.1.1 Binärcodes einheitlicher Länge Wir betrachten nun für gegebenes n ∈ N die Abbildung c : A → Bn . Achtung: Wir setzen voraus, dass c injektiv ist, aber nicht notwendigerweise surjektiv. Für zwei Codewörter aus Bn können wir nach dem Abstand fragen, wir sprechen von Hamming-Abstand: hd : Bn timesBn → N0 n X hd(ha1 , . . . , an i , hb1 , . . . , bn i) = d(ai , bi ) i=1 111 112 KAPITEL 1. CODIERUNG / INFORMATIONSTHEORIE wobei für x, y ∈ B gilt ( 1 d(x, y) = 0 falls x 6= y falls x = y Auf dieser Basis definieren wir einen Hammingabstand für einen Binärcode c: hd(c) = min {hd(c(a), c(b)) : a, b ∈ A ∧ a 6= b} hd(c) = 0 c nicht injektiv. Um den Hammingabstand hoch zu halten, benutzt man Paritätsbits. Beispiel Gegeben sei eine Codierung c : A → Bn mit hd(c) ≥ 1. Gesucht ist eine Codierung c0 : A → Bn+1 mit hd(c) ≥ 2. Wir definieren: ( L falls die Quersumme von b gerade ist pb : Bn → B, pb(b) = 0 sonst und für a ∈ A c0 (a) = c(a) ◦ hpb(c(a))i Behauptung: hd(c0 ) ≥ 2 Beweis: Seien a, a0 ∈ A, a 6= a0 gegeben. 1. Fall: hdc (a, a0 ) ≥ 2 ⇒ hdc0 (a, a0 ) ≥ 2 2. Fall: hdc (a, a0 ) = 1. Dann gilt pb(a) 6= pb(a0 ), da die Quersumme von a gerade ist, wenn die Quersumme von a0 ungerade ist und umgekehrt. Also ist hdc0 (a, a0 ) = 2. Wichtiges Thema der Codierung: Kryptographie: Verschlüsselung von Nachrichten. Weiteres Thema: Codierung der Ziffern. Direkter Code: c : {0, . . . , 9} → B4 c(z) 1-aus-10-Code z 0 0000 000000000L 1 000L 00000000L0 2 00L0 0000000L00 3 00LL 000000L000 4 0L00 00000L0000 0000L00000 5 0L0L 6 0LL0 000L000000 7 0LLL 00L0000000 8 L000 0L00000000 9 L00L L000000000 1.1.2 Codes variabler Länge Wir betrachten nun Codes, bei denen die Codewörter unterschiedliche Längen besitzen. Beispiele • altes Fernsprechwählsystem: 1 7→ L0 2 7→ LL0 .. . 0 7→ LLLLLLLLLL0 • Morsecode 1.1. CODES / CODIERUNG 1.1.3 113 Serien-/ Parallelwortcodierung Bei der Übertragung von binär codierter Information unterscheiden wir zwei Situationen: • ein Draht / sequentielle Übertragung • n Drähte (n > 1) / parallele Übertragung Bei Übertragung eines Wortes w ∈ A∗ mit Binärcodierung c : A → B∗ einfachstes Vorgehen: c∗ : A∗ → B∗ c∗ (ha1 , . . . , an i) = c(a1 ) ◦ c(a2 ) ◦ · · · ◦ c(an ) Kritische Frage: Ist c∗ injektiv, wenn c injektiv ist? Nein! Gegenbeispiel: Seien a1 , a2 ∈ A mit a1 6= a2 , c(a1 ) =< L > und c(a2 ) =< LL >. Dann ist c∗ (< a1 a1 a1 >) =< LLL >= c∗ (< a1 a2 >). Frage: Unter welchen Voraussetzungen ist die Antwort ja? Antwort: (1) Codes gleicher Länge (2) die Fano-Bedingung gilt Fano-Bedingung von Coole: Kein Codewort c(a1 ) für a1 ∈ A ist Präfix eines Codeworts c(a2 ) für a2 ∈ A mit a1 6= a2 . Behauptung: Gilt für c die Fano-Bedingung, so ist c∗ injektiv. Beweis (durch Widerspruch): Seien a = ha1 , . . . , an i , b = hb1 , . . . , bm i ∈ A∗ , a 6= b und c∗ (a) = c∗ (b). O.B.d.A. sei n < m. Sei i so gewählt, dass ak = bk für 1 ≤ k ≤ i und n = i oder ai+1 6= bi+1 . 1. Fall: ai+1 6= bi+1 ; dann gilt c(ai+1 ) Präfix von c(bi+1 ) oder c(bi+1 ) Präfix von c(ai+1 ). Widerspruch zur Fano-Bedingung. 2. Fall: n = i; dann gilt (wegen m > n) c∗ (hbi+1 , . . . , bm i) = ε. Widerspruch zur Fano-Bedingung. Parallelwortcodierung: c : A → Bn ck∗ : A∗ → (Bn )∗ ck∗ (ha1 , . . . , ak i) = hc(a1 )i ◦ · · · ◦ hc(ak )i Beispiel L 0 L 0 0 0 L L 0 0 L 0 L 0 L 0 0 0 L L 0 0 L 0 Trivial: ck∗ injektiv, falls c injektiv. 1.1.4 Codebäume Codes lassen sich durch Tabellen oder durch Codebäume (s. Abb. (1.1)) darstellen. B C D A L 0LL 0L0 00 Trivialerweise ist für Codes, dargestellt durch Codebäume, stets die Fano-Bedingung gegeben. 114 KAPITEL 1. CODIERUNG / INFORMATIONSTHEORIE L 0 A L 0 D L 0 B C Abbildung 1.1: Codebaum 1.2 Codes und Entscheidungsinformation Kernfrage: Wie viel Information trägt eine Nachricht? Prinzip: Um so ungewöhnlicher (seltener) eine Information ist, desto mehr Informationsgehalt hat sie. Dafür setzen wir gegebenenfalls Wahrscheinlichkeiten für Nachrichten voraus. Eine stochastische Nachrichtenquelle für ein endliches Alphabet A erzeugt eine Folge von Zeichen aus A, bei der zu jedem Zeitpunkt die Wahrscheinlichkeiten für jedes Zeichen einer vorgegebenen zeitunabhängigen Wahrscheinlichkeitsverteilung entsprechen. Beobachtung: Seltene Zeichen tragen viel Information, häufige Zeichen wenig. P Gegeben: A Alphabet, p : A → [0, 1] Wahrscheinlichkeiten (d.h. a∈A p(a) = 1 und p(a) > 0∀a ∈ A). Mittlerer Entscheidungsgehalt (Entropie): X 1 H= p(a) ld p(a) a∈A Entropie: Maß für Gleichverteilung der Wahrscheinlichkeiten: H groß ⇔ Wahrscheinlichkeiten etwa gleichverteilt, H klein ⇔ starke Unterschiede in den Wahrscheinlichkeiten. H Zeichen i p(i) 1 a 2 1 1 b Beispiel 2 3 a 4 ≈ 0, 8 1 b 4 Seien für die Zeichen aus A Wahrscheinlichkeiten p : A → [0, 1] sowie eine Binärcodierung c : A → B gegeben. Wir können die mittlere Wortlänge L berechnen als X L= p(a)|c(a)| a∈A wobei |c(a)| die Länge von c(a) bezeichnet. Um L klein zu halten, codieren wir Zeichen mit großen Wahrscheinlichkeiten mit kurzen Codewörtern. Größere Spielräume erhalten wir, wenn wir nicht Einzelzeichen, sondern Wörter aus A∗ codieren. → Huffman-Algorithmus P Jede Menge M ⊆ A von Zeichen hat auch eine Wahrscheinlichkeit p(M ) = a∈M p(a). Es ist p(A) = 1. Ziel: A so in zwei disjunkte Teilmengen A0 und AL aufteilen, dass p(A0 ) ≈ 12 ≈ p(AL ) ist. 1.3. SICHERUNG VON NACHRICHTENÜBERTRAGUNG Beispiel i a b p(i) 3 4 1 4 i aa ab ba bb 115 p(i) L= 9 16 3 16 3 16 1 16 1 X p(w)|c(w)| m m w∈A Shannonsches Codierungstheorem: (1) Für beliebige Wortcodes gilt H ≤ L. (2) Der Wert L − H kann durch geschickte Wahl der Wortcodes beliebig klein gemacht werden. Redundanz eines Codes: L − H Relative Redundanz eines Codes: 1 − H L Gesetz von Merkel: Die Reaktionszeit t einer Versuchsperson, um aus n Gegenständen einen bestimmten auszuwählen, lässt sich berechen durch t = 200 + 180 ld(n)[msec] 1.3 Sicherung von Nachrichtenübertragung Beim Übertragen von Nachrichten sind zwei Aspekte von besonderer Bedeutung: • Kapazität: Wie viele Informationseinheiten pro Zeiteinheit können übertragen werden? • Störung: Wie hoch ist die Wahrscheinlichkeit, dass Daten fehlerhaft übertragen werden? 1.3.1 Codesicherung Diskreter Kanal ohne Speicher: Überträgt Binärwerte, wobei Störungen auftreten können, deren Wahrscheinlichkeiten zu jedem Zeitpunkt gleich sind (vgl. stochastische Nachrichtenquelle). Irrtumswahrscheinlichkeiten: p0 Wahrscheinlichkeit, dass das Zeichen 0 als L übertragen wird pL Wahrscheinlichkeit, dass das Zeichen L als 0 übertragen wird einseitige Störung: (p0 = 0 ∧ pL > 0) ∨ (p0 > 0 ∧ pL = 0) symmetrische Störung: p0 = pL In manchen Fällen führt eine Störung nicht zu einer Veränderung L zu 0 oder 0 zu L, sondern führt auf ein Zeichen, das als fehlerhaft erkannt werden kann (Verlustzeichen). Wir erhalten eine Übertragung von Zeichen aus {0, L} in Zeichen aus {0, L, ⊥} (⊥: Fehlerzeichen). Ziel bei Kanälen mit Übertragungsfehlern: Die Wahrscheinlichkeit für Fehler soll möglichst klein gehalten werden, d.h. Fehler sollen erkannt und korrigiert werden (durch Redundanz). Lemma: Hat ein Code Hammingabstand h, so können Störungen, die weniger als h Zeichen betreffen, sicher erkannt werden. Idee: Prüf- / Paritätsbits einsetzen 116 1.3.2 KAPITEL 1. CODIERUNG / INFORMATIONSTHEORIE Übertragungssicherheit Kanäle haben beschränkte Übertrangugskapazität: s1 : Anzahl der Zeichen, die ein Kanal pro Zeiteinheit überträgt s0 : Anzahl der Zeichen, die pro Zeiteinheit übertragen werden sollen R = ss01 : Senderate R < 1: Überkapazität, d.h. Codesicherung durch Redundanz möglich R > 1: Unterkapazität: Nachricht kann nur in Teilen übertragen werden, kommt gestört an. Beispiel R = 31 → wir übertragen statt der Einzelzeichen jedes Zeichen drei Mal hintereinander. Bei Störungen mit pE < 0, 5 decodieren wir wie folgt: 000, 00L, 0L0, L00 → 0 Die Fehlerrate sei p für Einzelzeichenübertragung; dann ist 3p2 − 2p3 die Wahrscheinlichkeit einer Störung bei Dreifachübertragung 14 Liegt die Übertragungsrate bei 16 , dann können wir 14-Bit-Wörter als 16-BitWörter mit 2 Paritätsbits übertragen. Falls R ≥ 1 ist, reicht die Kapazität nicht aus. Trotzdem können wir Nachrichten (gestört) übertragen. Beispiel R = 3, d.h. wir sollen drei Mal so viel Nachrichten übertragen, als möglich ist. Idee: Drei Bits werden auf ein Bit komprimiert (Mehrheitstechnik. s.o.) und beim Empfang wieder in drei gleiche Bits umgewandelt. Irrtumswahrscheinlichkeit: (R > 1) pE = p R−1 + R 2R Kapitel 2 Binäre Schaltnetze und Schaltwerke Zur Darstellung diskreter (symbolischer) Information reichen zwei Werte (Bits) aus. Zur Darstellung technischer Art bits es viele Möglichkeiten: • elektrische: – Spannung / keine Spannung – Strom / kein Strom – Licht / kein Licht • mechanische: – Wasser / kein Wasser – Dampf / kein Dampf Wir betrachten im Weiteren nicht die technische Darstellung von Bits, sondern studieren, wie wir bestimmte Information (Zahlen, Datenstrukturen) durch Bitsequenzen darstellen können und wir wir Verarbeitungsvorgänge durch boolesche Funktionen realisieren können. 2.1 Boolesche Algebra / Boolesche Funktionen Die Wahrheitswerte bilden mit den Operationen ¬, ∨ und ∧ eine boolesche Algebra. Prädikate bilden ebenfalls eine boolesche Algebra: Statt einer Prädikatsdarstellung p : M → B können wir stets eine Mengendarstellung S ⊆ M verwenden. Jedes Prädikat über M definiert genau eine Teilmenge von M und umgekehrt. Sp = {x ∈ M : p(x)}: p(x) = (x ⊂ Sp) Allgemein gilt: ∧ ∼ ∩, ∨ ∼ ∪, ¬ ∼ Komplement und ⇒∼⊆. 2.1.1 Boolesche Funktionen n-stellige boolesche Funktion: f : Bn → B Beispiel: (1) 0-stellige boolesche Funktionen: Bn = {ε} Es gibt genau zwei 0-stellige boolesche Funktionen: true, false. 117 118 KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE (2) 1-stellige boolesche Funktionen: f : B → B true false x ¬x konstant L konstant 0 Identität Negation (3) 2-stellige boolesche Funktionen: f : B2 → B true false x y ¬x ¬y x∨y x∧y x ∨ ¬y ¬x ∨ y Konstanten (x ∧ y) ∨ (¬x ∧ ¬y) (x ∧ ¬y) ∨ (¬x ∧ y) ¬(x ∨ y) ¬(x ∧ y) x ∧ ¬y ¬x ∧ y Äquivalenz Antivalenz nor nand Bisubtraktion Projektionen negative Projektionen Disjunktion Konjunktion Inverse Implikation, Subjunktion Implikation (4) 3-stellige boolesche Funktionen: f : B3 → B ... n Anzahl der n-stelligen booleschen Funktionen 2(2 ) Für jedes n ∈ N bilden die n-stelligen booleschen Funktionen eine boolesche Algebra. BFn : Menge der booleschen Funktionen der Stelligkeit n. Seien f, g ∈ BFn . Dann sind (¬f ), (f ∧ g) ∈ BFn und es gilt: • (¬f )(x1 , . . . , xn ) = ¬f (x1 , . . . , xn ) • (f ∧ g)(x1 , . . . , xn ) = f (x1 , . . . , xn ) ∧ g(x1 , . . . , xn ) • (f ∨ g)(x1 , . . . , xn ) = f (x1 , . . . , xn ) ∨ g(x1 , . . . , xn ) Außerdem ist (f ∧ ¬f ), (f ∨ ¬f ) ∈ BFn . Wie lassen sich boolesche Funktionen darstellen? (1) Durch Formeln oder Aussagenlogik: Sei f ∈ BFn . Dann können wir f (x1 , . . . , xn ) durch die aussagenlogischen Terme über den Identifikatoren x1 , . . . , xn (z.B.: f (x1 , x2 , x3 ) = x1 ∨(¬x1 ∧x2 )) darstellen. 2.2. NORMALFORMEN BOOLESCHER FUNKTIONEN x1 x2 (2) Durch Tabellen: x3 f (x1 , x2 , x3 ) 0 0 0 0 0 0 L 0 0 L 0 L 0 L L L L 0 0 L 119 L 0 L L L L 0 L L L L L (3) Entscheidungsdiagramme (Bäume) 2.1.2 Partielle Ordnung auf BF Für eine beliebige boolesche Algebra definieren wir eine partielle Ordnung f ≥ g durch f ≥ g ⇔def f = f ∧ g. Aus den Gesetzen der booleschen Algebra lässt sich nachweisen, dass ≥ eine partielle Ordnung ist. f ≥ g ist gleichbedeutend mit f ⇒ g. f ∨ ¬f ist kleinstes Element: true (schwächste Aussage). f ∧ ¬f ist größtes Element: false (stärkste Aussage). 2.2 Normalformen boolescher Funktionen Eine boolesche Funktion kann immer durch boolesche Terme dargestellt werden. Es existieren viele syntaktisch verschiedene Terme, die die gleiche Funktion darstellen: (¬x1 ∧ ¬x2 ) ∨ (x1 ∧ x2 ) ≡ (x1 ⇒ x2 ) ∧ (x2 ⇒ x1 ) ≡ (¬x1 ∨ x2 ) ∧ (¬x2 ∨ x1 ) Wir behandeln im Weiteren folgende Fragen: (1) Gibt es einheitliche Termdarstellungen für BF (Normalformen)? (2) Wie können wir aus einer Tabellen- oder Entscheidungsbaumdarstellung eine Termdarstellung bekommen? (3) Wir können wir besonders einfache und kurze Termdarstellungen bekommen? Diese Fragen sind auch für die technische Realisierung von booleschen Funktionen durch Schaltungen entscheidend, da Termdarstellungen direkt in Schaltungen umgesetzt werden können. 2.2.1 Das boolesche Normalformtheorem Ziel: Wir wollen für beliebige boolesche Funktionen eine Termnormalform definieren und zeigen, wie diese aus einer Tabelle erzeugt werden kann. Wir definieren eine disjunktive Normalform (DNF): hdefi ::= hkfi {∨ hkfi}∗ |0|L hkfi ::= hliterali {∧ hliterali}∗ hliterali ::= {¬} hidi Beispiel (x1 ∧ ¬x2 ∧ x3 ∧ ¬x4 ) ∨ (¬x1 ∧ x2 ∧ x3 ∧ x4 ) ∨ (∧x1 ∧ ¬x2 ∧ x3 ∧ ¬x4 ) Als Identifikatoren wählen wir x1 , . . . , xn . In der vollständigen DNF kommt in jedem Literal jeder Identifikator genau ein Mal vor. Hilfskonstruktion: Seien b1 , . . . , bn ∈ B. Wir definieren einen Term minterm(b1 , . . . , bn ) = (v1 ∧ · · · ∧ vn ) wobei ( xi vi = ¬xi falls bi = L falls bi = 0 120 KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE Beispiel minterm(0, L, L, 0, L) = (¬x1 ∧ x2 ∧ x3 ∧ ¬x4 ∧ x5 ) Theorem: Für f ∈ BFn gilt: _ f (x1 , . . . , xn ) = (f (b1 , . . . , bn ) ∧ minterm (b1 , . . . , bn )) b1 ,...,bn ∈B Beweis: durch Induktion über n. Beispiel x1 x2 x3 f (x1 , x2 , x3 ) L L L L L L 0 L L 0 L 0 L 0 0 0 0 L L 0 0 L 0 L 0 0 L 0 0 0 0 0 f (x1 , x2 , x3 ) =(L ∧ x1 ∧ x2 ∧ x3 ) ∨ (L ∧ x1 ∧ x2 ∧ ¬x3 ) ∨ (0 ∧ x1 ∧ ¬x2 ∧ x3 ) ∨ (0 ∧ x1 ∧ ¬x2 ∧ ¬x3 ) ∨ (0 ∧ ¬x1 ∧ x3 ∧ x3 ) ∨ (L ∧ ¬x1 ∧ x2 ∧ ¬x3 ) ∨ (0 ∧ ¬x1 ∧ ¬x2 ∧ x3 ) ∨ (0 ∧ ¬x1 ∧ ¬x2 ∧ ¬x3 ) =(x1 ∧ x2 ∧ x3 ) ∨ (x1 ∧ x2 ∧ ¬x3 ) ∨ (¬x1 ∧ x2 ∧ ¬x3 ) Analog zur disjunktiven gibt es die konjunktive Normalform (KNF); die Rollen von und und oder sind dort vertauscht. Die DNF beantwortet die am Eingang gestellten Fragen. Jede boolesche Funktion hat eine eindeutige vollständige DNF. 2.2.2 Vereinfachte Normalformen (DNF) Die vollständige DNF lässt sich oft weiter vereinfachen. Regeln zur Vereinfachung . . . ∨ (t1 ∧ · · · ∧ ti−1 ∧ xi ∧ ti+1 ∧ · · · ∧ tn ) ∨ . . . ∨(t1 ∧ · · · ∧ ti−1 ∧ ¬xi ∧ ti+1 ∧ · · · ∧ tn ) ∨ . . . ⇒ · · · ∨ (t1 ∧ · · · ∧ ti−1 ∧ ti+1 ∧ · · · ∧ tn ) ∨ . . . Beispiel (x1 ∧ x2 ∧ x3 ) ∨ (x1 ∧ x2 ∧ ¬x3 ) ∨ (¬x1 ∧ x2 ∧ ¬x3 ) =(x1 ∧ x2 ) ∨ (¬x1 ∧ x2 ∧ ¬x3 ) Nun können Terme folgender Gestalt auftreten: . . . ∨ (t1 ∧ t2 ) ∨ · · · ∨ (t1 ) ∨ . . . . Diese Terme können ersetzt werden durch . . . ∨ (t1 ) ∨ . . . . Durch diese Regeln kann von der vollständigen DNF zur vereinfachten DNF übergegangen werden. Binäre Entscheidungsdiagramme Da die Identifikatoren geordnet sind (x1 , . . . , xn ) können wir Entscheidungsbäume für boolesche Funktionen konstruieren. • n = 0, d.h. f ∈ BF0 ; der Entscheidungsbaum ist ein Blatt b ∈ B, und zwar L falls f (ε) = L 0 falls f (ε) = 0 2.3. SCHALTNETZE 121 Abbildung 2.1: Beispiel eines Schaltplans • n > 0: Wir definieren den Entscheidungsbaum wie folgt: 0 L EBL EB0 wobei EBL der Entscheidungsbaum der booleschen Funktion fL ∈ BFn−1 mit fL (x1 , . . . , xn−1 = f (L, x1 , . . . , xn−1 ) und EB0 der Entscheidungsbaum zur booleschen Funktion f0 ∈ BFn−1 mit f0 (x1 , . . . , xn−1 ) = f (0, x1 , . . . , xn−1 ) seien. 2.3 Schaltnetze Zur Realisierung von boolschen Funktionen (wir sprechen ab jetzt von Schaltfunktionen) können Schaltnetze verwendet werden. Schaltnetze entsprechen gerichteten, azyklischen Graphen mit Eingängen und Ausgängen. Definition: Ein Schaltnetz mit n Eingängen und m Ausgängen ist ein gerichteter, azyklischer Graph mit n Eingangskanten und m Ausgangskanten, dessen Knoten mit den Namen boolscher Funktionen markiert sind. Schaltnetze sind mit boolschen Termen eng verwandt. Jeder boolsche Term lässt sich als ein Schaltnetz darstellen. Die so entstehenden Schaltnetze haben jedoch die Eigentümlichkeit, dass jede Kante genau ein Ziel hat. Deshalb führen wir im Folgenden neben boolschen Termen eine Funktionaltermdarstellung für boolsche Funktionen ein, durch die beliebige Schaltnetze mit Kanten mit Mehrfachziel (Verzweigung) dargestellt werden können. Im Folgenden behandeln wir eine Reihe boolscher Funktionen, ihre Darstellung durch boolsche Terme und durch Funktionsterme (funktionale Terme). Funktionsterme sind durch Verknüpfungen aus Grundfunktionen aufgebaut. Die Verknüpfungen entsprechen der graphischen Darstellung durch Schaltnetze. Der Schaltplan, gegeben durch das Schaltnetz, kann direkt in eine technische Realisierung umgesetzt werden. 2.3.1 Schaltfunktionen und Schaltnetze Schaltfunktion: f : Bn → Nm , m, n ∈ N. Schaltglied: (s. Abb. 2.2) Ein Schaltnetz besteht aus einer Menge von Schaltgliedern, die über Kanten verbunden sind. Es entsteht ein gerichteter Graph. In Schaltnetzen sind keine Zyklen zugelasen. Jede Kante hat genau eine Quelle, unter Umständen aber mehrere Ziele, d.h. Kanten können sich verzweigen. 122 KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE ...n... f ...m... Abbildung 2.2: Schaltglied ...n... & Abbildung 2.3: Konjunktions-Schaltglieder • Konjunktion: logisches • Disjunktion: logisches und (s. Abb. 2.3) oder (s. Abb. 2.4) • Negation: (s. Abb. 2.5) Sei ein Schaltnetz mit n Eingängen und m Ausgängen gegeben. Wird für jedes Schaltglied des Netzes eine Schaltfunktion angegeben, so definiert das Schaltnetz selbst wieder eine Schaltfunktion aus SFm n . In einem Schaltnetz entspricht • jedes Schaltglied einer Schaltfunktion • jede Kante einem Wahrheitswert 2.3.2 Darstellung von Schaltnetzen Es gibt zwei grundlegende Möglichkeiten, Schaltnetze durch Formeln darzustellen: (1) benannte Kanten (2) Kombination (Komposition) von Schaltgliedern Benannte Kanten: Jeder Knoten in einem Netz (vgl. Abb. 2.6) entspricht einer Gleichung (y1 , . . . , ym ) = f (x1 , . . . , xn ). Dieses Darstellungsverfahren eignet sich gut für kleine Schaltnetze ohne Regelmäßigkeiten. Besser für große Schaltnetze mit starken Regelmäßigkeiten eignen sich kombinatorische Schreibweisen. ...n... >= 1 Abbildung 2.4: Disjunktions-Schaltglieder 2.3. SCHALTNETZE 123 1 Abbildung 2.5: Negations-Schaltglieder x1 ... xn f y 1 ... y m Abbildung 2.6: Benannte Kanten m2 1 (1) Parallele Komposition (s. Abb. 2.7): Seien f1 ∈ SFm n1 und f2 ∈ SFn2 . Dann ist m1 +m2 f = (f1 k f2 ) ∈ SFn1 +n2 , und es gilt f (x1 , . . . , xn1 +n2 ) = (y1 , . . . , ym1 +m2 ), wobei f1 (x1 , . . . , xn1 ) = (y1 , . . . , ym1 ) f2 (xn1 +1 , . . . , xn1 +n2 ) = (ym1 +1 , . . . , ym1 +m2 ) 0 m (2) Sequentielle Komposition (s. Abb. 2.8): Seien f1 ∈ SFm n , f2 ∈ SFm . Es ist m0 f = (f1 ◦ f2 ) ∈ SFn mit f (x1 , . . . , xn ) = f2 (f1 (x1 , . . . , xn )). Eigenschaften: k, ◦ sind assoziativ, aber nicht kommutativ. Zusatzfunktionen • Projektion πin ∈ SF1n 1≤i≤n n πi (x1 , . . . , xn ) = xi • Identität I ∈ SF11 In ∈ SFnn I(x) = x In (x1 , . . . , xn ) = (x1 , . . . , xn ) f ... ... f1 f2 ... ... Abbildung 2.7: Parallele Komposition 124 KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE n ... f f1 m ... f2 ... m’ Abbildung 2.8: Sequentielle Komposition • Verzweigung V (x) = (x, x) V ∈ SF21 Vn ∈ SF2n n Vn (x1 , . . . , xn ) = (x1 , . . . , xn , x1 , . . . , xn ) • Permutation P ∈ SF22 Pn ∈ SFnn P (x1 , x2 ) = (x2 , x1 ) Pn (x1 , . . . , xn ) = (x2 , . . . , xn , x1 ) • Tupelung m2 1 f1 ∈ SFm n , f2 ∈ SFn 2 [f1 , f2 ] ∈ SFm−1+m n [f1 , f2 ] = (Vn ◦ (f1 k f2 )) • Senken U ∈ SF01 Un ∈ SF0n • Konstanten K(0), K(L) ∈ SF10 2.3.3 Der a b s u Kn (0), Kn (L) ∈ SFn0 Halbaddierer Halbaddierer ist ein Schaltglied HA ∈ SF22 ; HA(a, b) = (u, s). 0 L 0 L 0 0 L L 0 L L 0 0 0 0 L s = (¬a ∧ b) ∨ (a ∧ ¬b) u=a∧b Antivalenz Diese Gleichungen führen auf den Aufbau gemäß Abbildung 2.9. Umformung ergibt: s = ¬(¬(a∨b)∨(a∧b)). Damit kann der Halbaddierer effizienter aufgebaut werden (s. Abb. 2.10). 2.3. SCHALTNETZE 125 a b u s Abbildung 2.9: Ineffiziente Schaltung für Halbaddierer a b u s Abbildung 2.10: Effizientere Schaltung für Halbaddierer 126 KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE a b c VA u s Abbildung 2.11: Volladdierer a b c VA HA HA u s Abbildung 2.12: Volladdierer aus Halbaddierern 2.3.4 Arithmetische Schaltnetze Beim Addieren von Binärworten, die Zahlen binär darstellen, verwenden wir Volladdierer (s. Abb. 2.11). Tabelle: a 0 L 0 L 0 L 0 L b 0 0 L L 0 0 L L c 0 0 0 0 L L L L s 0 L L 0 L 0 0 L u 0 0 0 L 0 L L L n-stellige Binäraddition in Binärzahldarstellung: +2 ∈ SFn2n : han . . . a1 i +2 hbn . . . b1 i = hsn . . . s1 i Zuzüglich wird ein Übertrag u erzeugt: u = hun+1 . . . u1 i mit u1 = 0. Es gilt (uk+1 , sk ) = VA(ak , bk , uk ) Dies definiert s1 , . . . , sn , d.h. das Ergebnis hsn . . . s1 i (s. Abb. 2.13). un+1 zeigt an, ob die Arithmetik übergelaufen ist. Induktive Definition eines Addiernetzes: AN1 = VA ANn+1 = (I2 k An )(VA k In ) 2.3. SCHALTNETZE a n bn 127 un VA a 2 b2 a 1 b1 0 VA VA ... sn s2 u3 u n+1 s1 u2 Abbildung 2.13: Addiernetz für zwei n-Bit-Worte (vgl. Abb. 2.14) Analog definieren wir die Binärsubtraktion −2 : (Bn )2 → Bn , han . . . a1 i −2 hbn . . . b1 i = hsn . . . s1 i. si = (ai ∧ ¬bi ∧ ¬ui ) ∨ (¬ai ∧ bi ∧ ¬ui ) ∨ (¬ai ∧ ¬bi ∧ ui ) ∨ (ai ∧ bi ∧ ui ) u1 = 0 ui+1 = (¬ai ∧ bi ) ∨ (¬ai ∧ ui ) ∨ (bi ∧ ui ) Achtung: Der Übertrag repräsentiert nun einen negativen Wert. Bemerkung: Falls un+1 = L ist, dann gilt b >2 a (>2 : (Bn )2 → B). Die Subtraktion liefert als Abfallprodukt den Größenvergleich von a und b. 2.3.5 Zahldarstellung Positive Zahlen (natürliche Zahlen im Intervall [0, 2n − 1]) stellen wir durch die übliche Gewichtdarstellung dar: w (han . . . a1 i) = n X w(ai )2i−1 i=1 wobei w(L) = 1 und w(0) = 0 seien. Zur Darstellung negativer Zahlen: Statt Darstellung der Zahl durch den Absolutwert und ein Vorzeichen wählen wir eine Darstellung mit einem negativen Gewicht. Einerkomplementdarstellung c1 : [−2n + 1, 2n − 1] → Bn+1 d1 : Bn+1 → [−2n + 1, 2n − 1] d1 (c1 (z)) = z ∀z ∈ [−2n + 1, 2n − 1] d1 (hbn+1 . . . b1 i) = (−2n + 1)w(bn+1 + n X w(bi )2i−1 i=1 Achtung: Wir erhalten zwei Darstellungen der Null! d1 ( h0 . . . 0i ) = 0 = d1 ( hL . . . Li ) = −2n + 1 + | {z } | {z } positive Null negative Null n X 2i−1 i=1 Die Vorteile der Einerkomplementdarstellung sehen wir, wenn wir arithmetische Operationen ausführen. 128 KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE an a 1 b1 u bn ... AN n ... sn u’ a n+1 b n+1 an bn s1 a 1 b1 u ... AN n u’ VA u’’ sn ... s1 AN n+1 s n+1 Abbildung 2.14: zur induktiven Definition des Addiernetzes 2.3. SCHALTNETZE 129 Beispiel: Komplementbildung d1 (hbn+1 . . . b1 i) = z ⇒ c1 (−z) = h¬bn+1 . . . ¬b1 i Addition in der Einerkomplementdarstellung: Wir definieren hsn+2 . . . s1 i = h0an+1 . . . a1 i +2 h0bn+1 . . . b1 i hrn+2 . . . r1 i = han+1 an+1 . . . a1 i +2 hbn+1 bn+1 . . . b1 i +2 h0 . . . 0sn+2 i Behauptung: Bei der Addition der durch a und b in Einerkomplementdarstellung gegebenen Zahlen tritt genau dann ein Überlauf auf (d.h. die Summe der Zahlen liegt außerhalb von [−2n + 1, 2n − 1]), falls rn+1 6= rn+2 . an+1 an+1 an . . . a1 bn+1 bn+1 bn . . . b1 sn+1 sn . . . s1 • 1. Fall: an+1 = bn+1 = 0, d.h. beide Zahlen sind positiv, d.h. Überlauf genau dann, wenn un+1 = L, rn+2 = 0 ⇒ rn+2 6= rn+1 . • 2. Fall: an+1 6= bn+1 , d.h. eine negative und eine positive Zahl werden addiert. Gilt un+1 = 0, dann ist rn+1 = rn+2 = L. Gilt un+1 = L, dann ist rn+1 = r+2 = 0. • 3. Fall: an+1 = bn+1 = L. Beide Zahlen sind negativ. Nur wenn un+1 = L ist, findet kein Überlauf statt. Dann gilt rn+1 = rn+2 = L; sonst gilt rn+1 6= rn+2 ⇒ Überlauf Ergebnis: In Einerkomplementdarstellung können wir praktisch ohne Aufwand das Komplement bilden (negieren des Zahlenwerts) und bei Addition der Zahlen unabhängig von der Frage, ob eine der Zahlen (oder beide) negativ ist, ein einfaches Addiernetz verwenden. Zusätzlich können wir noch durch Verwendung eines zusählichen Bits einen arithmetischen Überlauf feststellen. Zweierkomplementdarstellung c2 : [−2n , 2n − 1] → Bn+1 d2 : Bn+1 → [−2n , 2n − 1] d2 (hbn+1 . . . b1 i) = −2n w(bn+1 ) + n X w(bi )2i−1 i=1 Bemerkungen: Nur eine Darstellung der Null! d2 (hL . . . Li) = −1. Komplementbildung: d2 (hbn+1 . . . b1 i) = z ⇒ c2 (−z) = h¬bn+1 . . . ¬b1 i +2 h0 . . . 0Li (Einer rückt auf bei der Komplementbildung.) Dafür wird die Addition einfacher: hrn+1 . . . r1 i = han+1 an+1 . . . a1 i +2 hbn+1 bn+1 . . . b1 i Wieder gilt: rn+2 6= rn+1 ⇔ Überlauf bei Addition. Achtung: Kein Einerrücklauf bei Addition! 130 KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE 2.3.6 Weitere arithmetische Operationen Auch Multiplikation und Division von Binärzahlen können wir durch Schaltnetze darstellen. Wir beschränken uns auf positive Zahlen. Multiplikation ∗2 : Bm × Bn → Bm+n , ham . . . an i ∗2 hbn . . . b1 i = hpm+n . . . p1 i. i Zur Definition von p verwenden wir eine Hilfsdefinition für ri = rm+n+ . . . r1i mit ( ak−i+1 ∧ bi falls i ≤ k ≤ m + i − 1 i rk = 0 sonst 1 n . . . r11 . Damit gilt hpm+n . . . p1 i = rm+n . . . r1n +2 · · · +2 rm+n Division /2 : Bm × Bn → Bm−n+1 für m ≥ n; ham . . . a1 i /2 hbn . . . b1 i = hqm−n+1 . . . q1 i. Wir setzen ham . . . a1 i <2 hbn . . . b1 0i voraus. Dies kann durch anfügen führenden Nullen (falls hbn . . . b1 i = 6 h0 werden. Wir definieren . . . 0i) immer erreicht eine Folge von Binärworten rni . . . r0i durch rn0 . . . r00 = ham . . . am−n+1 i. Gilt rni . . . r0i ≥2 h0bm . . . b1 i, so gilt qm−n−i+1 = L . . . r0i+1 = rest( rni . . . r0i −2 h0bm . . . b1 i) ◦ ham−n−i+2 i rni+1 Sonst ist qm−n−i+1 = 0 i rni−1 . . . r0i = rn−1 . . . r0i am−n−i+2 Fazit: Alle arithmetischen Operationen auf natürlichen, ganzen Zahlen und Binärbrüchen (Fixpunktschreibweise) können durch Schaltnetze realisiert werden. Achtung: Genauigkeit beschränkt - wir rechnen in einem endlichen Teilbereich der Zahlen. Gleiches gilt für Operationen der Aussagenlogik. 2.3.7 Schaltnetze zur Übertragung von Information Wir können über Leitungen Nachrichten übertragen. Sind unterschiedliche Partner an einer Übertragung beteiligt, so besteht oft der Wunsch, dass Nachrichten gezielt von einem Partner zum anderen übertrangen werden (Bsp. Telefon, E-Mail). Dies Erfordert eine Adressierung der Nachrichten (im Bsp. Telefonnummern, E-MailAdressen). Wir betrachten als elementares Beispiel Schaltungen mit Steuerleitungen. Sei {1, . . . , n} die Menge der Teilnehmer und c : {1, . . . , n} → Bm eine Codierfunktion, die jedem Teilnehmer einen Binärcode zuordnet. Weiter sei d : Bm → {1, . . . , m}. Forderung: d(c(i)) = i ∀i ∈ {1, . . . , n}. Einfaches - aus - n - Code * Beispiel: 1 + c(i) = . . 0} 0 . . 0}10 | .{z | .{z i−1 n−i Sei ferner eine weniger redundante Binärcodierung c0 für {1, . . . , n} gegeben: c0 : {1, . . . , n} → Bj DF : Bj → Bn DF(c0 (i))0c(i) ∀1 ≤ i ≤ n 2.4. SCHALTWERKE 131 Teilnehmer 1 j ... Teilnehmer n k ... . . . k ... DF n ... . k. . Abbildung 2.15: Multiplexer Eine Schaltung wie in Abbildung 2.15 bledent aus n Eingangsbündeln der Breite k alle bis auf eines aus; welches weitergegeben wird, bestimmt die Adresse für DF. Diese Schaltung nennen wir Multiplexer: MX : Bj × Bk·n → Bk Demultiplexer: DMX : Bj × Bk → Bk·m 2.4 Schaltwerke Bisher haben wir Schaltnetze betrachtet. Diese hatten keine Zyklen. Nun betrachten wir Schaltwerke. Diese sind wie Schaltwerke, allerdings sind nun Zyklen zugelassen. In der Praxis wird eine Schaltung nicht nur ein Mal benutzt (d.h. es wird nicht nur ein Binärwort eingegeben), sondern nacheinander wird eine Folge von Binärwörtern eingegeben und an den Ausgängen entsteht eine Folge von Binärworten. 2.4.1 Schaltwerksfunktionen Schaltfunktion: f : Bn → Bm Schaltwerksfunktion: g : (Bn )∗ → (Bm )∗ , wobei |g(x)| = |x| für x ∈ (Bn )∗ , d.h. jedes Eingabewort wird auf ein Ausgabewort abgebildet. Beispiel: Schaltwerksfunktion x1 . . . xn y1 . . . ym Zeitpunkt (Takt) (x1 ) L 0 L 0 1 (y 1 ) 2 (x ) 0 L 0 L 2 (y 2 ) 3 (x ) L 0 L 0 3 (y 3 ) 4 0 L 4 (y 4 ) (x ) 0 L 5 (x ) 0 L 0 L 5 (y 5 ) 6 (x ) L L L L 6 (y 6 ) .. .. .. .. .. .. .. . . . . . . . Achtung: Zwischen xi und y j dürfen auch für i 6= j Abhängigkeiten bestehen. Wichtig: Zeitflusseigenschaft (Kausalität): Ausgaben zum Zeitpunkt j dürfen höchstens von Eingaben zum Zeitpunkt i ≤ j abhängen. 132 KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE Präfixordnung auf (Bn )∗ : Für x, y ∈ (Bn )∗ gelte x v y ⇔def ∃z ∈ (Bn )∗ : x ◦ z = y Die Kausalität entspricht der Präfix-Monotonie: x v x0 ⇒ g(x) v g(x0 ). Schaltwerksfunktion: g : (Bn )∗ → (Bm )∗ mit |g(x)| = |x| ∀x ∈ (Bn )∗ und g präfixmonoton (d.h. Kausalität ist gegeben). 2.4.2 Schaltfunktionen als Schaltwerksfunktionen Gegeben sei eine Schaltfunktion f : Bn → Bm . Wir definieren eine Schaltwerksfunktion f ∗ : (Bn )∗ → (Bm )∗ durch f ∗ ( x1 , x2 , . . . , xk ) := f (x1 ), f (x2 ), . . . , f (xk ) Die Schaltwerksfunktion f ∗ hat die Eigenschaft, dass die Ausgabe f (xk ) zum Zeitpunkt k lediglich von der Eingabe xk zum Zeitpunkt k abhängt. Die Funktion speichert nichts von den früheren Eingaben. Solche Schaltwerksfunktionen heißen auch kombinatorisch, die anderen sequentiell. 2.4.3 Schaltwerke Ein Schaltwerk ist ein gerichteter Graph mit Knoten, die Schaltwerksfunktionen entsprechen, und Kanten, die Leitungen entsprechen. Jetzt sind Zyklen zugelassen. In der Praxis werden wir jedoch nur eingeschränkt Zyklen betrachten. 2.4.4 Schaltwerksfunktionen und endliche Automaten Ein endlicher Automat hat einen Zustand aus einer endlichen Zustandsmenge state. Er bekommt in einem gegebenen Zustand eine Eingabe aus einer Eingabemenge in und erzeugt eine Ausgabe y aus einer Ausgabemenge out und einen neuen Zustand aus state. Wir sprechen von einem Zustand. Wir stellen die Zustandsübergänge durch eine Zustandsübergangsfunktion δ : state × in → state × out dar. Bei Schaltwerken betrachten wir Automaten, bei denen alle drei Mengen durch Binärwörter dargestellt werden (z, m, n ∈ N): δ : Bz × Bn → Bz × Bm Solche Automaten lassen sich kompakt durch Zustandsübergangsdiagramme darstellen. Beispiel: B2 × B2 → B2 × B2 . Zustandsübergangsdiagramm: s. Abb. 2.16. Achtung: Bestimmte Binärkombinationen treten als Zustände und Eingänge nicht auf. Die Kreise stellen die Zustände dar. Ein Pfeil vom Kreis σ1 zum Kreis σ2 mit Beschriftung x/y steht für δ(σ1 , x) = (σ2 , y). Ein Zustandsübergang besteht in einem Wechsel von einem Zustand σ1 zu einem Zustand σ2 . Ausgelöst wird der Übergang von einer Eingabe x und er erzeugt eine Ausgabe y. Schaltwerke können in ihrem Verhalten durch 2.4. SCHALTWERKE 133 L0/L0 00/0L 0L/0L L0 0L 00/L0 L0/L0 0L/0L Abbildung 2.16: Zustandsübergangsdiagramm • Automaten mit Ein-/Ausgabe • Schaltwerksfunktion beschrieben werden. Dabei gilt: jede Schaltewerksfunktion definiert einen Automaten. Automaten definieren Schaltwerksfunktionen Gegeben sei δ : state × in → state × out. Jeder Zustand σ ∈ state definiert eine Schaltwerksfunktion fσ : in∗ → out∗ wie folgt: Seien a ∈ in, x ∈ in∗ und b ∈ out. fσ (hai ◦ x) = hbi ◦ fσ0 (x) ⇔ delta(σ, a) = (σ 0 , b) fσ beschreibt das Ein-/Ausgabeverhalten des Automaten, wenn er im Zustand σ gestartet wird. Beispiel: obiger Automat Es gilt fL0 (h00i ◦ x) = hL0i ◦ fL0 (x) und f0L (h00i ◦ x) = hoLi ◦ f0L (x), d.h. fσ (h00i ◦ x) = hσi ◦ fσ (x) (Lesegleichung: Durch Eingabe von h00i wird der Zustand ausgegeben (gelesen) und beibehalten). Für σ, σ 0 ∈ {0L, L0} gilt fσ (hσ 0 i ◦ x) = hσ 0 i ◦ fσ0 (x) (Schreibgleichung: Der Zustand σ 0 wird geschrieben: Die Eingabe von σ 0 ∈ {0L, L0} führt dazu, dass der Automat in den Zustand σ 0 übergeht). Fazit: Der Automat speichert durch seinen Zustand genau ein Bit, das beliebig oft gelesen oder überschrieben werden kann. Schaltwerksfunktionen definieren Automaten Wir ordnen einer Schaltwerksfunktion f : in∗ → out∗ einen Automaten δf : SWF × in → SWF × out zu. Dabei steht SWF für die Menge der Schaltwerksfunktioonen in∗ → out∗ . Dabei gelte für g, g 0 ∈ SWF, a ∈ in, b ∈ out: δf (g, a) = (g 0 , b), wobei b und g 0 definiert sind durch g(hai ◦ x) = hbi ◦ g 0 (x) für alle x ∈ in∗ . Da g eine Schaltwerksfunktion ist, ist b durch a eindeutig bestimmt. Es gilt g 0 (x) = rest(g(hai ◦ x)). Jede Funktion f ∈ SWF definiert einen Zustand des Automaten. Jedes Schaltwerk kann in seinem Verhalten entweder als Automat mit Ein-/Ausgabe oder als Schaltwerksfunktion beschrieben werden - die Darstellungen sind ineinander überführbar. 2.4.5 Schaltwerke zum Speichern: Verzögerungsnetze Rückkopplung: das Flip-Flop (s. Abb. 2.17) Es gilt Qneu = ¬(r ∨ Q), Qneu = ¬(s ∨ Q). Beispiel für das Schalten des Flip-Flops: • Schreiben: 134 KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE s Q Q r Abbildung 2.17: Flip-Flop: Nor-Latch L L L L L ... s r 0 0 0 0 0 ... Q 0 0 L L L ... Q L 0 0 0 0 ... Qneu 0 L L L L . . . Qneu 0 0 0 0 0 . . . Idee: Wir wiederholen die gleiche Eingabe aus s und r, bis die Q, Q stabilisieren. • Lesen: s r Q Q Qneu Qneu 0 0 0 L 0 L 0 0 0 L 0 L 0 0 0 L 0 L 0 0 0 L 0 L ... ... ... ... ... ... Das Flip-Flop ähnelt in seinem Schaltverhalten stark dem Automaten, den wir im vorhergehenden Beispiel beschrieben haben. Allerdings treten beim Schreiben Zwischenzustände auf. Das Flip-Flop stabilisiert sich nach einem Zwischenschritt. Allgemeine Feststellung: (1) Bei komplizierten Schaltungen können vorher Zwischenschritte erforderlich sein, um einen stabilen Zustand und eine stabile Ausgabe zu erreichen. (2) Unter gewissen Umständen tritt keine Stabilisierung ein. Bei Schaltungen, die sich stabilisieren, ergeben sich zwei Sichten: • Mikrosicht: Automat, Schaltwerksfunktion, in der der Stabilisationsvorgang sichtbar ist. • Makrosicht: Abstraktion der Mikrosicht. Wir wiederholen in der Mikrosicht die Eingabe, bis Stabilisierung eintritt, und nehmen die vorliegende Ausgabe als Ausgabe und den dann vorliegenden Zustand als neuen Zustand. Die Zahl, wie oft die Eingabe wiederholt wird, bis eine Stabilisierung erreicht wird, bestimmt die Frequenz und den Takt der Schaltung. 2.4.6 Klassen von Schaltwerken Typischerweise baut man nicht Schaltwerke mit beliebig komplizierten, verzögerungsfreien Rückkopplungsleitungen, sondern verwendet Flip-Flops zur Realisierung von Speichern und verwendet sonst nur verzögerte Rückkopplungen. 2.4. SCHALTWERKE 135 Verzögerung (delay): Dn : (Bn )∗ → (Bn )∗ a ∈ Bn Dna (hx1 . . . xn i) = hax1 . . . xn−1 i d.h. die Ausgabe ist um einen Takt verzögert. 2.4.7 Komposition von Schaltwerken und Schaltnetzen Aus Speichergliedern (Flip-Flops) und Verzögerungsgliedern können wir, unter Verwendung der Konzepte aus den Schaltnetzen, beliebig komplizierte und mächtige Schaltungen aufbauen: • Speicher • Verarbeitungswerke • Übertragungsnetze Damit lassen sich durch Schaltungen Bausteine (Chips) realisieren, aus denen dann Rechner aufgebaut werden können. 136 KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE Kapitel 3 Aufbau von Rechenanlagen Eine Rechenanlage kann zunächst als riesiges Schaltwerk begriffen werden. Diese Sichtweise zeigt uns, wie wir uns die Realisierung einer Rechenanlage vorstellen können, ist aber für die Nutzung und Programmierung nicht sehr hilfreich. Programme entsprechen auf Schaltwerksebene bestimmten gespeicherten Binkombinationen (Bitwörtern). Für das Entwickeln größerer Programme brauchen wir eine strukturierte Sicht auf die Rechenanlage, durch die Programme greifbarer werden. 3.1 Strukturierter Aufbau von Rechenanlagen Das Grobkonzept eines Rechners (von-Neumann-Architektur) ist sein 50 Jahren praktisch unverändert. Eine Rechenanlage besteht demnach aus: • Rechnerkern / Prozessor / CPU • Speicher • Verbindungshardware (Bussysteme) • Peripherie (Ein-/Ausgabevorrichtungen) Wir sind im Weiteren primär an der Programmierung von Rechenanlagen interessiert. Somit interessiert uns die physikalische Struktur nur insoweit, als sie die Programme auf Maschinenebene beeinflusst. Dabei stellt sich die Frage, mit welchen Mitteln die Struktur einer Rechenanlage modelliert wird. Wir verwenden die Idee der Zustandsmaschine um eine Rechenanlage zu beschreiben. Dabei konzentrieren wir uns auf die Zustandsübergänge, die durch die Ausführung von Befehlen ausgelöst werden. Der Zustand einer Rechenanlage ist gegeben durch die Belegung ihrer Speicherzellen und Register (spezielle zusätzliche Speicherzellen für besondere Aufgaben). Zur Erläuterung der Struktur einer Rechenanlage und ihrer Programmierung verwenden wir eine Maschine MI (angelehnt an die VAX von Digital). Die Bestandteile des Rechners, insbesondere seine Speicherzellen und Register, stellen wir durch Programmvariable geeigneter Sorten dar und die Wirkungsweise der Programme / Befehle durch zuweisungsorientierte Programme, die auf diesen Programmvariablen arbeiten. Wichtig: Im Speicher stehen Programme und Daten! Der Modellmaschine MI besteht im Prinzip aus vier Rechnerkernen, zwei E/AProzessoren, dem Arbeitsspeicher und dem Bussystem. 137 138 KAPITEL 3. AUFBAU VON RECHENANLAGEN Rechnerkern E/A−Prozessoren Steuerwerk Peripherie Rechenwerk Bus (Verbindung) Speichereinheit Abbildung 3.1: Schematischer Aufbau eines von-Neumann-Rechners Plattenspeicher Arbeitsspeicher E/A 1 E/A 2 Adressbus Datenbus Steuerbus Abbildung 3.2: Spezifische Rechnerarchitektur der MI . . . . . . Rechnerkern Terminals 3.1. STRUKTURIERTER AUFBAU VON RECHENANLAGEN 3.1.1 139 Der Rechnerkern Rechnerkern: Hardware-Betriebsmittel, welches autonom den Kontrollfluss steuert (entscheidet, welcher Befehl ausgeführt wird) und die Datentransformationen ausführt. Der Rechnerkern wird in Steuerewerk und Rechenwerk unterteilt. Das Steuerwerk enthält den Taktgeber und die Register, die die Information enthalten, die für die Ablaufsteuerung erforderlich ist (Befehlszähler, . . . ). Das Steuerwerk erzeugt auch die Steuersignale für die Ansteuerung des Rechenwerks. Das Rechenwerk enthält die Operandenregister und die Schaltnetze/-werke zur Ausführung der Operationen. Damit bestimmt das Rechenwerk den Befehlsvorrat. Die Register im Rechnerkern sind Speicherzellen mit extrem kurzen Zugriffszeiten. Die Bestandteile der MI werden durch folgende Deklarationen von Programmvariablen charakterisiert: 16 Register für 32-Bit-Wörter: var [0:31] array bit R0, ..., R15 Aufgaben: R0 : R13 frei verwendbar für Programmierung R14 Zeiger für Stapel (stack pointer, SP) R15 Befehlszähler (program counter, PC) Zusäzlich im Steuerewerk: var [0:7] array bit IR Instruktionsregister var [0:31] array bit PSL Prozessorstatusregister Weitere Hilfsregister für Steuer-/Rechenwerk: var [0:31] array bit tmp0, ..., tmp3 Hilfsregister für die ALU var [0:7] array bit am legt Adressiermodus fest var [0:31] array bit adr für die Adressrechnung var [0:31] array bit index für die Indexrechnung Die Anzahl der Register in einem Rechnerkern kann sehr stark schwanken (früher oft nur ein Register: Akkumulator, heute ganze Registerbänke). 3.1.2 Speichereinheit Die Speichereinheit besteht aus einer Folge von Speicherzellen, auf die über Adressen zugegriffen wird. MI: var [0:232 − 1] array [0:7] array bit M Speicher var [0:31] array bit MAR Speicheradressregister var [0:31] array bit MBR speicherregister 3.1.3 E/A Die Ein-/Ausgabe erfolgt über Peripheriegeräte durch die E/A-Prozessoren. (genaueres später) 3.1.4 Befehle und Daten auf Maschinenebene Auf Maschinenebene werden alle Daten und Befehle durch Bitwörter dargestellt (Binärdarstellung). Ein Befehl ist ein Binärwort, das im Hauptspeicher gespeichert werden kann und bei Ausführung (laden in den Rechnerkern und Interpretation) eine Zustandsänderung der Maschine bewirkt. Bestandteile eines Befehls: • Charakterisierung der Operation • Charakterisierung der Operanden 140 KAPITEL 3. AUFBAU VON RECHENANLAGEN Wir unterscheiden eine ganze Reihe unterschiedlicher Befehlsarten. Die Operanden kennzeichnen die Werte, mit denen wir arbeiten: Sie werden immer durch Bitwörter dargestellt, können aber ganz unterschiedliche Werte bezeichnen: • Adressen • Zahlen • Charakter Darstellung von Zahlen durch Bitwörter: natürliche Zahlen Binärschreibweise ganze Zahlen Zweierkomplementdarstellung Fixpunktdarstellung Darstellung von Zahlen aus [−1, 1[ Gleitpuntkdarstellung Darstellung von Zahlen durch Wert und Exponent Festpunktdarstellung: Zweierkomplementdarstellung (32 Bit), Multiplikation mit 2−31 . Gleitpunktdarstellung: b = hb0 b1 . . . bn bn+1 . . . bn+m i mit b0 : Vorzeichen, b1 . . . bn Exponent, bn+1 . . . bn+m Mantisse. Wir berechnen den dargestellten Zahlenwert mit Hilfe folgender Hilfswerte: e= n X 2n−i w(bi ) i=1 v= m X 2−i w(bn+1 ) i=1 Es gilt 0 ≤ e < 2n , 0 ≤ v < 1. exp = e − 2n−1 + 1 ⇒ −2n−1 + 1 ≤ exp ≤ 2n−1 man = 1 + v ⇒ 1 ≤ man < 2 Fälle der Interpretation: • Normalfall: −2n−1 + 1 < exp < 2n−1 . Wir erhalten als dargestellt Zahl (−2)w(b0 ) · 2exp · man • Darstellung der Null: e = 0 ∧ v = 0 • Unterlauf: e = 0 ∧ v 6= 0 • Überlauf: e = emax = 2n − 1 ∧ v = 0 • Fehlerfall: e = emax = 2n − 1 ∧ v 6= 0 Unterlauf, Überlauf und Fehlerfall sind keine gültigen Zahldarstellungen. Auf Maschinenebene werden alle Daten durch Bitsequenzen dargestellt. Sorte Kennung Wortlänge sort byte = [0:7] array bit B 8 sort half-word = [0:15] array bit H 16 sort word = [0:31] array bit W 32 sort floating-point = [0:31] array bit F 32 (8 + 23) sort double-float = [0:63] array bit D 64 (11 + 52) Problem: Einer Ziffernfolge 37216 ist nicht mehr anzusehen, ob sie in Oktal-, Dezimaloder Hexadezimaldarstellung ist. Ausweg: (37216)8 oktal, (37216)10 dezimal, (37216)16 hexadezimal Konvention: Wir verwenden Dezimaldarstellung und geben Oktal- und Hexadezimaldarstellung explizit an. 3.1. STRUKTURIERTER AUFBAU VON RECHENANLAGEN 141 Auch Befehle werden auf Maschinenebene durch Bitsequenzen darstestellt. Bequemer und lesbarer sind Darstellungen, bei denen die Teile des Befehls durch Namen udn Dezimalzahlen dargestellt werden. Bestandteile eines Befehls: • Operation: Angabe, welche Umformung bzw. welche Umspeicherung ausgeführt wird • Operandenspezifikation: Angabe, mit welchen Werten / Speicherzellen / Registern dabei gearbeitet wird Syntax: hcommandi ::= hinstructionParti {hopParti {, hopParti}∗ } Es gibt Maschinen mit starrem Befehlsformat (festgelegte Länge der Bitsequenz). Nachteil: Jeder Befehl hat die gleiche Anzahl von Bits für Operandenspezifikationen zur Verfügung. Vorteil: Jeder Befehl passt genau in eine Speicherstelle, das Ansteuern von Befehlen wird einfacher. Die MI hat flexible Befehlsformate. Der Instruktionsteil (Operationsangabe) des Befehls wird im Rechnerkern (im Steuerwerk) in Steuerbits umgesetzt, die die entsprechenden Umformungen und Umspeicherungen auslösen. 3.1.5 Operandenspezifikation und Adressrechnung Jeder Befehl arbeitet mit bestimmten Werten (Bitsequenzen) und bestimmten Speicherplätzen / Registern. Wir sprechen von Operanden. Beispiel: Einfachste Art der Operandenspezifikation: wir geben das Register W R9 (W: Kennung, R9: Register) oder die Speicherzelle W 1743 direkt an. Damit werden die Speicherzellen 1743-1746 angesprochen. Die einfachste Form der Angabe von Operanden ist die direkte (absolute) Adressierung: Es werden Register / Adresse explizit angegeben. Weil wir an mehr Flexibilität interessiert sind und uns in einem Programm nicht festlegen wollen, an welcher Stelle im Speicher es (absolut) steht, verwenden wir kompliziertere Adressierverfahren. Dazu verwenden wir eine Operandenspezifikation. Dies ist eine Angabe, aus der wir die Operanden berechnen können (Adressrechnung). Jeder Operand entspricht einer Lokation (einem Speicherplatz oder Register; genauer: einem Ausschnitt aus einem Speicher / Register oder einer Folge). Ein Operand bestimmt einen bestimmten Platz (Lokation) in der Maschine, an der eine Bitsequenz gespeicher ist. sort operand = register (type k, nat n) | memo (type k, nat n) sort type = {B, H, W, F, D} Eine Operandenspezifikation ist ein syntaktischer Ausdruck, der angibt, wie der Operand zu berechnen ist. Jede Operandenspezifikation wird bei Ausführung eines Befehls im Befehlszyklusin einen Operanden umgerechnet. Dazu verwenden wir folgende Hilfsprozeduren: 1 2 4 6 8 fct val = (operand x) seq bit: if x in register then R[n(x)][32-wl(k(x)):31] else get (wl(k(x))/8, n(x)) fi fct get = (nat i, nat n) seq bit: if i = 0 then empty else conc (M[n], get(i-1, n+1)) fi 142 KAPITEL 3. AUFBAU VON RECHENANLAGEN Adressierarten: • direkte Adressierung: Die absolute Adresse (oder das Register) wird im Adressteil angegeben. • direkte Angabe des Operandenwerts im Befehl als Binär-, Hexadezimal- oder Dezimalzahl. • relative Adressierung: Zum Adressteil im Befehl wird der Inhalt eines Registers addiert. • indirekte Adressierung / Adresssubstitution: Die Speicherzelle, die über den Adressteil angesteuert wird, enthält selbst eine Adresse der Speicherzelle, die den Operanden bildet. • Indizierung: Zur ermittelten Adresse wird ein weiterer Wert addiert (typische Anwendung: hochzählen eines Zählers in einer Wiederholungsanweisung zum Zugriff auf Feldelemente) BNF-Syntax der MI-Operandenspezifikation: hopParti ::= habsoluteAddressi | himmediateOpi | hregisteri | hrelativeAddressi | hindexedRelativeAddressi | hindirectAddressi | hindexedIndirectAddressi | hstackAddressi Ein Teil der Adressierart wird durch folgende Werte bestimmt: 1 2 4 sort operandSpec = opspec (nat reg, bool isr, bool idr, incr icr, nat rel, indexSpec int) sort indexSpec = {-} | reg (nat) sort incr = {-1 | 0 | 1 } • absolute Adressierung: habsoluteAddressi ::= hintexpi (integer expression) • Operand als Wert: himmediateOpi ::= I hintexpi • Register als Operand: hregisteri ::= R0| . . . |R15|P C|SP • relative Adresse: hrelativeAddressi ::= {hintexpi +}! hregisteri • indizierte relative Adressierung: hindexedRelativeAddressi ::= hrelativeAddressi hindexi hindexi ::= / hregisteri / 3.1. STRUKTURIERTER AUFBAU VON RECHENANLAGEN 143 • indirekte Adressierung: hindirectAddressi ::=!(hrelativeAddressi)|!! hregisteri • indizierte indirekte Adressierung: hindexedIndirectAddressi ::= hindirectAddressi hindexi • Stack-Adressierung: hstackAddressi ::= −! hregisteri |! hregisteri + 3.1.6 Der Befehlszyklus Der Befehlszyklus bezeichnet die Folge der Schritte, die bei der Ausführung eines Befehls ausgeführt werden. Die Maschinen durchlaufen immer wieder die gleiche Sequenz von Schritten und führen dabei jeweils einen Befehl aus. Wir beschreiben den Befehlszyklus wiederum durch ein Programm. In der MI besteht ein Befehl aus einem Operationsteil und bis zu vier Operanden. In der Maschine ist der Operationsteil durch ein Byte repräsentiert. In diesem Byte sind die folgenden Informationen verschlüsselt: fct args = (byte) nat Anzahl der Operanden fct kenn = (byte) type Kennung fct result = (byte) bool Wird ein Resultat erzeugt? Der Befehlszyklus der MI entspricht dem folgenden Programm. 1 2 4 6 8 10 12 14 16 18 20 22 24 26 28 while not(stop) do fetch_operator; if 1 <= args(IR) then fetch_operand (kenn(IR), tmp1[32-wl(kenn(IR)):31]) fi; if 2 <= args(IR) then fetch_operand (kenn(IR), tmp2[32-wl(kenn(IR)):31]) fi; if 2 <= args(IR) then fetch_operand (kenn(IR), tmp3[32-wl(kenn(IR)):31]) fi; execute (IR); if result (IR) then put_result fi od proc fetch_operator =: fetch (B, R15, IR); R15 := R15 + 1 proc fetch = (type k, [0:31] array bit adr, var array[0:wl(k)-1] array bit r): MAR := adr; for i := 1 to wl(k)/8 do MBR[(i-1)*8:i*8-1] := M[MAR]; MAR := MAR + 1 od; r := MBR[0:wl(k)-1] 144 KAPITEL 3. AUFBAU VON RECHENANLAGEN 30 32 34 36 38 proc put = (type k, [0:31] array bit adr, var array[0:wl(k)-1] array bit r): MAR := adr; MBR[0:wl(k)-1] := r; for i := 1 to wl(k)/8 do M[MAR] := MBR[(i-1)*8:i*8-1]; MAR := MAR + 1; od Für die Adressrechnung zur Ermittlung der Operanden benötigen wir eine weitere Information, die aus dem Byte ermittelt wird, das den Adressiermodus bestimmt: fct reg = (byte) nat Registerzahl fct isr = (byte) bool steht Operand im Register? fct isrel = (byte) bool relative Adressierung? fct idr = (byte) bool indirekte Adressierung? fct isidx = (byte) bool Indexadressierung? fct icr = (byte) incr Registerinkrement / -dekrement Bei der Ausführung eines Befehls (execute) wird nicht nur ein Ergebnis berechnet und zurückgeschrieben. Es werden auch bestimmte Bits in einem zusätzlichen Register (dem Prozessorstatusregister) gesetzt. Dabei sind folgende vier Bits für uns von besonderer Bedeutung: C carry Übertrag V overflow Überlauf Z zero Resultat ist Null N negative Resultat ist negativ Die Schritte zum Bereitstellen der Operanden und der Abspeicherung des Resultats sind für alle Befehle, abhängig von der Zahl der Argumente und der Frage, ob ein Resultat anfällt, gleich. Die Wirkung eines Befehls wird durch die Rechenvorschrift proc execute = (byte): ... beschrieben. 3.2 Hardwarekomponenten Wir unterscheiden folgende Bestandteile einer Rechenanlage: • Prozessor • Speicher – Hauptspeicher – Hintergrundspeicher – Speichermittel für das Archivieren • Eingabegeräte • Ausgabegeräte • Datenübertragungsgeräte / Netze Kapitel 4 Maschinennahe Programmierung Die im letzten Kapitel eingeführten Programmstrukturen beschreiben am Beispiel der MI die Wirkungsweise eines Rechners. Nun führen wir die einzelnen Befehle ein, beschreiben ihre Wirkung und formulieren erste kurze Maschinenprogramme. Grundsätzliche Feststellungen: • Ein Maschinenprogramm ist eine endliche Folge von Befehlen, die zur Ausführungszeit im Hauptspeicher stehen. Damit hat jeder Befehl eine absolute und eine relative Adresse. • Diese Befehle werden, gesteuert durch den IC (PC) nacheinander ausgeführt. • Jedes Programm, das nicht in Maschinensprache geschrieben ist, muss – von Hand oder durch ein Übersetzungsprogramm (Compiler) in Maschinensprache übersetzt werden, bevor es ausgeführt werden kann, oder – ein Interpretationsprogramm (Interpreter) angegeben werden; dies ist ein in Maschinensprache gegebenes Programm, das das Programm in Programmiersprache durchläuft und dabei die entsprechenden Befehle ausführt. 4.1 Maschinennahe Programmiersprachen Struktur, Form und Umfang eines Befehlssatzes und der einzelnen Befehle wird durch den Rechnerkern geprägt. Maschinenprogramme sind Folgen von Befehlen und weisen somit keine besondere Struktur auf. Es ist Aufgabe des Programmierers, Maschinenprogramme strukturiert zu schreiben. Deshalb: ! Trickreiche Programmierung vermeiden! ! Transparente Programmstrukturierung anstreben! ! Ausreichend dokumentieren! ! Umfangreiche Programme in Programmteile strukturieren! 145 146 KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG 4.1.1 Binärwörter als Befehle In der MI entspricht ein Programm zur Ausführungszeit einer Folgt von Bytes im Speicher. In dieser Form ist ein Programm für Menschen praktisch unlesbar. ∗ hbinaryMachineCommandi ::= hbytei hbinaryAddressi ::={0|L}8L hbinaryMachineProgrammi ::={hbinaryAddressi hbytei}∗ |{hbinaryAddressi hbinaryMachineCommandi} Wir vermeiden diese Form der Befehlsangabe und wählen eine etwas lesbarere. Dabei werden Befehle durch Angabe der Befehlsbezeichnung, des Typs der Operanden und der Adressen in Dezimalschreibweise beschrieben. 4.1.2 Der Befehlsvorrat der MI Die Wirkung eines Befehls wird durch die Rechenvorschrift execute im Befehlszyklus festgelegt. Dabei werden gewisse Hilfsregister / Spezialregister und insbesondere das PSL (Prozessorstatusregister) geändert. Beispiele für Befehle der MI: • Transportbefehle - Umspeichern von Informationen MOVE W 1000, 2000 Es wird der Inhalt der Speicherzellen 1000 - 1003 in tmp1 geladen, in tmp0 umgesetzt und in die Speicherzellen 2000 - 2003 zurückgespeichert. MOVE B 1000, 2000 Der Inhalt der Speicherzelle 1000 wird mit Hilfe der temporären Register in die Zelle 2000 geschrieben. MOVE B 1000, R4 R4[24:31] wird durch das Byte in Zelle 1000 überschrieben. MOVEA 1000, 2000 (move address) ist wirkungsgleich zu MOVE W I1000, 2000: der Wert (die Adresse) 1000 wird den Speicherzellen 2000 - 2003 zugewiesen. CLEAR B R7 Dieser Befehl bewirkt das Überschreiben von R7[24:31] durch das Byte 0016 . • Logische Operationen OR W R8, 3001, R1 bewirkt bitweise Disjunktion der 32 Bit in R8 mit den 32 Bit in 3001 - 3004; das Resultat wird in Register R1 geschrieben. Typische Anwendung: Maskieren von Werten. • Arithmetische Operationen ADD, SUB, MULT, DIV Wirkung entsprechend der Zahldarstellung (gesteuert über die Kennung), s. später • Vergleichsoperationen CMP W 3007, R6 Dieser Befehl dient dazu, gewisse Bits im PSL zu setzen. Im Beispiel werden die 32 Bit in den Zellen 3007 - 3010 in tmp1 und die 32 Bit aus R6 in tmp2 geschrieben. Dann wird tmp1 mit tmp2 verglichen: 1 2 Z := (tmp1 = tmp2) N := (tmp1 < tmp2) 4.1. MASCHINENNAHE PROGRAMMIERSPRACHEN 147 • In Sprungbefehlen können wir nun auf die so gesetzten Bits Bezug nehmen. JUMP 5000 Einfacher (unbedingter) Sprung: der Befehlszähler wird mit Adresse 5000 belegt. JEQ 5000 (jump equal): Der Befehlszähler wird mit Adresse 5000 belegt, falls Z=L gilt; anderenfalls hat der Befehl keine Wirkung. • Shiftbefehle erlauben es, die Bits in einem Operanden zu verschieben. (im Bsp: Rechtsshift um 2 Bit) LL000L0L 0LLL000L Zwei Bits werden frei und können entweder durch Nachziehen von 0 oder L belegt werden oder durch kreisförmiges Durchschieben. Dieser Befehlssatz ist nur ein Ausschnitt der wichtigsten Befehle der MI. Reale Maschinen haben oft erheblich umfangreichere Befehlssätze, wobei oft nur Abkürzungen für Befehle oder das Zusammenfassen mehrerer Befehle zu einem dadurch möglich wird. Wir haben als wichtige Befehle nur die ausgeklammert, die für die Systemprogrammierung von Bedeutung sind. 4.1.3 Einfache Maschinenprogramme Zur Ausführungszeit der Programme stehen Daten und Programme gleichermaßen binär codiert im Speicher. Dabei ist es ratsam, Daten und Programme in getrennten Speicherbereichen abzulegen. Beispiel: Speicherorganisation Adresse (hex) Verwendung 0000 0000 reserviert 0000 0400 Programmspeicher 3FFF FFFF 4000 0000 Kontrollbereich (Daten, Stack) 7FFF FFFF 8000 0000 Systembereich BFFF FFFF C000 0000 reserviert für Systemprogramme FFFF FFFF Die im Beispiel verwendeten Adressen sind virtuell, d.h. die tatsächlich im Hauptspeicher vorhandene Menge von Adressen ist erheblich kleiner. Zur Ausführungszeit werden den virtuellen Adressen richtige Adressen zugeordnet. Ein Teil der Daten steht im Hintergrundspeicher und wird bei Bedarf in den Hauptspeicher geladen. Zur Erhöhung der Lesbarkeit unserer Programme arbeiten wir mit Adressen, ohne uns um die Frage zu kümmern, ob diese virtuell oder real sind, und verwenden in Programmen symbolische Adressen für Sprungbefehle. Beispiele für Maschinenprogramme 148 KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG (1) Berechnen des Maximums zweier Zahlen: Seien zwei Zahlen (32 Bit) im Speicher durch die Adressen in den Registern R1 und R2 gegeben. Das Maximum der Zahlen soll in Register R0 geschrieben werden. 1 2 4 6 CMP W !R1, !R2 JLE max2 MOVE W !R1, R0 JUMP ende max2: MOVE W !R2, R0 ende: (2) Durchsuchen eines Speicherabschnitts. Gegeben seien Adressen in R0 und R1, wobei R0 ¡ R1 gelte. Der Speicher von R0 bis R1 soll nach einem 32-Bit-Wert, der in R2 steht, durchsucht werden. Falls der Wert gefunden wird, ist die Adresse dazu in das Register R3 zu schreiben. Anderenfalls wird 0 nach R3 geschrieben. Bei etwas komplizierteren Aufgaben ist es hilfreich, die Lösung zuerst als problemorientiertes Programm zu formulieren und dann erst das MI-Programm zu schreiben. 1 2 4 6 8 R3 := R0; suche: if M[R3] = else R3 := if R3 fi; R3 := fi ende: R2 then goto ende R3 + 4; <= R1 then goto suche 0 Umsetzung in ein MI-Programm: 1 2 4 6 8 MOVE W R0, R3 suche: CMP W !R3, R2 JEQ ende ADD W I4, R3 CMP W R3, R1 JLE suche CLEAR W R3 ende: (3) Sortieren der Inhalte eines Speicherbereichs. Aufgabe: Man sortiere die Bytes im Speicher zwischen der Adresse in R0 und der Adresse in R1 aufsteigend. R2, . . . , R11 dürfen als Hilfsregister verwendet werden. Wir verwenden einen einfachen Algorithmus: 1 2 4 6 8 R2 := R0; while R2 < R1 do if M[R2] > M[R2+1] then M[R2], M[R2+1] := M[R2+1], M[R2]; if R0 < R2 then R2 := R2 - 1 fi else R2 := R2 + 1 fi od 4.1. MASCHINENNAHE PROGRAMMIERSPRACHEN 149 Wir brechen das Programm in Sprungbefehle auf: 1 2 4 6 8 10 12 R2 := R0; if R2 >= R1 then goto ende fi; R3 := R2 + 1; if M[R2] > M[R3] then goto vertauschen fi; R2 := R3; goto m_while; vertauschen: R4[24:31] := M[R2]; M[R2] := M[R3]; M[R3] := R4[24:31]; if R0 = R2 then goto m_while; R2 := R2 - 1; goto m_while; ende: m_while: Nun ist das Schreiben des MI-Programms einfach: 1 2 4 6 8 10 12 14 16 4.1.4 MOVE W R0, R2 CMP W R1, R2 JLE ende ADD W I1, R2, R3 CMP B !R3, !R3 JLT vertauschen MOVE W R3, R2 JUMP loop vertauschen: MOVE B !R2, R4 MOVE B !R3, !R2 MOVE B R4, !R3 CMP W R0, R2 JEQ loop SUB W I1, R2 JUMP loop ende: loop: Assemblersprachen Assemblersprachen sind maschinennahe Programmiersprachen, die durch geringfügige Erweiterung reiner Maschinensprachen entstehen. Die Umsetzung von Assemblersprachen in reine Maschinensprachen erfolgt durch Programme, sogenannte Assemblierer (engl. assembler). Typische Erweiterungen in Assembler sind: • mehrfache Marken • eingeschränkte arithmetische Ausdrücke • direkte Operanden (in der MI ohnehin vorgesehen) • symbolische Adressierung (analog zu Programmvariablen) • Segmentierung (Bindungs- und Gültigkeitsbereiche für Namen) • Definition und Einsetzung von Substitutionstexten (Makros) • Substitution von Makros = Makroexpansion Über die geschickte Einführung von Makros können wir auf bestimmte Anwendungssituationen zugeschnittene Assemblerstile definieren. 150 4.1.5 KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG Ein- und Mehradressform Eine wesentliche Charakteristik einer Maschinensprache ist die Anzahl der Operanden pro Befehl. • starres Befehlsformat: einheitliche Zahl von 0, 1, 2, 3 Operanden pro Befehl • flexibles Befehlsformat (siehe MI): Befehle haben zwischen 0 und n ∈ N Operanden Bei starrem Befehlsformat sprechen wir von Einadressform, wenn jeder Befehl genau einen Operanden enthält (entsprechend Zwei- und Dreiadressform). Wir erhalten bei Dreiadressform folgende Syntax: hbefehli ::= hregisteri ::=[hoperandi | − hoperandi |AC | hoperandi hoperatori hoperandi] if hbedingungi then goto hmarkei f i |if |skip goto hmarkei |goto hbefehlsfolgei ::={{hmarkei}∗ hbefehli}∗ hspeedi ::= hkonstantei |AC |h[1]|h[2]| . . . |a|b| . . . hbedingungi ::= hregisteri = 0 | hregisteri = 6 0 | hregisteri < 0 | hregisteri ≤ 0 hoperatori ::={+, −, ∗, /, . . .} hregisteri ::=AC |a|b| . . . |h[1]|h[2]| . . . Wir sprechen von einem Programm in Einadressform, wenn in jedem Befehl außer AC höchstens eine weitere Registerangabe auftritt. Der AC ist das zentrale Rechenregister, auf dem alle Berechnungen ausgeführt werden. Achtung: Dreiadressformen können immer in Einadressformen übersetzt werden. Beispiel a := b + c Dreiadressform wird umgeschrieben in AC := b AC := AC + c a := AC 4.1. MASCHINENNAHE PROGRAMMIERSPRACHEN 4.1.6 151 Unterprogrammtechniken Wird eine Folge von Befehlen an mehreren Stellen in einem Maschinenprogramm benötigt, so bietet es sich an, das Programm nicht mehrfach zu schreiben, sondern • ein Makro zu definieren • ein Unterprogramm zu definieren, d.h. ein Programmstück, auf das über eine Marke gesprungen werden kann, und das über einen Rücksprung verlassen wird. Beim Springen in ein Unterprogramm sind folgende Daten zu übergeben (d.h. abzuspeichern, so dass das Unterprogramm darauf Zugriff hat): • Rücksprungadresse • Parameter Beim Rücksprung sind unter Umständen gewisse Resultate aus dem Unterprogramm an das aufrufende Programm zu übergeben. Techniken zur Übergabe der Daten: • Abspeichern in bestimmten Zellen im Unter- oder Hauptprogramm • Abspeichern in Registern • Abspeichern im Stack Wichtig: Einheitliche, für das ganze Programm gültige Regeln für die Übergabe der Parameter und Rücksprungadresse festlegen. Geschlossenes Unterprogramm: Ansprung erfolgt über Anfangsadresse, Rücksprung über die vor Ansprung angegebene Adresse. Rekursives Unterprogramm: Aus dem Unterprogramm wird die Anfangsadresse angesprungen. Dann muss die Rückkehradrese im Stack verwaltet werden. Beim offenen Einbau eines Unterprogramms (insbesondere beim Einsatz von Makroexpansion) kann Rekursion nicht behandelt werden. Die MI sieht spezielle Befehle für Unterprogrammaufrufe vor, die jedoch auch durch Befehle ersetzt werden können, die den Stack manipulieren und durch Sprungbefehle: • CALL speichert den aktuellen Stand des Befehlszählers (Register PC) im Stack ab und setzt den Befehlszähler auf die angegebene Adresse. • RET bewirkt die Rückkehr an die Adresse hinter der Aufrufstelle. Diese wird aus dem Stack in den PC geladen und der Stack wird zurückgesetzt. Beispiel: Aufruf eines Unterprogramms zur Berechnung der Fakultät MOVE W ..., R0 (Argument in R0 ablegen) CALL fac (Unterprogramm aufrufen) MOVE W R2, ... (Resultat abspeicher) Wichtig: Durch Unterprogrammaufrufe werden Teile des Speichers und der Register verändert. Für die Nutzung eines Unterprogramms ist es wichtig zu wissen: • wie Parameter / Resultat übergeben werden • welche Teile des Speichers / der Register durch das Unterprogramm verändert werden Bei einer strukturierten Programmierung auf Maschinenebene kommt der systematischen Nutzung von Unterprogrammen eine entscheidende Bedeutung zu. Unterprogramme sind sorgfältig zu dokumentieren. 152 KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG Im allgemeinen Fall arbeitet das Hauptprogramm mit allen Registern. Das gleiche gilt für das Unterprogramm. Deshalb ist es naheliegend, vor dem Aufruf des Unterprogramms alle Register zu retten (im Speicher zwischenzuspeichern). Dann kann das Unterprogramm alle Register frei nutzen. Nach Rücksprung werden die alten Registerwerte restauriert. Bei der MI gibt es einen speziellen Befehl zum Retten der Register im Stack: PUSHR bewirkt ein Abspeichern der Register R14 bis R0 auf dem Stack entsprechend der folgenden Befehlssequenz: 1 2 4 SUB W I4, R14 MOVE W R14, !R14 SUB W I4, R14 MOVE W R13, !R14 ... Der Stackpointer wird insgesamt um 60 dekrementiert und die Registerinhalte werden im Stack gespeichert. Der Befehl POPR bewirkt das Restaurieren der Register aus dem Stack. Der Stackpointer wird um 60 erhöht. Ratsam ist es, bei der Verwendung von Unterprogrammen eine einheitliche Standardschnittstelle zu nutzen. Prinzipien: • Im aufrufenden Programm werden die Parameter des Unterprogramms in der Reihenfolge pn , . . . , p1 auf dem Stack abgelegt. Für ein Ergebnis wird ebenfalls Speicherplatz im Stack reserviert. Dann erfolgt der Aufruf des Unterprogramms. Die Rückkehradresse steht im Stack. • Im Unterprogramm werden die Registerwerte gesichert. Der nun erreichte Stackpointer-Stand wird in R13 abgelegt. R13 dient also als Basisadresse für den Stackpointer-Stand zur Aufrufzeit. In R12 tragen wir die Basisadresse der Parameter ein. Bei der Rückkehr aus dem Unterprogramm können wir diese Basisadresse verwenden, um den alten Stack-Pegel wiederherzustellen. • Nach der Ausführung des Unterprogramms wird über POPR der Registerzustand wiederhergestellt und über RET erfolgt der Rücksprung. • Nach Rückkehr stehen die Parameter und das Resultat im Stack. Nach Zurücksetzen des Stacks um die Parameter steht das Resultat zu Beginn des Stacks und kann abgespeichert werden. Also: 1 2 4 6 ... MOVE kn pn, -!SP ... MOVE k1 p1, -!SP CALL up ADD W I..., SP ... 8 10 12 14 up: PUSHR MOVE W SP, R13 MOVEA 64+!R13, R12 ... MOVE W R13, SP POPR RET 4.2. ADRESSIERTECHNIKEN UND SPEICHERVERWALTUNG 153 Beispiel: ggt 1 2 MOVE W I0, -!SP MOVE W ..., -!SP MOVE W ..., -!SP CALL ggt ADD W I8, SP MOVE W !SP+, ... 4 6 8 10 12 14 16 18 20 22 24 26 ggt: PUSHR MOVE W SP, R13 MOVEA 64+!R13, R12 CMP W 4+!R12, !R12 JNE else then: MOVE W !R12, 8+!R12 JUMP rueck else: JLT op1lop2 SUB W 4+!R12, !R12 JUMP reccall op1lop2: SUB W !R12, 4+!R12 reccall: MOVE W I0, -!SP MOVE W !R12, -!SP MOVE W 4+!R12, -!SP CALL ggt ADD W I8, SP MOVE W !SP+, 8+!R12 rueck: MOVE W R13, SP POPR RET 4.2 Adressiertechniken und Speicherverwaltung In höheren Programmiersprachen werden Datenstrukturen zur strukturierten Darstellung von Informationen verwendet: • Grunddatentypen • Enumerationssorten • Records • Union • Array • rekursive Datensorte • Referenzen Im Folgenden interessiert uns die Frage, wie wir diese Datenstrukturen auf Maschinenebene darstellen und darauf zugreifen können. 4.2.1 Konstante Eine Konstante entspricht einer Operandenspezifikation, die einen Wert repräsentiert, ohne dabei auf Register oder den Speicher Bezug zu nemen. In der MI schreiben wir z.B. I 2735. Zur Ausführungszeit ist der Wert 2735 in Binärdarstellung im 154 KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG Hauptspeicher abgelegt. Zur Ausführungszeit des entsprechenden Befehls zeigt der Befehlszähler in der Adressrechnung auf die Adresse im Hauptspeicher, unter der der Wert abgelegt ist. 4.2.2 Operandenversorgung über Register Bei der Operandenversorgung über Register sind die Zugriffszeiten in der Regel geringer als beim Zugriff auf den Hauptspeicher. In der MI: Rn, 0 ≤ n ≤ 15. 4.2.3 Absolute Adressierung Im Befehl wird die Adresse als Wert angegeben. Dies bedeutet, dass das Programm bzw. seine Daten im Hauptspeicher fixiert sind, d.h. die Lage im Hauptspeicher festgelegt ist. Dies bedeutet eine Inflexibilität, die zu vermeiden ist. Absolute Adressierung macht höchstens für eine Reihe von Systemprogrammen Sinn. 4.2.4 Relative Adressierung Idee: Wir adressieren relativ zu einer frei festlegbaren Anfangsadresse für • den Programmcode • die Daten Ein Maschinenprogramm heißt relativ adressiert, wenn es keine Adressteile bzw. zur Verwendung als Adressteile vorgesehene Operandenteile enthält, die auf eine absolute Lage des Programms oder seiner Daten im Speicher Bezug nehmen. Nur dann können wir das Programm und seine Daten an beliebigen Stellen im Speicher ansiedeln. Für die Methodik wichtige Frage: Ist die Tatsache, dass ein Maschinenprogramm relativ adressiert ist, einfach zu erkennen und zu überprüfen (Codierstandards)? In der MI ist natürlich syntaktisch erkennbar, ob ausschließlich mit relativer Adressierung in der Operandenspezifikation gearbeitet wird. Dies garantiert allerdings noch nicht, dass das Programm relativ adressiert ist. (Achtung: Unterschied Syntax / Semantik!) 4.2.5 Indizierung, Zugriff auf Felder In höheren Programmiersprachen verwenden wir Felder ([n:m] array m a). Wir behandeln nun die Frage, wie solche Felder in einer Maschine (im Hauptspeicher) abgelegt werden und wie systematisch darauf zugegriffen wird. Wir legen die Elemente eines Felds konsekutiv im Speicher ab. Um den Zugriff zu organisieren, verwenden wir folgende Darstellung: Adresse Inhalt 4711 4711 + 1 − n fiktive Anfangsadresse a0 für a[0] 4712 a[n] 4713 a[n+1] .. .. . . 4711+m a[m] Für den Index k, n ≤ k ≤ m, erhalten wir durch k + a0 die Adresse des Feldelements. Sei α die Anfangsadresse des Felds. Dann schreiben wir in die Speicherzelle die fiktive Adresse α + 1 − n. Vorteil: Wenig Speicher für Verwaltungsinformationen zum Feld, uniformer Zugriff. Nachteil: keine vollständigen Informationen zur Überprüfung, ob der Index außerhalb der Indexgrenzen liegt. 4.2. ADRESSIERTECHNIKEN UND SPEICHERVERWALTUNG 155 In der MI: Enthält R0 die fiktive Anfangsadresse des Felds und R1 den Index k, so wird auf a[k] durch !R0/R1/ zugegriffen. Enthält R0 hingegen die effektive Anfangsadresse des Felds, so erfolgt der Zugriff auf das k-te Feldelement durch !!R0/R1/. Diese Zugriffstechnik ist problemlos mit der relativen Adressierung kombinierbar. Die Zugriffstechnik setzt voraus, dass zur Ausführungszeit beim Anlegen des Felds (auf dem Stack) die Feldgrenzen festliegen und sich nicht mehr ändern. Mehrstufige Felder können nach dem selben Prinzip behandelt werden: Sei [n1:m1, ..., ni:mi] array m a mit nq ≤ mq für 1 ≤ q ≤ i gegeben. Wir verwenden folgendes Schema: Adresse Inhalt 4711 a0 fiktive Adresse von a[0, ..., 0] 4712 s1 Spanne m1 − n1 .. .. .. . . . 4711+i-1 si−1 4711+i a[n1, ..., ni] Fiktive Anfangsadresse: Spanne mi−1 − ni−1 erstes Feldelement a0 = α + i − (n1 + s1 (n2 + s2 (. . . ))) Adresse des Elements a[k1, ..., ki] (mit nq ≤ kq ≤ mk für 1 ≤ q ≤ i): a0 + k1 + s1 (k2 + s2 (. . . )) Beispiel: 1 2 4 Einfacher Algorithmus auf Feldern (alle Feldelemente addieren) v := 0; for i := 1 to n do v := v + a[i] od Werte in der MI: R0 Anfangsadresse der Feldelemente (zeigt auf das erste Feldelement a[1]) R1 Zähler R2 Abbruchswert R3 Aufnahme des Resultats 1 2 4 6 8 MOVE W I n, R2 CLEAR W R3 CLEAR W R1 for: CMP W R2, R1 JLT ende ADD W !R0/R1/, R3 ADD W I 1, R1 JUMP for ende: HALT 4.2.6 Symbolische Adressierung Auch die relative Adressierung ist noch sehr fehleranfällig, da der Programmierer in Registern und Zahlen sein Programm verstehen muss. Bequemer und weniger fehleranfällig ist das Arbeiten in Programmen mit frei gewählten Bezeichnungen. Bei Assemblerprogrammen sprechen wir von symbolischen Adressen. Bei frei gewählen Bezeichnungen sind folgende Prinzipien zu beachten: • sprechende Bezeichnungen 156 KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG • data dictionary anlegen: ein Verzeichnis aller Begriffe und deren Bedeutung • Codierungsstandards für die Wahl von Bezeichnungen Vor der Ausführung des Programms werden die Bezeichnungen vom Assemblierer durch Adressen ersetzt. Dazu benutzt man eine Datenstruktur Adressbuch, in der die Beziehung Bezeichnung – relative Adresse festgelegt ist. Dabei ist darauf zu achten, dass diese Zuordnung so gewählt ist, dass das Programm effizient ist: • wenig Speicher verbraucht • kurze Ausführungszeiten hat Dazu werden Programme gezielt optimiert (unter Einsatz von Pipelining und Caching). 4.2.7 Geflechtsstrukturen und indirekte Adressierung Bei problemorientierten Programmiersprachen findet sich das Konzept Referenz / Verweis / Zeiger / Pointer. Auf der MI-Ebene bilden wir Referenzen auf Adressen ab. Die Referenz auf einen Wert oder eine Variable wird zu der Adresse der Speicherzelle, die diesen Wert enthält oder diese Variable repräsentiet. Bei umfangreichen Datenpaketen / -strukturen (beispielsweise Feldern) entsprechen die Referenzen den Anfangsadressen des Datenpakets. Referenzen entsprechen den Konzept der indirekten Adressierung. Referenzen / Verweise bieten große Vorteile für die Effizienz von Programmen: • Statt großer Datenpakete können wir in Prozedur- / Unterprogrammaufrufen die Verweise / Anfangsadresse übergeben. • In Geflechtsstrukturen können wir gewisse Daten, die für eine Reihe von Programmelementen bedeutsam sind, einheitlich verwalten (data sharing). Beispiel: Eine Typvereinbarung für Pointer / Records, aus der wir verkettete Listen (aber auch Bäume) aufbauen können, lautet wie folgt: 1 2 4 6 type rlist = ^slist; list = record inhalt: s; naechster, letzter: rlist end; Die Abbildung auf den Speicher (MI) könnte - unter der Annahme, dass der Typ s der Kennung W entspricht - so aussehen, dass ein Knoten aus 3 * 4 Bytes besteht, von denen je ein 4-Byte-Block die Adresse naechster, letzter und den abgelegten Wert enthält. Wir nehmen an, dass durch die Listenelemente eine zweifach verkettete Liste aufgebaut wird, wobei im ersten Element der Verweis letzter nil ist und im letzten Listenelement der Verweis naechster nil ist. Durchlaufen der Liste: 1 2 4 zeiger := listenanfang; while zeiger <> nil do if zeiger^.inhalt = suchmuster then goto gefunden fi; 4.2. ADRESSIERTECHNIKEN UND SPEICHERVERWALTUNG 6 8 zeiger := zeiger^.naechster od goto nicht_gefunden R0 Darstellung in der MI: R1 R2 1 2 4 6 Zeiger Listenanfang Suchmusteradresse MOVE W R1, R0 loop: CMP W I 0, R0 JEQ nicht_gefunden CMP W !R2, 8+!R0 JEQ gefunden MOVE W !R0, R0 JUMP loop Programm für das Anhängen eines Listenelements am Ende der Liste: 1 2 4 6 8 10 12 14 if listenanfang <> nil then zeiger := listenanfang; while zeiger^.naechster <> nil do zeiger := zeiger^.naechster od; zeiger^.naechster := neu; neu^.letzter := zeiger; neu^.inhalt := ...; neu^.naechster := nil else listenanfang := neu; neu^.letzter := zeiger; neu^.inhalt := ...; neu^.naechster := nil fi R0 R2 R3 R4 1 2 4 6 8 10 12 14 16 Zeiger Listenanfang neu inhalt MOVE W R2, R0 CMP W I 0, R0 JNE loop CLEAR W !R3 CLEAR W 4+!R3 MOVE W R4, 8+!R3 MOVE W R3, R2 JUMP ende loop: CMP W I 0, !R0 JEQ listenende MOVE W !R0, R0 JUMP loop listenende: MOVE W R3, !R0 CLEAR W !!R0 MOVE W R0, 4+!!R0 MOVE W R4, 8+!!R0 157 158 4.2.8 KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG Speicherverwaltung Für die Ausführung eines Maschinenprogramms mit komplexen Daten ist eine Speicherorganisation erforderlich. Darunter verstehen wir die Systematik der Ablage von Programm und Daten im Speicher. Ziele dabei sind: • effiziente Nutzung des Speichers • einfache, gut handhabbare Strukturen Beobachtungen: • Praktisch alle Programme benötigen während ihrer Ausführung zusätzlichen Speicherplatz, der stückweise belegt und wieder freigegeben wird. • Oft werden Speicherplätze in der umgekehrten Reihenfolge, in der sie belegt werden, wieder freigegeben (Stack-Prinzip) - der zuletztbelegte Platz wird zuerst wieder freigegeben. • Für gewisse Datenstrukturen eignet sich das Stack-Prinzip nicht (Zeiger- / Referenzstrukturen). Damit nutzen wir drei Arten der Verwaltung von Speicher: • statischer Speicher: Hier werden alle Daten abgelegt, die über die gesamte Laufzeit des Programms benötigt werden. • Stack: Hier werden alle Daten abgelegt, die nach dem Stack-Prinzip benötigt und freigegeben werden. Bei der Blockstruktur wird beim Betreten eines Blocks der Speicher für die lokalen Variablen (bei Prozeduraufrufen für Parameter) bereitgestellt und beim Verlassen des Blocks wieder freigegeben. • Listenspeicher (Heap): Hier werden alle Daten abgelegt, deren Speicherplätze nicht im Stack verwaltet werden können. Dabei wird im Quellprogramm in der Regel Speicherplatz explizit angefordert (Kreieren von Objekten, new/allocate zum Bereitstellen von Speicher für Zeiger). Die Freigabe von Speicher erfolgt: – entweder explizit (deallocate) Vorteil: Der Programmierer kann die Speicherverwaltung kontrollieren. Nachteil: Erhöhte Komplexität der Programme, Gefahr ins Leere zeigender Referenzen (dangling references). – oder implizit durch Speicherbereinigung (garbage collection). Dabei wird durch Erreichbarkeitsanalyse festgestellt, auf welche Zeigerelemente / Objekte noch zugegriffen werden kann; die nicht mehr erreichbaren werden entfernt. Vorteil: erheblich einfacher, keine Fehlergefahr durch Zeiger ins Leere. Nachteil: weniger effizient, Echtzeitverhalten der Programme kaum vorhersagbar. 4.2.9 Stackverwaltung von blockstrukturierten Sprachen Eine Programmiersprache heißt blockstrukturiert, wenn lokale Variablen / Konstanten verwendet werden, die in abgegrenzten Bindungsbereichen deklariert werden, soweit keine Referenzen / Zeiger auftreten. Für lokale Bezeichner / Variablen unterscheiden wir: • Deklaration / Bindung (genau ein Mal) • benutzendes Auftreten 4.2. ADRESSIERTECHNIKEN UND SPEICHERVERWALTUNG 159 Da die gleiche Bezeichnung in unterschiedlichen Bindungsbereichen gebunden werden kann, ist die Zuordnung zwischen Bindung und Nutzung wichtig (vgl. statische gegen dynamische Bindung). Blockstrukturen entsprechen Klammerstrukturen. Durch Durchnummerieren der Blockklammern kann jeder Block eindeutig gekennzeichnet werden. Durch dieses Verfahren können wir Blöcke eindeutig kennzeichnen. Blockschachtelungstiefe (BST): Anzahl der einen Block umfassenden Blöcke + 1. Die Blöcke definieren einen Baum. Beispiel: 1 2 4 6 8 10 Die Blockstruktur begin begin begin end begin end end begin end end entspricht dem Baum 1 1.1 1.2 1.1.1 1.1.2 In diesem Fall bestimmt die statische Struktur der Blöcke die Struktur des Stacks. Beispiel: 1 2 4 Blockorientiertes Programm mit Prozeduraufruf | // 1 proc p =: | // 1.1 ... | // 1.1 6 8 | // 1.2 ... | // 1.2 10 12 14 16 | // 1.3 ... p; ... | // 1.3 | // 1 Hier unterscheiden wir für jeden Abschnitt im Stack (der den lokalen Variablen eines Blocks entspricht) den statischen Vorgänger (in der Aufschreibung der nächste umfassende Block) und den dynamischen Vorgänger (in der Ausführung der nächste umfassende Block) (s. Abb 4.1). In der Speicherorganisation für blockorientierte Sprachen verwenden wir einen Stack, in dem jeweils auch der Verweis auf den statischen und dynamischen Vorgänger sowie die Blockschachtelungstiefe abgelegt wird. Ein Prozeduraufruf entspricht 160 KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG 1.1 1 1 ... 1.3 1.3 1.3 1 1 1 1 Abbildung 4.1: Blöcke auf dem Stack imer dem Betreten eines Blocks. Jeder Speicherabschnitt auf dem Stack enthält schematisch folgende Informationen: • statischer Vorgänger • dynamischer Vorgänger • Blockschachtelungstiefe • Rückkehradresse (bei Prozeduraufrufen) • Parameter (bei Prozeduraufrufen) • Resultat (bei Prozeduraufrufen) • lokale Variablen 4.3 Techniken maschinennaher Programmierung Ein komplexes maschinennahes Programm kann kaum in einem Anlauf gleich als Maschinenprogramm geschrieben werden. Geschickter ist es, zunächst ein problemorientiertes Programm zu schreiben, dies zu analysieren und zu verifizieren und dann in ein Maschinenprogramm umzusetzen. Die Umsetzung erfolgt • entweder per Hand • oder durch ein Übersetzungsprogramm (Compiler) Typische Bestandteile höherer Programmiersprachen: • Datenstrukturen • Blockstrukturen • Ablaufstrukturen – Ausdrücke – Zuweisungen – if - then - else – Wiederholungsanweisungen – Funktionen, Prozeduren Maschinennahe Umsetzung: • Speicherung • Umsetzung in Konzepte maschinennaher Sprachen (s.u.) 4.3. TECHNIKEN MASCHINENNAHER PROGRAMMIERUNG 4.3.1 161 Auswertung von Ausdrücken / Termen Ein Ausdruck besteht aus einem Gebirge von Funktionsaufrufen. abs(ab − cd) + ((e − f )(g − j)) + abs - * - - * efgj * a bcd Zugehöriges Single-Assignment-Programm: 1 2 4 6 8 h1 h2 h3 h4 h5 h6 h7 h8 := := := := := := := := a * b; c * d; h1 - h2; abs (h3); e - f; g - j; h5 * h6; h4 + h7; := := := := := := := := a * b; c * d; h1 - h2; abs (h1); e - f; g - j; h2 * h3; h1 + h2; oder: 1 2 4 6 8 h1 h2 h1 h1 h2 h3 h2 h1 Zur Optimierung des Speicherbedarfs beim Auswerten von Ausdrücken verwenden wir statt Hilfsvariablen einen Stack. Operationen finden immer auf dem ersten Element des Stacks statt. Deshalb kann das erste Element auch in einer speziellen Hilfsvariable AC gehalten werden. Operationen: • lege Wert auf den Stack • führe ein- oder zweistellige Operation auf dem Stack aus Das Arbeiten auf dem Stack zur Auswertung eines Ausdrucks kann einfach durch die Postfix-Darstellung des Operatorbaums erreicht werden. im Beispiel: 1 2 4 a b * c d * - abs e f - g j - * + AC := a push AC; AC := b AC := top * AC; pop push AC; AC := c 162 6 8 KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG push AC; AD := d AC := top * AC; pop AC := top - AC; pop AC := abs (AC) ... In der MI können wir ohne Verwendung eines Registers direkt auf dem Stack arbeiten: 1 2 MOVE W a, -!SP MULT W !SP+, !SP ... Wir schreiben nun ein Programm, das einem kleinen Ausschnitt aus einem Compiler entspricht. Es nimmt die Umsetzung von Ausdrücken in eine Sequenz von Operationen auf dem Stack vor. (Das erzeugte Programm legt schließlich das Ergebnis in AC ab, der Stack ist nach Ausführung des Programms wieder im Anfangszustand.) Wir benötigen eine Datenstruktur zur Darstellung von Operatorbäumen. 1 2 sort expr = nullary (symbol sym) | mono (op op, expr e0) | duo (expr e1, op op, expr e2) Dabei nehmen wir an, dass die Sorten symbol (Menge der Identifikatoren für Werte) und op (Operationssymbole) gegeben sind. Im Folgenden wird ein Programm angegeben, das einen Operatorbaum als Eingang nimmt und ein Stackprogramm zur Auswertung des durch den Operatorbaum gegebenen Ausdrucks ausdruckt: 1 2 4 6 8 10 12 proc eaf = (expr e): if e in nullary then print ("AC := ", sym(e)) elif e in mono then eaf (e0(e)); print ("AC := ", op(e), "AC") else eaf (e1(e)); print ("push AC"); eaf (e2(e)); print ("AC := top ", op(e), "AC"); print ("pop") fi 4.3.2 Maschinennahe Realisierung von Ablaufstrukturen Eine Zuweisung 1 x := E wird umgesetzt in (sei e der Operatorbaum zu E): 1 2 eaf (e); x := AC Die bedingte Anweisung 1 if E = 0 then S1 else S2 fi wird wie folgt umgesetzt (sei e der Operatorbaum zu E): 4.3. TECHNIKEN MASCHINENNAHER PROGRAMMIERUNG 1 2 4 6 8 t: 10 12 163 eaf (E); if AC = 0 then goto t fi ..... (Maschinencode fuer S2) ..... goto ende ..... (Maschinencode fuer S1) ..... ende: Die Wiederholungsanweisung 1 while E <> 0 do S od wird zu: 1 2 4 6 8 while: eaf (e); if AC = 0 then goto ende fi .... (Maschinencode fuer S) .... goto while ende: Im Folgenden schreiben wir ein Programm, das die Umsetzung (Übersetzung) eines while / if - Programms in ein maschinennahes Programm vornimmt. Wir benötigen eine Sorte zur Repräsentation von Programmen: 1 2 4 sort statement = assign (symbol x, expr e) | if (expr cond, statement st, statement se) | while (expr e, statement s) | seq (statement s1, statement s2) Im Übersetzungsvorgang werden zusätzlich Marken für Sprünge benötigt. Wir nehmen an, dass wir zu einer gegebenen Marke m in der Sorte label Über eine Funktion 1 fct next = (label) label verfügen, so dass m, next(m), next(next(m)), . . . eine Folge unterschiedlicher Marken liefert (Generator für Marken). Programm für das Erzeugen eines maschinennahen Programms: 1 2 4 6 8 10 proc gen = (statement z, var label m): if z in assign then eaf (e(z)); print (x(z), " := AC") elif z in seq then gen (s1(z), m); gen (s2(z), m) elif z in if then label me := m; label mt := next (m); 164 12 14 16 18 20 22 24 26 28 KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG m := next (mt); eaf (cont(z)); print ("if AC = 0 then goto ", mt, " fi"); gen (se(z), m); print ("goto ", me); print (mt, ":"); gen (st(z), m); print (me, ":") else label mw := m; label ma := next (m); m := next (ma); print (mw, ":"); eaf (c(z)); print ("if AC = 0 then goto ", ma, "fi"); gen (s(z), m); print ("goto ", mw); print (ma, ":") fi Teil III Systeme und systemnahe Programmierung 165 167 Hintergrund: • Systemprogramme (Betriebssysteme) • Telekommunikation • Rechnernetze • eingebettete Systeme (∼ Software zur Steuerung technischer Systeme) Weiteres Anliegen: • Beschreibung von allgemeinen diskreten (digitalen) Systemen (Bsp.: Geschäftsprozesse) 168 Kapitel 1 Prozesse, Interaktion, Koordination in verteilten Systemen Wir behandeln verteilte, parallel ablaufende, interaktive (diskrete) Systeme. Definition: System Unter einem System verstehen wir eine von ihrer Umgebung abgegrenzte Anordnung von aufeinander einwirkenden Komponenten. Wichtige Begriffe: Systeme: führen Aktionen aus Parallelität: Aktionen finden möglicherweise nebeneinander (gleichzeitig) statt (Nebenläufigkeit) Prozess / Ablauf: Die Anordnung der Aktionen im Ablauf eines Systems und ihre kausalen Abhängigkeiten Interaktion / Kommunikation: (Koordination, Synchronisation etc.) Zustand: Eine Sicht auf Systeme ist die Zuordnung eines Zustands und von Zustandsübergängen Struktur / Verteilung: Aufgliederung eines Systems in Komponenten (Subsysteme) Zeit: Oftmals als diskrete Zeit verstanden Schnittstellen / Interfaces: Markieren die Interaktion (Wirkung) an Systemgrenzen (am Übergang von zwei oder mehreren Systemen) 1.1 Prozesse Wir betrachten diskrete Prozesse, aufgebaut aus Aktionen, die in kausalen Beziehungen stehen. Etwas enger wird der Begriff Prozess in der Systemprogrammierung verwendet. Dort ist ein Prozess der Vorgang eines Programms in Ausführung. 1.1.1 Aktionsstrukturen als Prozesse Ein Prozess besteht aus einer Reihe von Aktionen. 169 170 KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . . off_hook(3) off_hook(2) free(3) free(2) dial(3, 2) busy(3) on_hook(3) ring(3) off_hook(3) connected(3) busy(3) dial(2, 3) ringtone(2) connected(2) on_hook(2) on_hook(3) Abbildung 1.1: Aktionsstruktur Beispiel: Telefonanlage • Telefone: t ∈ {1, 2, . . . } • Aktionen: off hook(t) on hook(t) dial(t, t’) ring(t) stopRing(t) free(t) busy(t) ringtone(t) connected(t) abheben des Hörers an Telefon t auflegen wählen der Nummer t0 an t läuten aufhören zu läuten Freizeichen Partner besetzt beim Partner klingelts Telefon ist verbunden Aktionsstruktur (Abb. 1.1): Definiert einen Ablauf im Telefonsystem Ein Prozess ist ein gerichteter azyklischer Graph, dessen Knoten mit Aktionen markiert sind (Aktionsgraphen). Typischerweise kommen in Prozessen gewisse Aktionen mehrfach vor. Deshalb verwenden wir den Begriff des Ereignisses. Ein Ereignis ist ein innerhalb eines Prozesses einmaliger Vorgang, der der Ausführung (Instanz) einer Aktion entspricht. Ein Prozess entspricht einer Ablaufstruktur p0 = (E0 , ≤0 , α0 ), wobei: E0 Menge von Ereignissen ≤0 partielle Ordnung auf E0 α0 Abbildung E0 → A, wobei A eine Menge von Aktionen ist Wir nennen zwei Ereignisse e0 , e1 ∈ E0 in p0 parallel bzw. kausal unabhängig, wenn gilt ¬(e0 ≤0 e1 ) ∧ ¬(e1 ≤0 e0 ) Anderenfalls nennen wir die Ereignisse zueinander sequentiell. Sequentieller Prozess: Aktionsstruktur, in der alle paarweisen Ereignisse sequentiell sind. Beobachtung: Programme mit vorgegebener Eingabe definieren Prozesse. Aktionen ∼ atomare Befehle, Anweisungen 1.1. PROZESSE 171 Ereignisse ∼ Instanz: Ausführung eines Befehls Sequentielle Programme entsprechen sequentiellen Prozessen. Forderung: Jeder Prozess ist endlich fundiert, d.h. zu jedem Ereignis ist die Menge der Vorgänger endlich. Deutungsmöglichkeiten der Relation ≤: • echt kausale Deutung (Wirkursache) • zeitliche Reihenfolge • nicht-parallele Ereignisse 1.1.2 Strukturierung von Prozessen Ein Prozess p1 = (E1 , ≤1 , α1 ) heißt Teilprozess von p2 = (E2 , ≤2 , α2 ), wenn: E1 ⊆ E 2 ≤1 =≤2 |E1 ×E1 α1 = α2 |E1 p1 heißt Präfix von p2 , wenn darüber hinaus gilt ∀e ∈ E2 , d ∈ E1 : e ≤2 d ⇒ e ∈ E1 Wir schreiben dann p1 v p2 (ist partielle Ordnung). Lemma: Für jeden endlich fundierten Prozess p1 = (E1 , ≤1 , α1 ) gilt: Die Menge seiner endlichen Präfixe M = {p : p v p1 ∧ p endlich} bestimmt den Prozess p1 eindeutig. Insbesondere gilt p1 = sup M . Beweis: Sei p2 = (E2 , ≤2 , α2 ) ebenfalls obere Schranke von M . Dann gilt E1 ⊆ E2 , denn [ E1 = {E0 : (E0 , ≤0 , α0 ) ∈ M } Da ≤1 und α1 auf E1 mit ≤2 und α2 übereinstimmen, gilt p2 v p1 . Sequentialisierung von Prozessen: p1 heißt Sequentialisierung von p2 , falls gilt: E1 = E2 ∀e, d ∈ E1 : e ≤2 d ⇒ e ≤1 d α1 = α2 Eine Sequentialisierung heißt vollständig, wenn p1 sequentiell ist, d.h. wenn E1 hinsichtlich ≤1 eine Kette bildet. Satz: Jeder endliche fundierte Prozess p0 ist duch die Menge seiner vollständigen Sequentialisierungen eindeutig bestimmt. Ein Prozess p1 heißt Verfeinerung von p0 , wenn es eine injektive Abbildung γ : E1 → E0 gibt mit ∀e, d ∈ E1 : γ(e) 6= γ(d) ⇒ e ≤1 d ⇒ γ(e) ≤0 γ(d) In der Praxis treten Prozesse auf, die sehr umfangreich und unübersichtlich sind. Beispiel: • Workflow in betriebswirtschaftlichen Anwendungen • Rechenvorgänge in Rechenanlagen 172 KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . . • Verkehrsgeschehen • Datenübertragungen (Telekommunikation) Ein Ziel der Prozessmodellierung ist eine übersichtliche und strukturierte Darstellung. Eine Methode: Vereinfachung und Abstraktion. • nur sequentielle Prozesse betrachten • nur endliche Prozesse betrachten • Prozesse als komponiert aus Teilprozessen betrachten 1.1.3 Sequentielle Prozesse und Spuren Sequentieller Prozess: Kausalordnung ist lineare Ordnung. Sequentielle Prozesse lassen sich durch endliche und unendliche Sequenzen von Aktionen darstellen - die explizite Betrachtung von Ereignismengen erübrigt sich. Wir sprechen von Spuren oder Strömen von Aktionen: Sei A Aktionsmenge; die Menge der Ströme über A ist definiert durch Aω = A∗ ∪ A∞ A∞ entspricht der Menge der Sequenzen der Form ha1 a2 a3 . . . i, genauer: der Abbildung a : N \ {0} → A. Durch Aω können wir jeden sequentiellen Prozess als Spur darstellen. Wir können die Konkatenation auf Aω erweitern: ha1 . . . an i ◦ hb1 . . . i = ha1 . . . an b1 . . . i ha1 . . . i ◦ hb1 . . . i = ha1 . . . i Präfixordnung v auf Aω : s1 v s2 ⇔ ∃s3 ∈ Aω : s1 ◦ s3 = s2 Für einen sequentiellen Prozess a1 → a2 → a3 → . . . ist die Spur einfach zu definieren: spur(p) = ha1 a2 a3 . . . i Für einen nicht sequentiellen Prozess definieren wir die Menge seiner Spuren: spuren(p) := {spur(p0 ) : p0 ist vollständige Sequentialisierung von p} Achtung: • Durch den Übergang von Prozessen zu Spuren verlieren wir Informationen über die Nebenläufigkeit. • Systeme können wir durch die Menge ihrer Prozesse P definieren. Die Menge der Spuren von P ist definiert durch [ Spuren(P ) := spuren(p) p∈P Eine andere Vereinfachung erhalten wir, wenn wir nur endliche Prozesse betrachten. Sei P eine Menge von Prozessen (Beschreibung eines Systems). Die Menge der endlichen Anfangsprozesse ist definiert durch [ Mea(P ) = {p0 : p0 v p ∧ p0 endlich} p∈P Beide Vereinfachungen lassen sich kombinieren: Spuren(Mea(P )) Menge der Spuren der endlichen Anfangsprozesse 1.1. PROZESSE 173 send recieve send recieve send recieve Abbildung 1.2: Prozess Beispiel: (s. Abb. 1.2) Spur: < send send send recieve recieve . . . > Achtung: Durch die Menge Spuren(Mea(P )) können wir insbesondere nicht charakterisieren, welche Aktionen unendlich oft auftreten müssen (zumindest in bestimmten Fällen). Dies führt auf den Begriff der Fairness: Eine Spur s ∈ Aω heißt fair für eine Menge von Prozessen P , wenn es einen Prozess p ∈ P gibt mit spur(p0 ) = s ∧ p0 ist Sequentialisierung von p Die Spur s ∈ Aω heißt unfair, wenn es einen unendlichen Prozess p ∈ P und ein unendliches Anfangsstück p0 v p gibt, so dass s ∈ spuren(p0 ) und s nicht fair für P ist. 1.1.4 Zerlegen von Prozessen in Teilprozesse Wir wollen Prozesse strukturieren, indem wir sie in zusammengehörige Teilprozesse zerlegen oder umgekehrt aus Teilprozessen zusammensetzen. Wir betrachten Porzesse pi = (Ei , ≤i , αi ) i = 0, 1, 2 Wir sagen, p0 ist sequentiell zusammengesetzt aus p1 gefolgt von p2 , falls gilt: ∧ ∧ ∧ ∧ ∧ E0 = E1 ∪ E2 ∀e1 ∈ E1 ∀e2 ∈ E2 : e1 ≤0 e2 α0 |E1 = α1 α0 |E2 = α2 ≤0 |E1 ×E1 =≤1 ≤0 |E2 ×E2 =≤2 Wir schreiben dann isseq(p0 , p1 , p2 ). Wir können auch Prozesse parallel zerlegen bzw. parallel zusammensetzen. Dabei verwenden wir eine gewisse Menge gemeinsamer Aktionen S ⊆ A. Dies sind Aktionen, die in beiden Teilprozessen auftreten (die quasi gemeinsam ausgeführt werden). Wir sagen, Prozess p0 ist aus p1 und p2 über die gemeinsamen Aktionen 174 KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . . S ⊆ A parallel zusammengesetzt, wenn gilt: ∧ ∧ ∧ ∧ E0 = E1 ∪ E2 E1 ∩ E2 = {e ∈ E0 : α0 (e) ∈ S} α0 |E1 = α1 α0 |E2 = α2 ≤0 = (≤1 ∪ ≤2 )∗ Transitive Hülle (d.h. ≤0 |E1 ×E1 =≤1 ∧ ≤0 |E2 ×E2 =≤2 ). Wir schreiben dann ispar(p0 , p1 , p2 , S). 1.1.5 Aktionen als Zustandsübergänge Neben der Beschreibung von Systemen durch die Prozesse, die deren Abläufe bilden, können wir Systeme auch durch Zustände und ihr Verhalten durch Zustandsänderungen modellieren. Wir geben für ein System einen Zustandsraum an und deuten Aktionen in Prozessen als Zustandsänderungen. Wir definieren hierfür nichtdeterministische Zustandsautomaten mit Transitionsaktionen: • S Menge von Zuständen (Zustandsraum) • A Menge von Transitionsaktionen • R ⊆ S × A × S eine Zustandsübergangsrelation • S0 Menge von möglichen Anfangszuständen Seien σ0 , σ1 ∈ S und a ∈ A gegeben. (σ0 , a, σ1 ) drückt aus, dass im Zustand σ0 Aktion a ausgeführt werden kann und dies zum Nachfolgezustand σ1 führen kann. Das heißt, in einem Zustand können mehrere Transitionsaktionen zu unterschiedlichen Nachfolgezuständen führen. Der Automat ist nichtdeterministisch. a Wir schreiben σ0 → σ1 für (σ0 , a, σ1 ) ∈ R. Jedem Zustandsautomaten lassen sich Spuren zuordnen. Eine Folge ai , 1 ≤ i ≤ k mit k ∈ N ∪ {∞} ist eine endliche oder unendliche Aktionsspur des Zustandsautoa maten, falls eine Folge von Zuständen σi existiert mit σ0 ∈ S0 und σi−1 →i σi für alle 1 ≤ i ≤ k. • Eine Aktion a ∈ A heißt deterministisch, wenn für jeden Zustand σ ∈ S a höchstens ein Nachfolgezustand σ1 ∈ S existiert mit σ → σ1 . • Eine Aktion a ∈ A heißt total, wenn für jeden Zustand σ ∈ S ein Nachfola gezustand σ1 ∈ S existiert mit σ → σ1 . Totalen, deterministischen Aktionen können wir (totale) Zustandsänderungsabbildungen zuordnen. Sei S eine Menge von Zuständen, A eine Menge von Aktionen. Wir ordnen jeder Aktion a ∈ A eine Zustandsänderung zu: % : A → (S → S) Jede Aktion a ∈ A definiert durch %(a) eine Abbildung auf Zuständen. Endlichen sequentiellen Prozessen lassen sich Zustandsänderungsabbildungen zuordnen. 1.1. PROZESSE 175 Beispiel: 1 2 4 6 x := 10; y := 0; while x > 0 do y := y + x; x := x - 1 od Ereignisse Aktionen a0 b0 a1 b1 c1 .. . x := 10; y := 0; (x > 0) ? y := y+x; x := x-1; .. . Zustand x y 10 ? 10 0 10 0 10 10 9 10 .. .. . . 0 55 %(y := 10)(n, m) = (n, 0) %(x := 10)(n, m) = (10, m) %(y := y + x)(n, m) = (n, n + m) %(x := x − 1)(n, m) = (n − 1, m) Frage: Welche Zustandsübergänge können ohne Schwierigkeit zeitlich parallel ausgeführt werden? Wir sagen, Aktionen a, b ∈ A seien im Konflikt, wenn sie nicht problemlos nebeneinander ausgeführt werden können. Beispiel: Konflikt bei parallelen Zuweisungen (1) x := x+1 (2) x := x*2 Aktionen (1) und (2) sind im Konflikt. Bei paralleler Ausführung kann dem Prozess keine Zustandsänderung eindeutig zugewiesen werden. Modellierung: Die Relation Conflict ⊆ A × A gibt für jedes Paar von Aktionen a1 , a2 ∈ A an, ob Konflikt bestehen. Im Falle von Zuweisungen ist Conflict wie folgt definiert (Bernstein – Bedingung): Die Zuweisungen (1) x1, ..., xn := E1, ..., En (2) y1, ..., ym := F1, ..., Fm sind konfliktfrei, falls • jede Programmvariable xi nicht in der Anweisung (2) vorkommt und • jede Programmvariable yi nicht in der Anweisung (1) vorkommt. Gilt die Bernsteinbedingung, dann ist die Reihenfolge der Ausführung der beiden Zuweisungen ohne Einfluss auf den Endzustand. Für konfliktfreie Aktionen a1 , a2 ∈ A setzen wir voraus: 176 KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . . • dass die zugehörigen Zustandsübergänge unabhängig sind, d.h. die Reihenfolge der Ausführung spielt keine Rolle: %(a1 ) ◦ %(a2 ) = %(a2 ) ◦ %(a1 ) • a1 , a2 können problemlos zeitlich nebenläufig (parallel) ausgeführt werden. Wir schreiben a1 k a2 für die Aktion der Parallelausführung der Aktionen a1 und a2 . Unter der Voraussetzung der Konfliktfreiheit: %(a1 k a2 ) =def %(a1 ) ◦ %(a2 ) Parallele Komposition konfliktfreier Aktionen a1 , a2 ∈ A führt keine neuen Konflikte ein, d.h. ¬((a1 , a3 ) ∈ Conflict) ∧ ¬((a2 , a3 ) ∈ Conflict) ⇒ ¬((a1 k a2 , a3 ) ∈ Conflict) Damit gilt bei paralleler Komposition konfliktfreier Aktionen für die induzierte Zustandsänderung: • a1 k a2 = a2 k a1 • (a1 k a2 ) k a3 = a1 k (a2 k a3 ) Ist die Konfliktfreiheit verletzt, so kann über die Wirkung der Parallelausführung keine Aussage gemacht werden. p = (E0 , ≤0 , α) heißt konfliktfrei, falls alle konfliktträchtigen Aktionen nur bei nicht parallelen Paaren von Ereignissen auftreten, d.h. ∀e, d ∈ E0 : (α(e), α(d)) ∈ Conflict ⇒ e ≤0 d ∨ d ≤0 e Gegeben sei eine Zustandsmenge S, Aktionen A und eine Zustandsübergangsrelation R. R lässt sich auf endliche Prozesse und Spuren erweitern. Spuren: Seien σ0 , σ1 , σ2 ∈ S, a ∈ A und s1 , s2 ∈ S ∗ Sequenzen von Aktionen. Induktive Definition der Zustandsübergänge für Spuren über die Länge der Sequenzen: ε σ 0 → σ0 a hai σ 0 → σ1 ⇒ σ 0 → σ 1 s s s ◦s 1 2 σ0 → σ1 ∧ σ 1 → σ2 ⇒ σ0 1→ 2 σ2 Wir erweitern die Zustandsübergangssicht auf endliche Prozesse. Wieder definieren wir eine Zustandsübergangsrelation für Prozesse. Sei P ein Prozess; wir schreiben p σ0 → σ1 , um auszudrücken, dass bei Ausführung der Aktionen in P ausgehend vom Zustand σ0 der Zustand σ1 erreicht werden kann. Sei s eine Sequenz von Aktionen, die einer Sequentialisierung des endlichen Prozesses p s P entspricht und es gelte σ0 → σ1 . Dann und nur dann gelte auch σ0 → σ1 . Die Reihenfolge der Aktionen in der Sequentialisierung kann bewirken, dass unterschiedliche Zustände erreicht werden. Wir sprechen von Nichtdeterminismus, der sich aus der Zufälligkeit der Hintereinanderausführung paralleler Ereignisse bzw. der zugeordneten Aktionen ergibt. 1.2 Systembeschreibungen durch Mengen von Prozessen Ein Prozess beschreibt einen einzelnen Ablauf eines Systems. In der Regel besitzt ein System viele Abläufe (unter Umständen unendlich viele). Damit wird ein System durch eine Menge von Abläufen beschrieben. 1.2. SYSTEMBESCHREIBUNGEN DURCH MENGEN VON PROZESSEN 177 PN 1 PN 2 1 2 b a e d c f Abbildung 1.3: Petri-Netze Beispiel: Programme besitzen i.d.R. für unterschiedliche Eingaben unterschiedliche Abläufe. Wir betrachten exemplarisch drei Möglichkeiten zur Beschreibung von Systemen: • Petri-Netze • formale Beschreibungssprachen • Prädikatenlogik Alle drei Ansätze beschreiben Mengen von Abläufen. 1.2.1 Petri-Netze (eingeführt 1962 von C. A. Petri in seiner Dissertation) Beispiel: (s. Abb. (1.3)) Abläufe in PN1 mit Anfangsmarkierung 1 auf 1, 0 auf 2: b → a → c → a → c → a → b → ... c → a → c → a → b → a → b → ... Abläufe in PN2 : e f → d → e % & f → d → e → ... % & % f Petri-Netz: gegeben durch: T0 endliche Menge von Transitionen P0 endliche Menge von Plätzen R ⊆ (T0 × P0 ) ∪ (P0 × T0 ) Flussrelation Das Tripel (T0 , P0 , R) definiert ein Petri-Netz. Die Belegung eines Petri-Netzes entspricht einer Abbildung: b : P0 → N0 natürlichzahlige Belegung Stellen- / Transitionsnetze b : P0 → B boolesche Belegung Bedingungs- / Ereignisnetze Eine Belegung definiert einen Zustand des Petri-Netzes. Das Schalten einer Transition definiert einen Zustandsübergang 1 b 0 → 1 0 Eine Menge K ⊆ T0 von Transitionen heißt für eine natürlichzahlige Belegung schaltbereit, wenn gilt: |{k ∈ K : (p, k) ∈ R}| ≤ b(p)∀p ∈ P0 178 KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . . v h p l Abbildung 1.4: Erzeuger – Verbraucher – Modell Falls K schaltbereit für die Belegung b ist, so führt das Schalten von K auf die Belegung b0 mit b0 (p) = b(p) − |{k ∈ K : (p, k) ∈ R}| + |{k ∈ K : (k, p) ∈ R}| Bei booleschen Petri-Netzen definiert sich die Schaltbereitschaft ganz analog, nur, dass jetzt alle Zielplätze für jede Transition K mit false belegt sein müsen. Ansonsten entspricht true der 1 und false der 0. Lemma: Sequentialisierung Ist K schaltbereit für eine Belegung b und gilt K = K1 ∪ K2 K1 ∩ K 2 = ∅ K so sind auch K1 und K2 schaltbereit für b. Wir schreiben b → b0 , falls durch Schalten von K die Belegung b in b0 übergeführt wird (Zustandsübergang). Feststellung: Die Übergänge sind deterministisch. K K K b0 →1 b1 ∧ b1 →2 b2 ⇒ b0 → b2 (mit den Bezeichnungen wie oben) Beispiel: Petri-Netz des Erzeuger – Verbraucher – Systems (1.4)) Ablauf (ohne die strichlierte Erweiterung): (vgl. Abb. p → l → p → l → p → l → ... & & & h → v → h → v → h → ... Die Erweiterung des Netzes stellt sicher, dass hole (h) und liefere (l) nie parallel ausgeführt werden. Jedes Petri-Netz mit einer gegebenen Anfangsbelegung definiert eine Menge von Prozessen. Die Aktionen dieser Prozesse entsprechen den Transitionen. Seien b und b0 Belegungen eines Petri-Netzes und sei p ein Prozess. Wir schreiben p b → b0 wenn das Netz mit Belegung b den endlichen Prozess p ausführen kann und dies in die Belegung b0 führt. Diese Relation definieren wir induktiv (sozusagen Induktion über die Anzahl der Ereignisse im Prozess p): (1) Der leere Prozess p (d.h. der Prozess mit leerer Ereignismenge) hat folgenden p Übergang: b → b. 1.2. SYSTEMBESCHREIBUNGEN DURCH MENGEN VON PROZESSEN 179 b a y x c Abbildung 1.5: Petri-Netz (2) Sei p ein Prozess mit trivialer Kausalitätsordnung (jedes Ereignis ist nur für sich selbst kausal) und alle Ereignisse sind unterschiedlich markiert. Dann p gilt b → b0 genau dann, wenn die Menge {α(e) : e ∈ Ereignismenge zu p} schaltbereit ist und von b zu b0 führt. Für jeden Prozess p = (E0 , ≤0 , α0 ), auf den nicht die Bedingungen (1) und (2) p zutreffen, gilt b0 → b1 genau dann, wenn für jede nichttriviale Zerlegung von p in ein Präfix p1 v p und einen Restprozess p2 = p|E0 \E1 eine Belegung b0 existiert, so p1 p2 dass gilt b0 → b0 ∧ b0 → b1 . p Beobachtungen: Gilt b0 → b1 , dann gilt für jede Sequentialisierung p0 von p p0 ebenfalls b0 → b1 . Achtung: Die Umkehrung gilt in der Regel nicht! Auch für Petri-Netze ist der Begriff der Fairness von Interesse: Für ein PetriNetz mit gegebener Anfangsbelegung heißt ein unendlicher Ablauf unfair, wenn eine Transition nur endlich oft schaltet, obwohl sie unendlich oft schaltbereit ist. Die Menge der Belegungen eines Petri-Netzes N bezeichnen wir mit BE(N ). Für eine gegebene Anfangsbelegung (Anfangszustand) sind bestimmte Belegungen durch Schalten von Transitionen erreichbar, andere nicht. Die Menge der im Netz N vom Anfangszustand b0 aus erreichbaren Belegungen definieren wir wie folgt: n o p R(N, b0 ) = b ∈ BE(N ) : ∃p : p endlich ∧ b0 → b Beispiel: Fairness (s. Abb. (1.5)) Mögliche Schaltvorgänge bei Anfangszustand x 0 = y 1 : • a c a b a b a c . . . → fair • a b a b a b a b . . . → unfair • a c a b a b a b . . . → unfair Menge der erreichbaren Zustände: 01 , 10 Es gilt: x + y = 1 (Systeminvariante). Mengen von Zuständen lassen sich durch Prädikate q : BE(N ) → B charakterisieren. Eine Systeminvariante für das Netz N mit Anfangsbelegung b0 ist ein Prädikat q, für das gilt: ∀b ∈ BE(N ) : b ∈ R(N, b0 ) ⇒ q(b) Idee: Systeminvarianten charakterisieren, welche kritischen Zustände nicht eingenommen werden. Die Gültigkeit einer Invariante für ein Petri-Netz beweisen wir wie folgt: 180 KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . . Menge aller Zustaende q Menge der erreichbaren Zustaende Abbildung 1.6: Zustände und Invarianten (1) q(b0 ) t (2) ∀b, b0 ∈ BE(N )∀t ∈ T0 : q(b) ∧ b → b0 ⇒ q(b0 ) Achtung: Diese Beweismethode funktioniert für bestimmte Invarianten nicht, wenn für nicht erreichbare Zustände b ∈ BE(N )\R(N, b0 ) die Aussage (2) nicht beweisbar ist (vgl. Abb. (1.6)). Ausweg: Wir suchen eine Invariante q 0 mit der Eigenschaft ∀b ∈ BE(N ) : q 0 (b) ⇒ q(b) (wobei für q 0 die Beweismethode funktioniert). Im Zweifel können wir immer q 0 (b) = b ∈ R(N, b0 ) wählen. Beispiel: Erzeuger – Verbraucher – Problem Können l und h gleichzeitig schalten? Ist t ≤ 1 Invariante? Hilfsinvariante: t + 1 ≤ 1 Im Anfangszustand: t + 1 = 1 Beobachtung: Jede Transition lässt t + 1 unverändert ⇒ Hilfsinvariante ist ok ⇒ t ≤ 1. Petri-Netze sind in der Regel nicht deterministisch! Für eine gegebene Belegung b gibt es in der Regel viele unterschiedliche Belegungen, die über die einzelnen Transaktionen erreicht werden können. Unterschiedliche Transitionen (insbesondere bei Konflikten) können schalten. Petri-Netze sind der vielleicht einfachste Formalismus, in dem wir die typischen Probleme verteilter Systeme studieren können: • Struktur der Systeme • Prozesse, die auf dem System ablaufen (Prozesssicht) • Zustandsraum, Zustandsübergänge, Menge der erreichbaren Zustände • Fairness Typische Fragestellungen für ein System (u.U. modelliert durch ein Petri-Netz): (1) Welche Eigenschaften haben erreichbare Zustände? (Invarianten) Ist die Menge der erreichbaren Zustände endlich? (2) Ist Nebenläufigkeit (gleichzeitiges Schalten) für bestimmte Transitionen ausgeschlossen? (3) Können Belegungen erreicht werden, von denen aus gewisse oder alle Transitionen nie wieder schalten werden (Verklemmung, Deadlock) (4) Sind alle Abläufe fair? 1.2. SYSTEMBESCHREIBUNGEN DURCH MENGEN VON PROZESSEN 181 Beobachtung: Jedes Petri-Netz mit Anfangszustand definiert einen Zustandsübergangsautomaten für die Menge der erreichbaren Belegungen / Zustände. Die Übergänge entsprechen dem Schalten von Transitionen. Jedem Petri-Netz mit Anfangsbelegung kann solch ein Automat zugeordnet werden. Allerdings drücken diese Automaten die Nebenläufigkeit in Prozessen nicht mehr aus. Wir beschränken uns auf sequentielle Prozesse. 1.2.2 Prozessalgebra Wir können Prozesse und damit verteilte, parallel ablaufende Systeme auch durch Sprachen (Terme) beschreiben. Ein Spezialfall sind Programmiersprachen. Wir betrachten eine vereinfachte Programmiersprache zur Beschreibung von Prozessen. Wir sprechen von Prozessalgebra (Nache Ideen von C.A.R. Hoare: CSP, Communicating Sequential Processes, R. Milner: CCS, Calculus of Communicating Systems). Wir definieren die Sprache der CSP-Agenten: hagenti ::=skip | hactioni | hagenti ; hagenti | hagenti or hagenti | hagenti k hagenti | hagent idi | hagent idi :: hagenti Beispiel: Ampel mit Aktionen rot, gelb, grün: Agentenidentifikatoren: ampel:: (rot; gelb; grün; ampel) Dies entspricht dem Agenten mit Identifikator ampel, für den die Gleichung ampel = rot; gelb; grün; ampel (Rekursion) gilt. ampel beschreibt den folgenden Prozess: rot → gelb → grün → rot → . . . Beispiel: Fahrkartenautomat Aktionen: • Zielwahl • Preisanzeige • Geldeinwurf • Kartenausgabe • Rückgeldausgabe • Abbruch Agent, der den Automaten beschreibt: 1 2 4 automat :: Zielwahl; Preisanzeige; ((Geldeinwurf; ((Kartenausgabe || Rueckgeldausgabe) or Abbruch)) or Abbruch); 182 KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . . Prozesse für den Automaten: Zielwahl → Preisanzeige → Abbruch → . . . Zielwahl → Preisanzeige → Geldeinwurf → Abbruch → . . . ... Die Sprache der Agenten ist eine Prozessbeschreibungssprache. Da wir aus gegebenen Termen der Spache Agent durch or, ; und || neue Agenten bilden können, sprechen wir von einer Prozessalgebra. Agenten beschreiben Mengen von Prozessen. Um genau festzulegen, welche Prozesse durch einen gegebenen Agenten beschrieben werden, führen wir (ähnlich wie für Belegungen bei Petri-Netzen) eine Relation p t → t0 ein, die ausdrückt, dass Agent t den Prozess p ausführen kann und sich dadurch verhält wie durch den Agenten t0 beschrieben. Dies wird durch folgende Regeln festgelegt: (1) Jeder Agent kann den leeren Prozess p∅ ausführen: p∅ t→t (2) Sei a eine Aktion. pa a → skip wobei pa der Prozess mit nur einem Ereignis e sei mit α(e) = a. (3) Auswahl: p p p p t1 → t0 ⇒ t1 or t2 → t0 t2 → t0 ⇒ t1 or t2 → t0 (4) Sequentielle Komposition: p p t1 → t 0 ⇒ t 1 ; t 2 → t 0 ; t2 p1 p2 p t1 → skip ∧ t2 → t ∧ isset(p, p1 , p2 ) ⇒ t1 ; t2 → t (5) Parallele Komposition: p1 p2 p t1 → t01 ∧ t2 → t02 ∧ ispar(p, p1 , p2 , ∅) ⇒ t1 k t2 → t01 k t02 p1 p2 p t1 → skip ∧ t2 → skip ∧ ispar(p, p1 , p2 , ∅) ⇒ t1 k t2 → skip k skip Algebraisch: 1 2 4 skip || skip = skip skip; t = t; skip = t => skip; skip = skip t1 || t2 = t2 || t1 (t1 || t2) || t3 = t1 || (t2 || t3) skip || t = t o n p P (t) = p : ∃t0 : t → t0 Menge der Prozesse, die der Agent t ausführen kann. [ S(t) = spuren(p) p∈P (t) Zwei Aktionen t, t0 sind spuräquivalent, falls S(t) = S(t0 ) oder prozessäquivalent, falls P (t) = P (t0 ). 1.2. SYSTEMBESCHREIBUNGEN DURCH MENGEN VON PROZESSEN 183 a1 b1 b2 c a2 b3 Abbildung 1.7: Ablauf des Beispielagenten 6 a || b = (a; b) or (b; a) Achtung: Bestimmte Agenten führen nie nach skip! (6) Rekursion p p t[x :: t/x] → t0 ⇒ x :: t → t0 Alle Fragen, die wir für Petri-Netze gestellt haben, können wir analog für Agenten p stellen. Wir sagen, von t ist t’ erreichbar, wenn t → t’ für einen Prozess p gilt. Beispiele: • Ampel ampel :: rot; gelb; grün; ampel prot rot −→ skip pgelb gelb −→ skip pgrün grün −→ skip prot prot rot −→ skip ⇒ rot; gelb −→ skip; gelb pgelb prot p rot −→ skip ∧ gelb −→ skip ∧ isseq(p, prot , pgelb ) ⇒ rot; gelb −→ skip Menge der erreichbaren Agenten (Zustände): prot ampel −→ gelb; grün; ampel prot ;pgelb ampel −→ grün; ampel prot ;pgelb ;pgrün ampel −→ ampel • q :: a; q p0 p0 a; (q :: q; q) −→ q :: a; q ⇒ q :: a; q −→ q :: a; q 1.2.3 Synchronisation und Koordination von Agenten Für Agenten t1 und t2 gilt, dass diese in t1 || t2 ihre Aktionen / Prozesse unabhängig voneinander ausführen. Damit wir in der Lage sind, kausale Beziehungen zwischen den Prozessen von t1 und t2 herzustellen, verwenden wir eine Menge S ⊆ A von gemeinsamen Aktionen (englisch: handshake). Wir definieren nun eine parallele Komposition mit gemeinsamen Ereignissen, gekennzeichnet durch die Aktionen in der Synchronisationsmenge S. Wir schreiben t1 kS t2 für den Agengen, der Prozesse ausführt, die sich parallel aus den Prozessen von t1 und t2 zusammensetzen, wobei jedoch alle Ereignisse in diesen Prozessen für t1 und t2, die mit Aktionen aus S markiert sind, gemeinsame Ereignisse sind. Damit unterscheiden wir in den Prozessen p1 für Agent t1 und p2 für t2 zwischen den Ereignissen, die beide unabhängig parallel ausführen (das sind die Ereignisse mit Aktionen aus A \ S) und den Ereignissen, die die Agenten gemeinsam ausführen (die Ereignisse mit Aktionen aus S). 184 KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . . a1 b1 p a2 v p a1 v b2 a2 b1 p v p v p b2 a2 Abbildung 1.8: Ablauf mit gegenseitigem Ausschluss von Aktionen Beispiel: (a1; c; a2) k{c} ((b1 || b2); c; b3) Ablauf: s. Abb. (1.7) Definition der parallelen Komposition: p1 p2 p t1 → t01 ∧ t2 → t02 ∧ ispar(p, p1 , p2 , S) ⇒ t1 kS t2 → t01 kS t02 p1 p2 p t1 → skip ∧ t2 → skip ∧ ispar(p, p1 , p2 , S) ⇒ t1 kS t2 → skip Beispiel: Gegenseitiger Ausschluss für Aktionen b1, b2: [(t1 :: a1; p; b1; v; t1) || (t2 :: a2; p; b2; v; t2)] k{p,v} sema :: p; v; sema Ablauf: s. Abb. (1.8) Probleme: • Es kann passieren, dass ein Agent im obigen Beispiel nie ein p ausführt, da ihm der Partnerprozess ständig zuvorkommt. • Verklemmung: (a; c) k{c,d} (b; d) Die Agenten stimmen in den synchronisierten Aktionen nicht überein, es ist kein Fortschritt nach Ausfühung der Aktionen a und b mehr möglich. Wir sprechen von einer Verklemmung. 1.2.4 Beschreibung von Prozessen mit Prädikaten Wir beschreiben ein System durch eine Menge von Prozessen. Eine gängige Beschreibung von Mengen erhalten wir durch Prädikate. q : Prozesse → B (wobei Prozesse die Menge der Prozesse über einer gegebenen Menge A von Aktionen sei). Wesentliche Frage: Wie Prädikate auf Prozessen definieren? (1) Wir nutzen die Präfixordnung. (2) Wir definieren für einen Prozess p ∈ Prozesse und für eine Aktion a ∈ A die Zahl #(a, p) (Anzahl der Aktionen a in p): #(a, p) = |{e ∈ E0 : α0 (e) = a}| wobei p = (E0 , ≤0 , α0 ). Es ist #(a, p) ∈ N0 ∪ {∞}. Erweiterung auf Teilmengen K ⊆ A: X #(K, p) = #(a, p) a∈K Oft ist es von Interesse, nicht über den unendlichen Prozess p, sondern über Eigenschaften seiner endlichen Präfixe p0 v p zu reden: 0 ≤ #(a1 , p0 ) − #(a2 , p0 ) ≤ 1∀p0 v p, p0 endlich ⇒ a1 und a2 bilden eine kausale Kette 1.2. SYSTEMBESCHREIBUNGEN DURCH MENGEN VON PROZESSEN 185 Weiteres Prädikat: Gegenseitiger Ausschluss von Aktionen b1 und b2 : ggA(b1 , b2 , p) = ∀e, e0 ∈ E0 : α0 (e) = b1 ∧ α0 (e0 ) = b2 ⇒ (e ≤0 e0 ) ∨ (e0 ≤ e) Wichtiges Mittel, um Prädikate über sequentiellen Prozessen zu definieren: Temporale Logik. Betrachten wir Prädikate über Prozessen (und damit über Systemen) mit unendlichen Abläufen, so können wir zwei Klassen von Aussagen unterscheiden: ➥ Sicherheitseigenschaften: Dies sind Eigenschaften, bei denen ein Verstoß stets durch eine endliche Beobachtung nach endlich vielen Schritten festgestellt werden kann. ➥ Lebendigkeitseigenschaften: Dies sind Eigenschaften, bei denen wir einen Verstoß nicht durch endliche Beobachtung feststellen können, sondern nur durch Betrachtung des unendlichen Gesamtprozesses. Beobachtung: Jede Systemeigenschaft (in der Regel eine Mischung aus Sicherheitsund Lebendigkeitseigenschaft) lässt sich zerlegen in eine reine Sicherheits- und eine reine Lebendigkeitseigenschaft. Beispiel: Sicherheits- und Lebendigkeitseigenschaften für einen Lift: • Sicherheitseigenschaften: – Solange der Lift fährt, sind alle Türen geschlossen. – Stockwerktüren sind geschlossen, wenn der Lift nicht in diesem Stockwerk hält. – Lift hält nur auf Anforderung. – ... • Lebendigkeitseigenschaften: – Bei Anforderung hält der Lift irgendwann im entsprechenden Stockwerk, falls er nicht blockiert wird. – Bei Fahranforderung schließt die Tür irgendwann, falls sie nicht blockiert wird. – ... Wie formal ausdrücken? • Sicherheitseigenschaften: – Invarianten des Systems auf Zuständen – bei Prozessen: Prädikate, die für alle endlichen Präfixe gelten. q gilt als Sicherheitseigenschaft für den Prozess p, wenn ∀p0 v p, p0 endlich q(p0 ) gilt. • Lebendigkeitseigenschaften: Wir definieren für Zusicherungen auf Zuständen (oder Prozessen) folgende Aussage: Seien Q, P Zusicherungen. Q leads to P heißt für unser System: Ist das System in einem Zustand, in dem Q gilt, dann ist es irgendwann (nach endlich vielen Schritten) in einem Zustand, in dem P gilt. true leads to P steht für: Das System erreicht immer irgendwann einen Zustand, in dem P gilt. 186 KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . . 1.3 Programmiersprachen zur Beschreibung kooperierender / kommunizierender Systeme / Prozesse Programme, insbesondere anweisungsorientierte, beschreiben Prozesse. Beispiel: 1 2 4 var nat x := 10; while true do x := x+1 od Invariante: x ≥ 10. Lebendigkeitseigenschaften: • true leads to x = 100 • ∀n ∈ N : x = n leads to x > n Allerdings sind diese Prozesse sequentiell. Wir führen nun Sprachmittel ein, um parallele Prozesse durch Programme beschreiben zu können. 1.3.1 Parallelität in anweisungsorientierten Programmiersprachen Voneinander logisch unabhängige Anweisungen können wir ohne Probleme parallel ausführen. Wir schreiben k S1 k · · · k Sn k für eine parallele Anweisung. Dies steht für: parallel aus. führe die Anweisungen S1 , . . . , Sn Beispiel: k x := x + 1; z := x2 ky := y − 1 k Solche Anweisungen können unabhängig voneinander ausgeführt werden, da sie keine gemeinsamen Variablen verwenden (Bernstein-Bedingung). {x = 0 ∧ z = 0} k x := x + 1kz := x2 k {x = 1 ∧ (z = 0 ∨ z = 1)} Feststellung: Falls in S1 , . . . , Sn gemeinsame Programmvariablen auftreten (dies sind Programmvariablen, die in mindestens einer Anweisung geschrieben und in mindestens einer weiteren Anweisung gelesen oder geschrieben werden), so lässt sich über den Endzustand der Ausführung nichts aussagen, wenn man nichts über die Granularität (Atomizität) der Anweisungen weiß. Fazit: Zusätzliche Hilfsmittel (Konstrukte der Programmiersprache) sind erforderlich, um Parallelität zu kontrollieren, zu steuern und zu koordinieren. Dabei sind wir an folgenden Ausdrucksmöglichkeiten interessiert: (1) Sicherstellung der Ausführung bestimmter Anweisungen ohne unerwartete Einflüsse durch parallel ablaufende Programme (ohne Unterbrechung) — Konzept der unteilbaren Anweisung / Aktion (atomare Anweisung): gegenseitiger Ausschluss. 1.3. PROGRAMMIERSPRACHEN ZUR BESCHREIBUNG. . . 187 Nutzereingabe Sensoren Controller reaktives Programm gesteuertes System ("Regelstrecke") Aktuatoren Ausgabe fuer den Nutzer Abbildung 1.9: reaktives System (2) Warten auf das Eintreten gewisser Bedingungen (kann nicht einfach durch if erreicht werden, da if nicht zu Warten führt, sondern zu sofortiger Fortsetzung der Berechnung abhängig vom Wert der Bedingung). (3) Nachrichtenaustausch: Zwischen den parallel ablaufenden Anwendungen wollen wir Informationen austauschen. Achtung: Auch beim Arbeiten mit atomaren Anweisungen kann durch Parallelität Nichtdeterminismus auftreten, durch die Zufälligkeit der relativen Geschwindigkeit der Ausführung. Durch die Konzepte paralleler Prozesse können wir auch Anweisungen schreiben, die Vorgänge in der Umgebung eines Rechners steuern und überwachen. Wir sprechen von reaktiven Systemen / Programmen (z.B. Steuerung technischer Vorgänge durch Rechner (Controller) und ihre Software: mobile Telefone, Haushaltsgeräte, Automobiltechnik, Avionik, Robotik, . . . ). Eine Zahl: 98% aller programmierbaren Prozessoren laufen eingebettet / reaktiv. Definition: reaktiv Ein Program heißt reaktiv, wenn es auf Ereignisse seiner Umgebung reagiert und somit technische oder organisatorische Vorgänge steuert. Dies erfordert: • Sensoren: Vorrichtungen, die Ereignisse erkennen und in digitale Signale umsetzen • Aktuatoren: Geräte, die digitale Signale in Steueranweisungen umsetzen Für solche Systeme sind Echtzeitanforderungen typisch. Das heißt, dass die Systeme zeitbezogen reagieren. Wir unterscheiden zwei Kategorien: • Weiche Echtzeitanforderungen: keine scharfen Zeitschranken (z.B. Telefon) • Harte Echtzeitanforderungen: Wenn diese nicht eingehalten werden, treten katastrophale Fehler auf. (z.B. Motorsteuerung, Airbag, ABS, Robotik) 1 2 4 ||- x := 0; x := 1; || while x <> 0 do y := y+1 od -|| ➥ Terminiert dieses Programm? ➥ Wenn ja, welchen Wert haben x und y? 188 KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . . Die parallele Anweisung terminiert, falls alle parallel laufenden Programme terminieren. Zwei Fälle für unser Beispiel: (1) Die while – Bedingung wird nicht ausgewertet, solange x = 0 gilt. Dann terminiert die Wiederholungsanweisung und damit das ganze parallele Programm nicht. (2) Die while – Bedingung wird ausgewertet, wenn x = 1 gilt. Das Programm terminiert. Dann hat x den Wert 1 und y hat einen beliebigen Wert. Frage: Wie Information zwischen parallel ablaufenden Programmen austauschen? Wir unterscheiden grundsätzlich zwei Möglichkeiten: • Nachrichtenaustausch / Austausch von Signalen • gemeinsame Variablen 1.3.2 Kommunikation durch Nachrichtenaustausch Eine grundlegende Form der Kommunikation besteht im Austausch von Nachrichten. Dabei unterscheiden wir: • den Sender (Nachrichtenquelle) • den / die Empfänger der Nachricht. In der Regel haben wir • einen Sender • einen ( point – to – point), viele oder alle ( Wir konzentrieren uns auf den Fall Broadcasting) Empfänger. ein Sender, ein Empfänger. Wichtige Frage: Wie Nachrichten adressieren? ➥ direkt Empfänger adressieren ➥ über Nachrichtenmedien (Briefkästen, Kanäle etc.) Wir verwenden Nachrichtenkanäle. Deklaration / Vereinbarung eines Kanals: channel m c mit: m Kanalsorte ∼ Sorte der Nachrichten, die über den Kanal gesendet werden c Kanalidentifikator Operationen / Anweisungen für Kanäle: send E on c Senden einer Nachricht receive v on c Empfangen einer Nachricht und Ablegen in v mit: c Kanalidentifikator E Ausdruck von der Sorte des Kanals v Programmvariable von der Sorte des Kanals 1.3. PROGRAMMIERSPRACHEN ZUR BESCHREIBUNG. . . Sender Beispiel: 1 2 4 6 8 10 189 Empfaenger Erzeuger / Verbraucher |- channel m c; var m x, z := x0, z0; ||- while not B(x) do receive x on c; consume(x) od || while not B(x) do producenext(z); send z on c od -|| -| 1 2 4 6 8 proc consume = (m x): if x <> 0 then r := x * r fi proc producenext = (var m x): if x <> 0 then x := x-1 fi 10 B(x) = (x = 0) 12 m = nat Sender: schickt die Zahlen x0-1, x0-2, ..., 0 Empfänger: multipliziert die Zahlen in r auf. Wichtige Fragen: (1) Was passiert, wenn receive v on c ausgeführt wird und keine Nachricht vorliegt? Prinzipielle Möglichkeiten: (a) Es wird eine leere Nachricht zurückgegeben. (b) Es wird gewartet, bis eine Nachricht vorliegt. Wir wählen den Fall (b). (2) Was passiert, wenn send E on c ausgeführt wird, aber kein Empfänger bereit ist, die Nachricht zu empfangen? (a) Die Nachricht geht verloren. (b) Es wird gewartet, bis ein Empfänger bereit ist und dann wird die Nachricht übergeben (Telefonprinzip, Handshaking, synchroner Nachrichtenaustausch). 190 KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . . (c) Die Nachricht wird gesendet, gegebenenfalls zwischengepuffert (d.h. in einem Puffer / einer Warteschlange zwischengespeichert), bis ein Empfänger empfangsbereit ist (d.h. eine receive – Anweisung ausführt) (asynchroner Nachrichtenaustausch). Für (c) gibt es Varianten, falls die Puffergröße endlich beschränkt ist. Konvention: receive v on c und send E on c stehen für asynchronen, v ? c und E ! c für nachrichtensynchronen Nachrichtenaustausch. Achtung: Bei synchroner Arbeitsweise treten leicht Verklemmungen auf! 1 2 4 ||- send E1 receive || send E2 receive on x1 on x2 c1; on c2 c2; on c1 -|| keine Verklemmung 1 2 4 ||- E1 x1 || E2 x2 ! ? ! ? c1; c2 c2; c1 -|| Verklemmung Auch bei Regelung, wie viele der parallel ablaufenden Programme einen Kanal nutzen dürfen, können wir unterschiedliche Festlegungen treffen. (1) Pro Kanal gibt es genau ein Programm, das sendet, und ein Programm, das empfängt (exklusive Nutzung des Kanals). kein Nichtdeterminismus (2) Mehrere Sender: ||- send 1 on c || send 2 on c || receive x on c -|| Empfänger kann zuerst 1 oder zuerst 2 empfangen. Nichtdeterminismus (3) Mehrere Empfänger: ||- send 1 on c || receive x on c || receive y on c -|| Einer der Empfänger erhalt die Nachricht — welcher, ist nicht festgelegt. Nichtdeterminismus Fazit: Beim asynchronen wie beim synchronen Austausch von Nachrichten entsteht Nichtdeterminismus, wenn parallel gesendet oder parallel empfangen wird. Beim asynchronen Nachrichtenaustausch findet ein Informationsfluss nur in eine Richtung statt — vom Sender zum Empfänger. Anders beim synchronen Nachrichtenaustausch: Die Tatsache, dass eine Nachricht synchron übergeben wurde, liefert dem Sender die Information, dass der Empfänger eine entsprechende Anweisung ausgeführt hat. Im asynchronen Fall ist diese Form der Rückkopplung durch Rückkanäle zu übermitteln. Beispiel: 1 2 4 6 Asynchrone Kommunikation mit Rückkopplung sort ack = {ack}; proc sender = (channel m c, channel ack a): |- var m z = z0; var ack y = ack; while not B(z) do 1.3. PROGRAMMIERSPRACHEN ZUR BESCHREIBUNG. . . c Sender a 191 c’ Medium Empfaenger a’ Abbildung 1.10: Entkopplung von Sender und Empfänger durch ein Medium Sender Medium ack on a m on c Empfaenger ack on a’ m on c’ Abbildung 1.11: Message Sequence Chart 8 10 12 14 16 18 20 producenext(z); receive y on a; send z on c od -| proc receiver = (channel m c, channel ack a): |- var m x := z0; while not B(x) do send ack on a; receive x on c; consume(x) od -| channel m c, channel ack a; ||- sender (c, a) || receiver (c, a) -|| Frage: Wie können wir Sender und Empfänger besser entkoppeln? 1 2 4 6 8 proc medium = (channel m c, c’, channel ack a, a’): |- var m t := t0; var ack test = ack; while true do send ack on a; ||- receive test on a || receive t on c -||; send t on c’ od -| Frage: Wie muss das Medium formuliert werden, um Sender und Empfänger noch stärker zu entkoppeln? Diese Muster der Zusammenarbeit können anschaulich in Interaktionsdiagrammen (Message Sequence Charts, MSC) beschrieben werden (s. Abb. (1.11)). Zusammenarbeit: ||- sender (c, a) || medium (c, c’, a, a’) || receiver (c’, a’) -|| Auch das Steuern und Regeln von Systemen (Stichwort: eingebettete Systeme) kann über Nachrichtenaustausch modelliert werden. 192 KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . . Crashsensor Speedsensor c s a Airbag− Controller p Zuend− mechanismus Panel Abbildung 1.12: Airbagsteuerung Beispiel: 1 2 4 6 8 10 12 14 16 18 20 22 Airbagsteuerung (ohne Echtzeitbetrachtung) sort crashsignal = {ok, crash, error}; proc airbagcontroller = (channel crashsignal c, p, a, channel nat s): |- var nat speed := 0; var bool on := true; var crashsignal cs := ok; while on do ||- receive speed on s || receive cs on c -||; if cs = ok then send ok on p elif cs = crash then if speed > 20 then send crash on a else send crash on p; on := false fi else send error on p; on := false fi od -| Obwohl in diesem Programm das wichtige Thema Echtzeit ausgespart ist, wird die Logik des Auslöseverhaltens doch sichtbar. Mit den soweit verfügbaren Sprachmitteln können wir nicht alternativ auf Nachrichten von unterschiedlichen Kanälen warten. Dafür benötigen wir ein weiteres Sprachmittel. Dieses Konzept der Guarded Communication Commands geht zurück auf die Guarded Commands von Edsger Dijkstra (+ 2002) und CSP von Tony Hoare. do higcomi od if higcomi fi hgcci ::=do od|if higcomi ::= hgcomi { hgcomi}∗ hgcomi ::= hexpi : send hexpi on hidi then hstatementi | hexpi : receive hidi on hidi then hstatementi | hexpi : hexpi! hidi then hstatementi | hexpi : hidi? hidi then hstatementi Beispiel: 1 Nachrichtenweiche sort weiche = {links, rechts, alle}; 1.3. PROGRAMMIERSPRACHEN ZUR BESCHREIBUNG. . . 2 4 6 8 193 var weiche w := alle; var m v := ...; do w <> links: receive v on y then send v on r [] w <> rechts: receive v on x then send v on r [] true: receive w on z then nop od 1.3.3 Nachrichtenaustausch über gemeinsame Variablen Tritt in parallel ablaufenden Programmen eine Variable in einem Programm auf der linken Seite einer Zuweisung (wird geschrieben) und in mindestens einem weiteren Programm auf (gelesen oder geschrieben), dann sprechen wir von einer gemeinsamen Variablen. Bei gemeinsamen Variablen ist es entscheidend, festzulegen, welche Anweisungen atomar (in einem Stück, ununterbrochen, unteilbar) ausgeführt werden, da die Verzahnung der Anweisungen Auswirkungen auf das Ergebnis hat. Darüber hinaus wollen wir die Ausführung bestimmter Anweisungen zurückstellen, bis gewisse Bedingungen gelten. Diese Forderungen erreichen wir durch folgendes Sprachkonstrukt: await E then S endwait (bewachter kritischer Bereich) mit: E boolescher Ausdruck, Wächter S Anweisung, kritischer Bereich Beispiel: 1 2 var nat x, y := 0, 0; ||- await y > 0 then x := x+1 endwait || await true then y := y+1 endwait -|| Hier ist y gemeinsame Programmvariable. Die Anweisung wird wie folgt ausgeführt: Sobald die Bedingung E wahr ist, wird die Anweisung S als unteilbare Aktion ausgeführt. Falls E nicht wahr ist (dabei erfolgt auch die Auswertung von E als atomarer Schritt), wird gewartet, bis E zutrifft. Achtung: • Die Auswertung von E auf wahr und die Ausführung von S wird ebenfalls als unteilbare Aktion gesehen. ⇒ wenn mit der Ausführung von S begonnen wird, ist E sicher wahr. • Falls E nie wahr wird, ist die Ausführung des Programms für immer blockiert. Beispiel: 1 2 4 6 8 Parallele Berechung der Fakultät var bool taken, var nat y, x, z := true, 1, n, 0; ||- while n > 0 do await taken then taken, z := false, n endwait; n := n-1 od || while x > 1 do await not taken then taken, x := true, z endwait; y := y*x od -|| 194 KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . . Gemeinsame Variablen (shared variables): taken, z Wichtig: • Hier kommen gemeinsame Variablen nur innerhalb von await – Anweisungen vor. • Die Feststellung, ob eine Variable als gemeinsame Variable benutzt wird, ist syntaktisch überprüfbar. • Ob alle gemeinsamen Variablen nur innerhalb von await – Anweisungen auftreten, ist auch syntaktisch überprüfbar. Auch wenn der Zugriff auf gemeinsame Variablen durch await geschützt wird, kann Nichtdeterminismus auftreten: 1 2 4 6 8 10 12 var bool taken, var nat x, y := false, 0, 0; ||- await true then if not taken then taken := true; x := 1 fi endwait || await true then if not taken then taken, y := true, 1 fi endwait -|| Nachbedingung: {taken ∧ ((x = 1 ∧ y = 0) ∨ (x = 0 ∧ y = 1))} Zusammenhang zum Nachrichtenaustausch: Asynchroner Nachrichtenaustausch kann durch eine Warteschlange als gemeinsame Variable in Programmen modelliert werden. channel m c steht für var queue m c := emptyqueue send E on c steht für await true then c := stock(c, E) endwait receive v on c steht für await c <> emptyqueue then v, c := first(c), rest(c) endwait Eines der frühen Mittel zur Synchronisation von parallel ablaufenden Programmen sind Semaphore. Ein ganzzahliges (genau natürlichzahliges) Semaphor entspricht einer Variablen der Sorte nat, die nur in einer genau festgelegen Weise verändert und gelesen werden darf. Deklaration eines Semaphors: sema nat x := n; Operationen: 1 2 proc P = (sema nat x): await x > 0 then x := x-1 endwait 4 proc V = (sema nat x): await true then x := x+1 endwait Beispiel: 1 2 4 6 Erzeuger / Verbraucher durch Semaphore sema nat s1, s2 := 0, 1; var m x, y, z := x0, y0, z0; ||- while not B(x) do P(s1); x := y; V(s2); 1.3. PROGRAMMIERSPRACHEN ZUR BESCHREIBUNG. . . 195 p0 p1 p4 p2 p3 Abbildung 1.13: Fünf Philosophen 8 || 10 12 14 consume(x) od while not B(z) do producenext(z); P(s2); y := z; V(s1); od -|| Gemeinsame Variablen: s1, s2, y Jetzt ist die Frage, ob der Zugriff auf gemeinsame Variablen geschützt ausgeführt wird, keine syntaktische Eigenschaft mehr, sondern Teil der Logik des Programms. Boolesche Semaphore: 1 2 4 6 sema bool s := b; proc P = (sema bool s): await s then s := false endwait proc V = (sema bool s): await true then s := true endwait Beispiel: Problem der fünf dinierenden Philosophen (englisch: dining philosophs) Jeder Philosoph braucht zwei Gabeln, um zu essen. Jeder Philosoph isst und denkt abwechselnd. 1. Lösung: Wir verwenden await 1 2 var array [0:4] bool gabeln; gabeln[0], ..., gabeln[4] := true, ..., true; 4 proc philo = (nat i): while true do think; await gabeln[i] and gabeln[i+1 mod 5] then gabeln[i], gabeln[i+1 mod 5] := false, false endwait; eat; await true then gabeln[i], gabeln[i+1 mod 5] := true, true endwait 6 8 10 12 196 14 16 KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . . od ||- philo(0) || ... || philo(4) -|| Ziel des parallelen Programms zur Synchronisation der Philosophen: • Deadlock (zyklische Wartezustände) vermeiden! • Starvation vermeiden! Kein Programm kommt. verhungert, indem es nie zum Zug • Sind die Synchronisationsmittel fair? Schwache Fairness: Wenn jemand unbeschränkt lange wartet und die Fortsetzungsbedingung immer wahr ist, dann kommt er schließlich zum Zug. Starke Fairness: Wenn jemand unbeschränkt lange wartet und die Fortsetzungsbedingung immer wieder wahr wird, dann kommt er schließlich zum Zug. • Kritischer Bereich wird geschützt. Stehen nur Semaphore zur Verfügung (kein await), so wird es schwierig, die Wartebedingung auszudrücken. 2. Lösung (1 Semaphor) 1 2 4 6 8 10 12 14 16 18 sema nat s := 1; proc philo = (nat i): |- var bool ready := false; while true do think; ready := false; while not ready do P(s); if gabeln[i] and gabeln[i+1 mod 5] then gabeln[i], gabeln[i+1 mod 5] := false, false; ready := true fi; V(s) od; eat; P(s); g[i], g[i+1 mod 5] := true, true; od -| Lösung ohne 1 2 4 6 8 10 busy wait: Jede Gabel entspricht einem Semaphor. array[0:4] sema nat g; g[0], ..., g[4] := 1, ..., 1; proc philo = (nat i): |- while true do think; P(g[i]); P(g[i+1 mod 5]); eat; V(g[i]); V(g[i+1 mod 5]); od -| 1.3. PROGRAMMIERSPRACHEN ZUR BESCHREIBUNG. . . 197 Achtung: Deadlock, falls alle Philosophen gleichzeitig die linke Gabel aufnehmen! Lösungsidee: Bei Philosoph 4 die Anweisungen P(g[0]) und P(g[4]) vertauschen. Beobachtungen: (1) Gabel 4 ist frei oder einer der Philosophen 3 oder 4 hat beide Gabeln. (2) Gabel 3 ist frei oder Philosoph 2 hat beide Gabeln oder Philosoph 3 hat sie. Konsequenz: Einer der Philosophen ist immer essbereit oder die Gabeln für ihn sind frei. ⇒ kein Deadlock! (Präzise Argumentation: über Invarianten) 3. Lösung: Jeder hungrige Philosoph trägt sich in eine Warteschlange ein. 1 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 array[0:4] sema nat k; k[0], ..., k[4] := 0, ..., 0; var queue nat q := emptyqueue; proc philo = (nat i): |- while true do think; P(s); q := stock (q, i); V(s); P(k[i]); P(s); g[i], g[i+1 mod 5] := false, false; V(s); eat; P(s); g[i], g[i+1 mod 5] := true, true; V(s); od -| proc manage =: |- var bool busy = false; var nat i := 0; while true do P(s); if q <> emptyqueue then i, q, busy := first(q), rest(q), true fi; V(s); P(s); if g[i] and g[i+1 mod 5] then V(h[i]); busy := false; fi V(s) od -| Frage: Behindern sich die Philosophen mehr als unbedingt erforderlich? Ein Sprachelement, das die synchronisierte Arbeitsweise mit gemeinsamen Variablen regelt (ohne Semaphoren oder await) sind Monitore (nach C.A.R. Hoare, Per Brinch-Hansen). Idee: In einem Monitor werden wie in einem Objekt in der objektorientierten Programmierung gemeinsame Variablen gekoppelt. Zugriff auf gekapselte Variablen erfolgt nur über Prozeduren (in der Objektorientierung Methoden). 198 KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . . Regel: Jeweils eine Prozedur kann gleichzeitig ausgeführt werden. Es wird jeweils höchstens ein Aufruf ausgeführt, bis er regulär terminiert oder in einen Wartezustand geht. Wartezustände werden (ähnlich wie P und V bei Semaphoren) über Operationen auf sogenannten Signalen erreicht. Beispiel: 1 2 monitor EV = |- var queue m q := emptyqueue; signal nonempty; 4 proc send = (m x): |- q := stock (q, x); nonempty.signal -| 6 8 10 12 proc receive = (var m x): |- if q = emptyqueue then nonempty.wait fi x, q := first(q), rest(q) -| 14 16 18 20 22 24 -| var m x, z := x0, z0; ||- while not B(x) do EV.receive(x); consume(x); od || while not B(z) do producenext(z); EV.send(z) od -|| Beobachtungen: Alle gemeinsamen Variablen sind im Monitor gekapselt! Koordination über Signale; ein Signal entspricht einer booleschen Variablen, vorbesetzt mit false. nonempty.signal steht für nonempty := true. nonempty.wait steht für 1 2 nonempty := false; await nonempty then nop endwait Hinweis: Monitore dienen für Java als Vorlage für Nebenläufigkeit. 1.3.4 Sprachmittel zum Erzeugen paralleler Abläufe Bisher haben wir Parallelität explizit über ||- p1 || ... || pn -|| eingeführt. Daneben gibt es noch ein weiteres Mittel zur Einführung von Parallelität: Das Starten einer weiteren, parallel ablaufenden Aktivität durch einen Prozeduraufruf. (s. Abb. (1.14)) Fork: Aufspalten von einer Aktivität in zwei parallele. Join: Aufsammeln zweier Aktivitäten in eine. Beispiel: Die Anweisung ||- s1 || s2 || ... || sn -|| kann wie in Abbildung (1.15) verstanden werden. 1.3. PROGRAMMIERSPRACHEN ZUR BESCHREIBUNG. . . (a) fork 199 (b) join Abbildung 1.14: Parallele Abläufe durch fork und join s1 Abbildung 1.15: s2 ... sn Parallele Anweisung durch fork und join Varianten von fork und join finden sich in zahlreichen (modernen) Programmiersprachen (bzw. deren Bibliotheken). Motivation für die explizite Erzeugung paralleler Prozesse: • Erzeugung von Familien paralleler Abläufe • Möglichkeit der Identifikation verschiedener Prozesse Hinweis: Mit ||- p1 || ... || pn -|| werden n anonyme Aktivitäten erzeugt, welche die Programme p1 bis pn ausführen. • Ausführung von Operationen auf Prozessen via deren Identifikator, z.B. Anhalten, Fortsetzen, Beenden und Priorisierung • Modularisierung paralleler Abläufe Sprachmittel: • process x deklariert den Bezeichner x, mit dem ein Programmablauf identifiziert werden kann. • start p name x erzeugt einen Prozess entsprechend der durch p gegebenen Anweisung und weist x eine Referenz auf diesen neuen Prozess zu. • terminated x: boolescher Ausdruck; true: der durch x bezeichnete Prozess ist beendet; false: der durch x bezeichnete Prozess läuft. • terminate x: der durch x bezeichnete Prozess und alle durch ihn initiierten Prozesse werden beendet. 1 2 4 6 process x; proc p =: ...; ... start p name x; ... if terminated x then 200 8 10 KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . . ... else terminate x fi Die Ausführung von start (bzw. fork) ähnelst der Ausführung einer Prozedur. Daraus resultieren häufig Missverständnisse, da zwischen den Ereignessen des erzeugten und denen des erzeugenden Prozesses - im Gegensatz zu Prozeduren - bereits bei der Initiierung der start – Operation kein kausaler Zusammenhang besteht (sofern nicht zusätzliche Synchronisationskonzepte eingesetzt werden). Beispiel: Sei x1 ein Prozess, der mit start p name x2 einen zweien Prozess x2 erzeugt. Unmittelbar nach der Ausführung von start p name x2 ist es möglich, dass x2: • noch nicht mit der Ausführung von p begonnen hat • p ausführt • bereits beendet ist Erläuterung der verschiedenen Konzepte zur Spezifikation von Parallelität: Beispiel: Binäre Suche in einem Feld Gegeben sei ein Feld von n natürlichen Zahlen: var array[1:n] nat a Zu entwickeln: proc bsuche = (int x, int low, int high, var bool b) 1 2 4 6 8 10 12 14 proc bsuche = (int x, int low, int high, var bool b): |- var int m := (low + high) / 2; if low <= high then if a[m] = x then b := true else var bool bl, bh; ||- bsuche (x, low, m-1, hl) || bsuche (x, m+1, high, bh) -||; b := bl or bh fi else b := false fi -| Analyse: Kaskade von maximal 2n − 1 parallel ablaufenden Prozessen bsuche. b enthält das Resultat, nachdem alle Aufrufe terminiert sind. Ineffizient: Es wird weitergesucht, selbst wenn die gesuchte Zahl bereits gefunden wurde. Abhilfe: Austausch von Nachrichten, ob die Zahl bereits gefunden ist, mit gemeinsamer Programmvariable b. 1 2 4 proc bsuche = (int x, int low, int high, var bool b): |- var int m := (low + high) / 2; if low <= high then if a[m] = x then await true then b := true endwait 1.3. PROGRAMMIERSPRACHEN ZUR BESCHREIBUNG. . . 6 201 else var bool bn; await true then bn := b endwait; if not bn then ||- bsuche (x, low, m-1, b) || bsuche (x, m+1, high, b) -|| fi fi fi -| 8 10 12 14 Analyse: Es werden keine neuen Prozesse mehr erzeugt, sobald einer der Prozesse x gefunden hat. Alternative ohne gemeinsame Programmvariable mittels terminate: 1 2 4 6 8 10 12 14 16 18 20 proc bsuche = (int x, int low, int high, var bool b): |- var int m := (low + high) / 2; if low <= high then if a[m] <> x then process pl, ph; var bool bl, bh := false, false; start bsuche (x, low, m-1, bl) name pl; start bsuche (x, m+1, high, bh) name ph; await bl or bh or (terminated pl and terminated ph) then b := bl or bh endwait; if not terminated pl then terminate pl fi if not terminated ph then terminate ph fi else b := true fi fi -| Erzeugung eines Baumes von Prozessen. Findet ein Kind x, so wird das Ergebnis nach oben propagiert. Alle noch nicht beendeten Prozesse werden dann zwangsweise terminiert. Bemerkungen: Es gibt eine Vielzahl von Beschreibungstechniken für Systeme parallel ablaufender Prozesse, die sich im wesentlichen auf die bisher beschriebenen Konstrukte zurückführen lassen. Für parallele Programmsysteme ist der Nachweis von Eigenschaften und die Sicherstellung der Korrektheit besonders schwierig. Auf die Beschreibung quantitativer Zeitaspekte wurde hier verzichtet. Zeitkritische Programme erfordern weitere Konzepte und werden in der Echtzeitverarbeitung zur Steuerung realer Prozesse (reaktive Systeme) verwendet. 1.3.5 Nebenläufigkeit in Java Java bietet mit einem Threadkonzept eine technische Realisierung für parallele Aktivitäten an. Unter einem Thread (= Ausführungsfaden) versteht man eine eigenständige Aktivität in einem Rechensystem, die eine Folge von Anweisungen ausführt. Jedes sequentielle Programm kann als Ausführung eines Threads verstanden werden. 202 KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . . new Thread() start running yield runnable I/O, sync, ... not running run terminiert dead Abbildung 1.16: Zustände eines Threads Thread-Erzeugung in Java In Java ist ein Thread stets ein Objekt der Klasse Thread oder einer Unterklasse hiervon. Sei MyThread eine Unterklasse von Thread. Ein Objekt m der Klasse MyThread wird mit 1 2 MyThread m; // vereinbart, mit m = new MyThread(); // instantiiert und mit m.start(); // gestartet Durch die ererbte Methode start() wird eine neue Aktivität p2 gestartet, die parallel zur Aktivität p1, die start() aufgerufen hat, abläuft. In dieser neuen Aktivität p2 wird die Methode run() des Objektes m ausgeführt. Der Thread ist so lange lebendig, bis die Methode run() endet. Daraus geregeben sich für einen Thread die Zustände gemäß Abbildung (1.16). Die Methode run() der Klasse Thread führt lediglich nop aus und kehrt zurück, d.h. für jeden nichttrivialen Thread muss eine Unterklasse der Klasse Thread erstellt und run() überschrieben werden. Weitere Operationen: • m.suspend() unterbricht den Thread und versetzt ihn in einen Wartezustand • m.resume(): fortsetzen aus dem Wartezustand • m.join(): warten auf das Terminieren des Threads • m.stop(): bricht den Thread ab. Achtung: nichtdeterministischer Zwischenzustand am Ende! Synchronisation in Java Soweit können mehrere Threads (parallel ablaufende Aktivitäten) nebeneinander in einem Objekt aktiv sein. Dabei ist in der Regel die Bernsteinbedingung verletzt und es können schwer beherrschbare Effekte entstehen. Abhilfe schafft eine Synchronisation der Methoden. Dort gilt für die Ausführung der so gekennzeichneten Methoden das Monitorprinzip: public synchronized void method (...) ... um festzulegen, dass die Methode synchronisiert ausgeführt wird, d.h. während der Ausführung der Methode durch einen Thread (die Ausführung umfasst auch Methodenunteraufrufe!) darf kein anderer Thread in dem Objekt aktiv sein. Bemerkung: Zusätzlich kann man durch synchronized(x) ... ein beliebiges Objekt x für andere Threads sperren. 1.3. PROGRAMMIERSPRACHEN ZUR BESCHREIBUNG. . . 203 Typisch für Monitore stehen folgende Signaloperationen zur Verfügung: • x.wait(): Anhalten eines Threads und Entblockieren der Synchronisation • x.notify(): Aufwecken eines Threads, der auf ein Signal wartet • x.notifyall(): Aufwecken aller Threads, die auf ein Signal warten 204 KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . . Kapitel 2 Betriebssysteme und systemnahe Programmierung Rechenanlage: Hardware, Gesamtheit aller Geräte: • Rechnerkern • Busse • Speicher • Peripherie • Kommunikationseinrichtungen • ... Wir erhalten ein maschinennahes Programmierkonzept (Maschinensprache). Um einen Rechner für Anwender bequemer nutzbar zu machen, verwendet man eine Familie von Programmen (Systemsoftware), die die Nutzung erleichtern. Wir sprechen auch von dem Betriebssystem. Dazu zählen Anwendungsprogramme in der Regel nicht. Der Nutzer und der Programmierer greifen über die vom Betriebssystem zur Verfügung gestellten Funktionen auf den Rechner zu. Ein Betriebssystem realisiert eine Nutzer- und Programmierschnittstelle. Dabei nutzt das Betriebssystem die Hardware – Schnittstelle der Rechenanlage. Die Hardware kann als eine Familie von Betriebsmitteln gesehen werden (Speicher, Rechenkapazität, Übertragungskapazität, Druckmöglichkeiten etc.). Das Betriebssystem unterstützt den Nutzer beim Zugriff auf diese Betriebsmittel. Dabei nutzt das Betriebssystem selbst die Betriebsmittel für seine eigenen Zwecke. Das Betriebssystem ist also stets durch zwei Schnittstellen gekennzeichnet: ➥ Hardware – Schnittstelle ➥ Nutzerschnittstelle Wir sprechen deshalb auch von einer Betriebssystem – Schicht (s. Abb. 2.1). Wichtige Aspekte eines Betriebssystems: • Nutzerschnittstelle und Leistungsumfang • Vorgesehene Hardware – Schnittstelle • Aufbau, Struktur und Realisierung des Betriebssystems 205 206KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG Nutzerschnittstelle Betriebssystem Hardware Abbildung 2.1: Betriebssystem – Schicht Die Entwicklung eines Betriebssystems ist eine Software-Entwicklungsaufgabe. Die Programmier – Teilaufgaben sind in der Regel aus dem Bereich der Systemprogrammierung. Aufgaben der Systemprogrammierung sind dadurch gekennzeichnet, dass: • technische Eigenschaften der Hardware eine (oft dominierende) Rolle spielen • Echtzeit (Zeitverhalten) von Bedeutung ist • maschinennahe Sprachelemente verwendet werden • Effizienz oft entscheidend ist 2.1 Grundlegende Betriebssystem – Aspekte Ein Betriebssystem hat bestimmte Aufgaben zu erfüllen und bestimmte Funktionen (Dienste) anzubieten. Welche Aufgaben und Dienste im Vordergrund stehen, hängt stark von der Betriebssituation der Rechenanlage ab. 2.1.1 Aufgaben eines Betriebssystems Betriebsmittel: alle Hardware- und Softwareanteile eines Rechensystems. Betriebssystem: Familie von Programmen zur Verwaltung der Betriebsmittel und zur Unterstützung des Nutzers beim bequemen Zugriff. Wichtige Teilaufgabe des Betriebssystems: Steuerung und Überwachung der Ausführung von Anwendungsprogrammen. Dabei ist eine wesentliche Frage, wie viele Programme gleichzeitig (nebeneinander) zur Ausführung kommen. Aufgaben des Betriebssystems: • Verwaltung der Betriebsmittel für die Ausführung von Anwendungsprogrammen (Mittel: Prozessor, Hauptspeicher) • langfristige Datenerhaltung, Ablage und Zugriff auf Datenträger • Nutzung von Ein-/Ausgabegeräten (Bildschirm, Tastatur, Maus, Drucker etc.) einschließlich Datenfernübertragungseinrichtungen und Sensorik/Aktuatorik. Zweiter Aufgabenbereich: • Benutzerverwaltung – Benutzeridentifikation: Im System ist die Unterscheidung von Benutzern vorgesehen. – Benutzerauthentifikation: Überprüfung, ob der Benutzer der ist, der er vorgibt zu sein (Passwörter, Biometrie, Zugangsschlüssel, . . . ). 2.1. GRUNDLEGENDE BETRIEBSSYSTEM – ASPEKTE 207 – Authorisierung, Benutzerrechte: Jeder Nutzer hat gewisse Nutzungsrechte, deren Einhaltung vom Betriebssystem überprüft wird. • Prozessverwaltung Sind mehrere Programme im System gleichzeitig bereit zur Ausführung, so muss das Betriebssystem regeln, welches Programm als nächstes an die Reihe kommt und dabei sicherstellen, dass alle benötigten Betriebsmittel verfügbar sind. Achtung: Im Zusammenhang mit Betriebssystemen verwenden wir den Begriff Prozess in einer leicht anderen Bedeutung: • Prozess im Betriebssystem: Programm in Ausführung im Rechner • Bisher: Familie von Ereignissen / Aktionen mit Kausalitätsrelation • Zusammenhang: Jedes Programm wird ausgeführt, indem eine Menge von Ereignissen / Aktionen stattfinden mit einer Kausalitätsrelation, die sich aus der Logik des Programms ergibt. Für das Betriebssystem gibt es eine Fülle von Informationen über den Betriebszustand eines Rechensystems, die das Betriebssystem für die Erfüllung seiner Aufgaben benötigt: • Benutzerdaten • Daten für langfristige Datenerhaltung • Daten über Hardware / Geräte • aktuelle Betriebsdaten: – Zustand des Prozessors und der Prozesse – Zustand und Aufteilung des Hauptspeichers – Belegung der Geräte (Geräteverwaltung) Die Aufgaben eines Betriebssystems hängen eng von der Betriebsart ab: ➥ Anzahl der interaktiven Nutzer: • Einplatzsysteme • Mehrplatzsysteme ➥ eigentliche Betriebsart: • Stapelbetrieb • Dialogbetrieb • Prozesssteuerbetrieb ➥ Anzahl der Prozesse im System: • Einprogrammbetrieb • Mehrprogrammbetrieb Mehrprogrammbetrieb führt in der Regel auf Multiplexbetrieb: Mehrere Programme werden zeitlich verzahnt ausgeführt. Die Organisation eines Betriebssystems hängt natürlich stark von der Hardware – Struktur ab. Prinzipielle Unterschiede ergeben sich aus der Zahl der verfügbaren Prozessoren: 208KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG • Einprozessorsystem • Mehrprozessorsystem Auch in Einprozessorsystemen existieren über Gerätetreiber oft weitere, (eingeschränkt) programmierbare Hardwareeinheiten. Dadurch liegt nicht nur bei Mehrprozessorsystemen, sondern auch bei Einprozessorsystemen ein gewisser Grad echter Parallelität vor. Beispiel: Algorithmus des Bankiers Sinn: Problematik der Betriebsmittelvergabe erläutern. Betrachtete Problemsituation: Eine Bank B hat n Kunden. Jeder Kunde k besitzt eine maximale Kreditzusage mk und einen augenblicklichen Kredit ak . Es existieren folgende Vereinbarungen zwischen Bank und Kunden (Kontrakt): (1) Garantie der Bank für jeden Kunden k: Falls k mehr Kredit benötigt, wird die Bank im Rahmen der Kreditzusage den Kredit irgendwann auszuzahlen. (2) Garantie des Kunden: Der Kunde zahlt seinen Kredit, nachdem er ihn bis zu einer bestimmten Höhe (maximal mk ) in Anspruch genommen hat, irgendwann zurück. Achtung: Die Garantien bestimmen sich wechselseitig. Nebenforderung: Die Bank sollte Anfragen nur zurückweisen, wenn das zwingend erforderlich ist, um die Garantie der Bank einhalten zu können. Frage: Wann ist ein Zustand der Bank sicher? Zustand der Bank: K Menge der Kunden mk Kreditlimit des Kunden k ∈ K ak aktueller Kredit des Kunden k, ak ≤ mk r Restkapital der Bank Definition sicher: Der Zustand der Bank ist für die Menge K sicher, wenn gilt: Es existiert ein Kunde k ∈ K: (1) k kann ausgezahlt werden: mk − ak ≤ r (2) Die Stellung mit K \ {k} Kunden und Restkapital r + ak ist sicher. Beobachtung: Die Bank ist genau dann sicher, wenn es eine Reihenfolge für die Bedienung der Kunden gibt (scheduling), die funktioniert. Die Bank kann (in einer sicheren Stellung) eine Anfrage eines Kunden nach mehr Kreditauszahlung ohne Probleme sofort befriedigen, falls die dadurch entstehende Situation für die Bank wieder sicher ist. Falls die Bank unvorsichtig ist, kann sie in eine unsichere Stellung geraten. Eine unsichere Stellung für die Bank nennen wir auch instabil. Falls das Verhalten der Kunden das erlaubt, kann die Bank aus einer unsicheren Stellung in einen sicheren Zustand kommen (sich stabilisieren). In einem unsicheren Zustand können die Kunden die Bank in eine Verklemmung führen (d.h. zu einem Nichteinhalten der Garantie). Moral von der Geschichte: Das Betriebssystem muss die Betriebsmittelvergabe so vornehmen, dass Verklemmungen (unsichere, instabile Zustände) vermieden werden. Dabei ist aber gleichzeitig dafür zu sorgen, dass die Betriebsmittel im Sinne der Anforderungen optimal ausgenutzt werden. Die Qualität eines Betriebssystems zeigt sich insbesondere unter Überlast. Hauptziele eines Betriebssystems: • Zuteilung der Betriebsmittel unter optimaler Befriedigung der Nutzanforderungen und Maximierung des Durchsatzes (Anzahl der gerechneten Programme / Zeiteinheit) 2.1. GRUNDLEGENDE BETRIEBSSYSTEM – ASPEKTE 209 • Geschickte Verlagerung von Programmen und Daten zwischen Haupt- und Hintergrundspeicher • Steuerung und optimale Ausnutzung der E/A – Geräte 2.1.2 Betriebsarten (a) Stapelbetrieb: Es werden Ströme von Auftragspaketen verarbeitet. Dabei müssen alle Bestandteile eines Auftragspakets vollständig beschrieben sein, damit dieses eigenständig (ohne Rückfrage) bearbeitet werden kann. Nachfragen unter der Bearbeitung sind nicht vorgesehen. Dabei sind die Aufträge sorgfältig zu planen, damit keine Zwischenfehler mit Folgeproblemen auftreten. Die Auftragspakete sind wieder strukturiert in Unteraufträge (Unterabschnitte). Abschnitt: Ausführung einer in einer Programmiersprache abgefassten Programmeinheit. Die Sprache des Betriebssystems (Kontrollsprache) dient der Formulierung der Auftragspakete. Ein- und Ausgabe erfolgt über Dateisysteme (file systems) oder Datenbanken. Besonders wichtig beim Stapelbetrieb (bei dem typischerweise umfangreiche Aufträge mit langer Rechendauer bearbeitet werden) ist der Durchsatz (Anzahl bearbeiteter Aufträge pro Zeiteinheit). Bei Stapelbetrieb fließt durch das System ein Abschnittsstrom. (b) Dialogbetrieb: Der Systemnutzer erteilt dem System Aufträge im Dialog und ein Teil der Ein/Ausgabe erfolgt auch im Dialog. Es findet eine Interaktion statt zwischen dem Nutzer und dem ablaufenden Programm. Die Aufgabe des Betriebssystems ist die Steuerung des Dialogs und der Ein-/Ausgabe aus den Programmen. Beherrschend ist dabei die Kommandosprache des Betriebssystems (die unter Umständen über graphische Nutzerschnittstellen angesprochen wird). Typisch für Dialogsysteme: Der Nutzer bewegt sich auf unterschiedlichen Dialogebenen. Jede Dialogebene stellt eine bestimmte Auswahl von Dialogbefehlen zur Verfügung. Ein Teil dieser Befehle dient dazu, die Ebenen zu wechseln, andere erlauben die Ausführung bestimmter Anweisungen innerhalb einer Ebene. Dies führt auf komplexe Nutzerschnittstellen, für die gute Hilfefunktionen und Benutzerführung wichtige Aufgaben sind. (c) Prozesssteuerung: Werden durch Rechensysteme technische oder organisatorische Prozesse überwacht und gesteuert, so sprechen wir von Prozesssteuerung (auch von eingebetteten Softwaresystemen und Echtzeitverarbeitung). 98% aller programmierbaren Rechner laufen eingebettet. Beispiele: Robotik, Verkehrstechnik, Produktionstechnik, Hausgeräte, Medizintechnik, Sonderfall: Kommunikationstechnik Eine Sonderstellung nehmen transaktionsorientierte Systeme ein. Dies sind typischerweise Datenbankanwendungen, bei denen eine große Anzahl von Nutzern gleichzeitig auf eine Datenbank zugreifen (z.B. Platzbuchungssysteme). Hierbei bilden in der Regel mehrere Interaktionen eine Einheit (Transaktion) (z.B. Anfrage, ob ein Platz verfügbar ist und zu welchem Preis und Buchung oder Reservierung). Oft sind bei Systemen alle drei Betriebsarten vorzufinden. Beispiel: Recheneinsatz im Krankenhaus Ein Großrechner verarbeitet: 210KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG Prozessor Speicher Kanal Eingabegeraet (Lochkartenleser) Ausgabegeraet (Drucker) Abbildung 2.2: Einfaches Rechensystem • im Stapelbetrieb: Verbuchung der Daten, Drucken von Rechnungen, . . . • im Dialogbetrieb: Eingabe neuer Daten am Bildschirm (Krankenaufnahme) • Prozesssteuerung: Überwachung von Daten in der Intensivstation Dabei wird auf die unterschiedlichen Aufgaben unterschiedliche Priorität gelegt. Weiteres Thema: Verteilte Rechensysteme, Rechnernetze 2.1.3 Ein einfaches Betriebssystem für den Stapelbetrieb Wir betrachten exemplarisch das einfache Rechensystem aus Abbildung (2.2). 1 2 4 6 8 10 12 14 16 proc reader =: |- var string lochkarte; while true do readnext (lochkarte); await l_free then l_register, l_free := lochkarte, false endwait od -| proc printer =: |- var string register; while true do await not d_free then d_free, register := true, d_register endwait; print (register) od -| 18 20 22 24 26 28 30 proc processor =: |- var nat k, var string sr; while p_active do for k := AR to ER do await not l_free then l_free, sr := true, l_register endwait; sp[k] := sr od {fuehre Programm in Speicherabschnitt AR bis ER aus und lege das Resultat im Speicherabschnitt von AW bis EW ab} for k := AW to EW do 2.1. GRUNDLEGENDE BETRIEBSSYSTEM – ASPEKTE 32 34 36 38 40 sr := sp[k]; await d_free then d_free, d_register := false, sr endwait od od 211 -| |- [0:n] array string sp; var string d_register, l_register; var bool d_free, l_free, p_active := true, true, true; ||- processor || reader || printer -|| -| Diese Programme liefern uns ein einfaches Betriebssystem. Nachteil: Das Betriebssystem ist ineffizient, da in der Regel die Geschwindigkeit der Ausführung der Befehle für den Drucker und das Lesegerät viel kleiner ist als für den Prozessor. 1 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 proc channel =: |- var string s, sr; var nat k, i, j; while p_active do await c_active then s, k, i := job, aa, ea endwait; if s = "read" then for j := k to i do await not l_free then l_free, sr := true, l_register endwait; sp[j] := sr od else for j := k to i do sr := sp[j]; await d_free then d_free, d_register := false, sr endwait od fi await true then c_active := false endwait od -| proc processor =: |- while p_active do await not c_active then c_active, job, aa, ea := true, "read", AR, ER endwait await not c_active then nop endwait {Rechne Programm im Speicherabschnitt AR bis ER} await not c_active then c_acitve, job, aa, ea := true, "write", AW, EW 212KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG endwait await not c_active then nop endwait od 40 42 Auch die Version mit Auftragsverteilung an den Kanal bringt keinen wesentlichen Vorteil, da der Prozessor auf das Ende des Lesevorgangs bzw. des Druckvorgangs warten muss. Idee: Wir arbeiten gleichzeitig mit mehreren Programmen. 2.1.4 Ein einfaches Betriebssystem für Multiplexbetrieb Idee: Wir arbeiten gleichzeitig mit drei Programmen: (1) druckt (2) rechnet Pipeline (3) wird eingelesen 1 2 4 6 8 10 12 14 16 proc processor =: |- var nat i := 0; await not cr_active then cr_active, ar, er := true, AR(0), ER(0) endwait; while p_active do i := (i+1) mod 3; await not cr_active then cr_active, ar, er := true, AR(i), ER(i) endwait; i := (i-1) mod 3; {Rechne Programm im Speicherabschnitt i} await not cp_active then cp_acitve, ap, ep := true, AR(i), ER(i) endwait; i := (i+1) mod 3 od -| 18 34 proc input_channel =: |- var nat k, i; while p.active do await cr_active then k, i := ar, er endwait; for j := k to i do await not l_free then l_free, sr := true, l_register endwait; sp[j] := sr od await true then cr_active := false endwait od -| 36 proc output_channel =: 20 22 24 26 28 30 32 2.1. GRUNDLEGENDE BETRIEBSSYSTEM – ASPEKTE 213 Speicher E−Kanal Prozessor Leser Unterbrechung A−Kanal Schreiber Abbildung 2.3: Betriebssystem mit Warteschlangen 38 40 42 44 46 48 50 |- var nat k, i; while p.active do await cp_active then k, i := ap, ep endwait; for j := k to i do sr := sp[j]; await d_free then d_free, d_register := false, sr endwait od await true then cp_active := false endwait od -| Dieses einfache Betriebssystem arbeitet effizient, wenn der Zeitaufwand für das Lesen, Drucken und Rechnen eines Programms etwa gleich groß ist — wenn nicht, gibt es wieder Wartezeiten. Durch die Einführung von Warteschlangen erreichen wir, dass die Geräte (im Prinzip) immer voll ausgelastet sind. Zur Verwaltung der Geräte und ihrer Warteschlange muss der Prozessor immer wieder seine Arbeit unterbrechen, um Aufträge zu behandeln und zu erteilen (s. Abb. (2.3)). Dabei haben wir zwei Aufgaben zu lösen (Scheduling des Betriebssystems): ➥ Organisation des Wechsels zwischen der Ausführung von Anwenderprogrammen und Verwaltungsaufgaben (Ausführung von Betriebssystemprogrammen) ➥ Auswahl der Reihenfolge der Ausführung der rechen-/ druck-/ lesewilligen Programme Auswahl der Reihenfolge der Ausführung: • Warteschlangenorganisation (auch andere Prinzipien wie Stack etc.) • Prioritäten (Prioritätswarteschlange): Bei der Wahl eines Programms für den nächsten Ausführungsschritt wird jeweil das mit der höchsten Priorität gewählt. – statische Priorität: Die Priorität eines Programms ändert sich während seiner Abarbeitung nicht. 214KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG – dynamische Priorität: Die Priorität eines Programms wird während der Abarbeitung geändert. Für die Festlegung, wann die Ausführung eines Programms unterbrochen wird, gibt es zwei prinzipielle Möglichkeiten: • Zeitscheibenverfahren: Es wird eine Zeitdauer festgelegt, über die der Rechner an einem Prozess arbeitet. Spätestens nach Ablauf dieser Zeitspanne (oder nach Terminierung des Prozesses) wird die Abarbeitung unterbrochen und anstehende Verwaltungsaufgaben ausgeführt. Kritisch: Wie groß die Zeitscheibe wählen? Bei zu großen Zeitscheiben werden die Geräte nicht ausgelastet, bei zu kleinen Zeitscheiben fällt zu viel Verwaltungsaufwand an. • Unterbrechungskonzept: Die Ausführung von Anwenderprogrammen wird unterbrochen, sobald die Verwaltungsaufgaben dies erfordern. Beispielsweise melden sich die Gerätetreiber / Kanäle durch Signale beim Rechnerkern zurück. Dabei ist es sinnvoll, dass der Rechnerkern seine augenblickliche Abarbeitung noch so lange fortsetzt, bis ein Zeitpunkt erreicht wird, an dem die Arbeit später sinnvoll fortgesetzt werden kann. Bedeutet die Unterbrechung, dass die augenblickliche Arbeit nie fortgesetzt werden muss, so kann auch ein schneller Abbruch erfolgen. (Zusammenhang: unteilbare Aktion ∼ ununterbrechbare Aktion) Für Unterbrechungen können ganz unterschiedliche Gründe vorliegen: – äußere: Eingaben des Operators – verwaltungstechnische: Geräte benötigen Aufträge – innere: Das auszuführende Programm läuft in eine Fehlersituation Das System läuft oft in unterschiedlichen Unterbrechungsmodi, d.h. gewisse Unterbrechungssignale führen nicht auf eine Unterbrechung, wenn Aufgaben mit höherer Priorität ausgeführt werden. Das Scheduling bestimmt bei komplexen Rechensystemen, die im Merprogrammbetrieb arbeiten, weitgehend den Wirkungsgrad eines Systems (= Grad der Nutzung des Systems im Verhältnis zwischen Aufgaben und technischer (Rechen-)Leistung). Um den Wirkungsgrad eines Systems empirisch einzuschätzen, werden Leistungsmessungen durchgeführt. Wichtige Kennzahlen: • Verweilzeit: Wie lange ist ein Antrag bis zu seiner Abarbeitung im System (auch von Interesse: mittlere Verweilzeit)? • Ankunftsrate: Wie viele neue Aufträge pro Zeiteinheit? • Durchsatz: Anzahl der pro Zeiteinheit abgearbeiteten Aufträge 2.2 Benutzerrelevante Aspekte eines Betriebssystems Die Benutzerschnittstelle eines Betriebssystems besteht im Wesentlichen in den Funktionen, die das Betriebssystem dem Nutzer zur Verfügung stellt, und aus den Effekten, die sich ergeben und vom Nutzer bemerkt werden. Typische Dienste: • Rechnerleistung 2.2. BENUTZERRELEVANTE ASPEKTE EINES BETRIEBSSYSTEMS 215 • Speicherung von Daten (langfristig) • Übertragung von Information (Kommunikationsdienste) und Zugriff auf entfernte Informationen (Internet) • Zugriff auf Geräte 2.2.1 Kommandosprache des Betriebssystems Das Betriebssystem stellt dem Nutzer bestimmte Operationen zur Verfügung. Diese werden über Kommandos ausgelöst. Im Betriebssystem gibt es einen Kommandointerpreter, d.h. eine Zustandsmaschine, die die Kommandos als Eingabe nimmt und in Folge entsprechende Betriebssystemprozeduren aufruft. 2.2.2 Benutzerverwaltung Bei Betriebssystemen mit einer Vielzahl von Benutzern wird durch das Betriebssystem die Nutzerverwaltung bereitgestellt. Jeder Nutzer kat Kenndaten: • Benutzer – ID • Passwort • Rechte – Rechenzeit – Speicher – besondere Dienste – Prioritäten – ... Dabei werden auch Betriebsdaten über den Nutzer gesammelt (beispielsweise für eine Abrechnung). 2.2.3 Zugriff auf Rechenleistung Der Zugriff auf Rechenleistung findet in Mehrprogrammsystemen in Konkurrenz mit anderen statt. Abhängig vom Betriebssystem kann der Nutzer diesen Zugriff unter Umständen steuern (Prioritäten, Rechenzeitschranken etc.). 2.2.4 Dateiorganisation und Verwaltung Damit Nutzer Informationen über längere Zeiträume im Rechensystem speichern können (Persistenz), stellt das Betriebssystem dem Nutzer eine Dateiorganisation und -verwaltung zur Verfügung. Wichtige Aspekte: • Umfang und Größe des Speicherplatzes • Art der Speichermedien (Platte, CD, Disc etc) • Art des Zugriffs, Sicherung von unberechtigten Zugriffen oder Datenverlust / -verfälschung • Art der Organisation in Dateibäumen, Hierarchien, Namensgebung etc. Interne Aufgaben des Betriebssystems: 216KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG • Lokalisierung der Daten auf den Speichermedien • Verwaltung der Speichermedien • Transport zwischen den Speichermedien Dateikonzept (UNIX, DOS, OS/2, Windows, . . . ) • Benutzersicht – mit einem Namen versehene Zusammenfassungen eines strukturierten Datenbestands – Dienste: erzeugen, lesen, schreiben, Rechte vergeben, umbenennen, kopieren, löschen, . . . • Realisierung durch das Betriebssystem – Bytefolgen bzw. Folgen von Records (abhängig vom Medium / Betriebssystem) – Speicherung in der Regel in Blöcken einheitlicher Größe auf dem Medium Vgl. Daten ↔ Information: Die Bedeutung einer Datei wird vom Benutzer festgelegt; das Betriebssystem verwaltet nur uniforme Container. + universell, für beliebige Datenbestände nutzbar, effizient - Benutzerprogramme müssen Dateiinhalte selbst korrekt interpretieren → Aufwand, fehleranfällig Beispiel: ditor Speicherung eines Java-Objekts als Datei und Bearbeitung mit Texte- Dateisysteme • Aus Gründen der Übersicht und der Effizienz ist es zweckmäßig, größere Mengen benannter Dateien hierarchisch (in Bäumen / Graphen) zu strukturieren. • Betriebssysteme bieten hierfür spezielle Dateien — Verzeichnisse — an. • Ein Verzeichnis fasst eine Menge von Dateien und Verzeichnissen zu einer Einheit zusammen und enthält die erforderliche Verwaltungsinformation. Beispiel: Ausschnitt aus einem UNIX-Dateisystem / home usr lib var etc Der Zugriff auf Dateien erfolgt über Zugriffspfade, die aus einer Folge von Verzeichnisnamen, gefolgt vom Namen der Datei, bestehen. Arten von Dateien in einem Dateisystem: • gewöhnliche Dateien: Benutzerdaten, Programme, . . . Blätter des Verzeichnisbaums • Verzeichnisdateien (directories) interne Knoten • (UNIX): spezielle Dateien zur Repräsentation von Geräten (z.B. CD-ROM, Audio, Scanner, . . . ) 2.2. BENUTZERRELEVANTE ASPEKTE EINES BETRIEBSSYSTEMS P1 abstrakt P2 abstrakt P3 real real Betriebssystem 217 Verbindungsnetz Hardware Betriebssystem Hardware Rechner 2 Rechner 1 Abbildung 2.4: Interprozesskommunikation (IPC) 2.2.5 Übertragungsdienste Motivation: Kommunikation zwischen den verschiedenen Benutzern oder den verschiedenen Prozessen eines Benutzers, z.B.: E-Mail, Koodination paralleler Abläufe, .... Da es sich bei den Kommunikationspartnern um Abläufen von Programmen handeln, sprechen wir von Interprozesskommunikation (IPC). Wir unterscheiden lokale IPC für Kommunikation innerhalb der Grenzen eines Rechners und verteilte IPC (vgl. Abb. (2.4)). Gängige Betriebssysteme stellen für untershciedliche Kommunikationsanforderungen zahlreiche verschiedene IPC-Dienste zur Verfügung. Merkmale: • lokal / verteilt • Bandbreite (schmal- / breitbandig) ∼ maximal übertragbare Datenmenge je Zeiteinheit • Latenz ∼ minimale Dauer für die Übertragung einer Nachricht • verbindungslos / verbindungsorientiert • Anzahl der sendenden und empfangenden Prozesse (1:1, 1:n, n:m) • (A)Synchronität von Sende- und Empfangsereignissen • Niveau der Kontrolle durch das Betriebssystem (in- / offizielle Kanäle) Beispiele: • Signals / Interrupts: lokal, schmalbandig • Pipes (UNIX): lokal, breitbandig, asynchron, verbindungsorientiert Benutzersicht • verbindungslose IPC: Kommandos für Übertragungsauftrag, Parameter: Empfänger (ggf. Netzadresse), Nachrichten, Lage, u.U. Diensteigenschaften (z.B. Weg) • verbindungsorientierte IPC: zusätzlich Kommandos für Verbindungsauf- und -abbau • in der Regel Erfolgs- / Fehlermeldung nach Beendigung der Übertragung 218KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG request Client−Prozess Server−Prozess reply Betriebssystem Betriebssystem Netz Das Client – Server – Modell Mit den genannten IPC-Diensten können verschiedene Kommunikationsstrukturen realisiert werden. Wichtige Kommunikationsstrukturen sind: • Gruppenkommunikation • Peer – to – Peer – Kommunikationssysteme; jeder Kommunikationspartner hat die gleichen Rechte und Fähigkeiten • Client – Server: asymmetrisch Client-Server-artig strukturierte Systeme sind weit verbreitet. Sie bestehen aus zwei Arten von Rechnern bzw. Prozessen: • Clients: Dienstnutzer, erteilen Aufträge • Server: Dienstanbieter, nehmen Aufträge entgegen und bearbeiten diese Client-Server-artige Kommunikation ist nicht auf den verteilten Fall beschränkt. Client- und Serverprozess können sich auf dem selben Rechner befinden. Beispiel: • Druckdienst, Dateiverwaltung • UNIX X11 – Server und Clients (z.B. mozilla, gvim, . . . ) Middleware Große, räumlich verteilt ablaufende Prozgrammsysteme (z.B. Flugbuchungssysteme) stellen auf Grund der großen Menge zu transportierender Nachrichten und ihrer Heterogenität (Programmiersprachen, Datenformate, Hardware-Plattformen) hohe Anforderungen an Übertragungsdienste. Unter dem Begriff Middleware werden Betriebssystemerweiterungen verstanden, die auf den IPC-Primitiven vorhandener Betriebssysteme basieren und die benötigten, höherwertigen, plattformübergreifenden Übertragungsdienste für verteilte Anwendungen zur Verfügung stellen. Dienste (u.a.): • Remote Procedure Call (RPC), Remote Method Invocation (RMI) • verteilte Namens- und Verzeichnisdienste • Konvertierung zwischen Repräsentationen (Programm, Hardware) • Dienstlokalisierung • Persistenz: verteilte Speicherung • Lastverteilung 2.2. BENUTZERRELEVANTE ASPEKTE EINES BETRIEBSSYSTEMS Rechner A Rechner B 219 Rechner C Verteilte Anwendung Middleware Betriebssystem Betriebssystem Betriebssystem Netz Abbildung 2.5: Middleware Client−stub des Dienstes D call Parameter packen Ergebnis entpacken Server−stub des Dienstes D Parameter entpacken call Ergebnis entpacken Betriebssystem D Betriebssystem Netz Abbildung 2.6: RPC Beispiel: RPC Idee: Analog zum gewöhnlichen Unterprogrammaufruf eine Prozedur / Funktion auf einem entfernten Rechner aufrufen. Einsatz: weit verbreitet; z.B. Network File System (NFS) Probleme: heterogene Hardware, Hardware-Ausfälle, . . . Anforderungen: • transparenter Nachrichtenversand • Transformation von Parametern, Ergebnissen • Gewährleistung der Semantik bei partiellen Systemausfällen Konzept: • Indirektion des Prozeduraufrufs über zusätzliche Client-Server-stub-Funktionen (∼ Stummel), die den Nachrichtenversand organisieren. • Für jede entfernt aufrufbare Prozedur wird ein Client-Server-stub-Paar benötigt. • Stub-Funktionen werden auf der Basis einer RPC-Spezifikation mit einem Werkzeug generiert. Beispiel: PRC einer Funktion D: s. Abb. (2.6) 220KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG Nutzerebene Verwaltung Betriebsmittelspeicher Tasks des Betriebssystems Benutzerprozesse Dateiverwaltung Systemtask Speicherverwaltung Platten Zeitgeber ... Prozessverwaltung und −steuerung Abbildung 2.7: Aufbau eines Betriebssystems 2.2.6 Zuverlässigkeit und Schutzaspekte Betriebssysteme sind die Basis aller Benutzerpgramme, äußerst komplex und extremen Belastungen ausgesetzt. Hinzu kommen Eingabe- und Hardwarefehler. Betriebssystemausfälle können zu kostspieligen Informationsverlusten und Nutzungsausfällen mit Folgeschäden führen. Die Zuverlässigkeit und Betriebssicherheit von Betriebssystemen ist daher entscheidend. Betriebssysteme sollten auf extreme Bedingungen, Hardware- und Nutzungsfehler möglichst unempfindlich reagieren, d.h. robust und fehlertolerant. Betriebssysteme verwalten umfangreiche Daten verschiedener Personen. Es muss vermieden werden, dass Benutzer — unabsichtlich oder vorsätzlich — unbefugt Daten lesen, verfälschen, zerstören oder kopieren können. Wir sprechen von Datenschutz und Datensicherung. • Sperrmechanismen • Passwörter • Verschlüsselung Schematischer Aufbau eines vereinfachten Betriebssystems (angelehnt an UNIX, nach Tanenbaum) in Schichten: s. Abb. (2.7). Die einzelnen Teile des Betriebssystems sind Programme, die bestimmte Aufgaben wahrnehmen; wir sprechen auch von Tasks. 2.3 Betriebsmittelverteilung Aufgabe des Betriebssystems: Zuteilung der Betriebsmittel unter dem Ziel der optimalen Auslastung der Betriebsmittel und der optimalen Versorgung der Tasks. 2.3.1 Prozessorvergabe Im Multiplexbetrieb sind mehrere Rechenwillige Programme (Tasks) gleichzeitig im System vorhanden. Das Betriebssystem regelt die Prozessorzuteilung und die Verwaltung der Tasks. Strategien für Prozessorvergabe (Scheduling): • lifo • fifo • Prioritäten • Mischformen Frage, wie lange ein Prozess (Task) ausgeführt wird: 2.3. BETRIEBSMITTELVERTEILUNG 221 • unbeschränkte Ausführung • Zeitscheibenverfahren • Unterbrechungskonzept Beim Unterbrechen von Programmen in Ausführung (Prozessen) müssen alle Daten gerettet werden, die das Fortsetzen erlauben. Dieses Datenpaket, das für die Fortsetzung entscheidend ist, beschreibt den Prozess eindeutig. Wir sprechen von einer Prozesspräskription. Auf vielen Rechensystemen laufen alle wichtigen Vorgänge auf nur einem Prozessor streng sequentiell ab. Trotzdem macht es Sinn, die Vorgänge gedanklich in eine Form von parallel ablaufenden Programmen zu strukturieren. Wir sprechen von quasi-paralleler Verarbeitung. 2.3.2 Hauptspeicherverwaltung Aufgaben: (1) Zuteilung des Hauptspeichers an Tasks / Prozesse: Der Hauptspeicher zerfällt in die Datenbestände der rechenwilligen Programme. (2) Verlagerung von Speicherinhalten in den Hintergrundspeicher. Dadurch wird erreicht, dass der gedachte Hauptspeicher (virtueller Speicher) viel größer ist als der wirklich vorhandene (näheres später). (3) Schutz der einzelnen Speicherabschnitte vor unberechtigtem Zugriff Beispiel: Speicherverwaltung Der Speicher entspricht logisch einem Feld var [0:n] array m h. Ziel: In h sollen k Untersequenzen (Teilfelder), genannt Segmente, der Länge li , 1 ≤ Pk i ≤ k abgelegt werden. (Annahme: i=1 li ≤ n) Die Zahl k der Segmente und ihre Ausdehnung li kann sich dynamisch ändern. Folgende Prozeduren sind zu implementieren: proc kreiere = (var nat i): legt neues Segment der Länge 0 auf h mit Namen k an proc erweitere = (nat i, m x): erweitere Segment i um einen Wert und besetze den Speicherplatz mit x proc lese = (nat i, nat j, var m x): lies im i-ten Segment den Eintrag mit Index j und weise das Ergebnis x zu proc laenge = (nat i, var nat j): weise j die Länge li des Segments zu proc loeschen = (nat i): lösche i-tes Segment ... Probleme: (1) Die benötigten Speicherbereiche sind ungleichmäßig groß. (2) Sie ändern ihren Umfang während des Betriebs. (3) Der Hauptspeicher ist insgesamt zu klein (weil zu teuer). (4) Inhaltlich zusammengehörige Datenbestände sollen auch organisatorisch zusammengefasst werden. Lösungen: 222KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG • Segmentierung – Unterteilen der Speicherelemente in Segmente • Seitenaustauschverfahren – gedachter Hauptspeicher ist viel größer als echter, Teile der Daten liegen auf dem Hintergrundspeicher. 2.3.3 Betriebsmittelvergabe im Mehrprogrammbetrieb Die Bedürfnisse der Programme nach Betriebsmitteln variieren stark. Wir spechen von: • rechenintensiven Programmen • speicherintensiven Programmen • E/A- und kommunikationsintensiven Programmen • verwaltungsintensiven Programmen Aufgaben: • Verzögerung der Ausführung einzelner Programme • faire Zuteilung der Betriebsmittel • Minimierung des Organisationsaufwands • Minimierung der Umschaltzeiten bei Unterbrechungen • Regelung der Zugriffsrechte Betriebsarten: • halbduplex: Es wird abwechselnd ein- und ausgegeben. • vollduplex: Es wird gleichzeitig ein- und ausgegeben. Im Dialogbetrieb führt jedes eingegebene Zeichen zu einer Unterbrechnung, um es im Prozessor zu behandeln. Aufgaben des Betriebssystems im Dialogbetrieb: • Aufsammeln der eingegebenen Zeichen zu Zeichenfolgen (Kommandos) • Zuordnen der E/A zu Benutzerprogrammen und den E/A – Geräten • Ausgabe am Bildschirm (Echo der Eingabe) • Verwaltung der dialogspezifischen Nutzerdaten 2.3.4 Zuteilung der E/A – Geräte Das Betriebssystem verwaltet und steuert die E/A – Geräte. Dabei setzen Nutzerprogramme E/A – Aufträge an das Betriebssystem ab, die dann vom Betriebssystem durchgeführt werden. (Details später) 2.4. TECHNIKEN DER SYSTEMPROGRAMMIERUNG 2.4 223 Techniken der Systemprogrammierung Die Systemprogrammierung ist die Lehre • vom Entwurf, • der Realisierung und • der Eigenschaften der Familie von Programmen eines rechnersystems, welche die Annahme und Bearbeitung von Nutzeraufträgen unter gewissen Optimalitätsgesichtspunkten organisieren und teilweise durchführen. Optimalitätsgesichtspunkte: • Zuverlässigkeit (Verfügbarkeit, Robustheit) • Komfort, Antwortzeiten • Sicherheit (security) • Änderungsfreundlichkeit, Wertbarkeit Typische Aufgaben der Systemprogrammierung: • Entwurf und Realisierung von Betriebssystemen • Kommunikationsaufträge in Rechner- oder Telekommunikationsnetzen • Übersetzung und Implementierung von Programmiersprachen • Entwurf und Realisierung eingebetteter Software – Systeme Dabei werden neben den üblichen Programmier- und Softwarekenntnissen (Algorithmen, Datenstrukturen, Softwarearchitekturen) auch Kenntnisse über Systemund Hardwareeigenschaften vorausgesetzt. 2.4.1 Unterbrechungskonzept Unterbrechungen werden aus internen oder externen Gründen notwendig. Beispiele: • zugeteilte Prozessorzeit verbraucht • Geräte oder Daten werden angefordert, sind aber nicht vorhanden • ein E/A – Gerät meldet sich nach ausgeführtem Auftrag zurück • der Operator unterbricht die Ausführung • ein Fehler (Alarm) tritt auf Zur Behandlung von Unterbrechungen könnte ein erweiterter Befehlszyklus wie folgt aussehen: 224KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG while true do Befehlszähler hochzählen Berechnung Befehlsadresse, Befehl laden Speicherschutzüberprüfung, ggf. Speicherschutzalarm falls Unterbrechung vorliegt, diese behandeln Wird ein Programm unBerechnung der Operandenadressen Speicherschutzüberprüfung, ggf. Speicherschutzalarm Befehlsüberprüfung Befehlsausführung od terbrochen, so sind alle Daten zu retten (abzuspeichern), die für eine nahtlose Fortsetzung nötig sind. Wichtige Informationen: • Registerinhalte • Angaben über die Lage des Programms und seiner Daten im Speicher Nicht zu jedem Zeitpunkt macht eine Unterbrechung Sinn. Deshalb gibt es in einer Reihe von Systemen das Konzept der Unterbrechungssperre. Ist die Unterbrechungssperre gesetzt, so kann das Programm nicht unterbrochen werden. Das Setzen der Unterbrechungssperre ist ein privilegierter Befehl. Solche Befehle können nur im Systemmodus, nicht aber im Nutzermodus ausgeführt werden. Das System arbeitet in folgenden Ausführungsmodi: • Normalmodus: Ausführung von Nutzerprogrammen und unkritischen Systemprogrammen — keine privilegierten Befehle • Systemmodus: Ausführung von Systemprogrammen, privilegierte Befehle, mit oder ohne Unterbrechungssperre Im technischen Detail wird dies am Beispiel der Maschine MI erläutert. Um Systemprogramme, die im System vorgegeben sind, nutzen zu können, rufen wir in der MI Systemdienste auf. • CHMK n: (change mode to kernel): n ist eine Zahl, die einen Systemdienst identifiziert. • REI: (return from interrupt) Wie Nutzerprogramme, verfügt auch das System über einen Stack (Systemstack). CHMK bewirkt das Abspeichern der Informationen, die für die Fortsetzung benötigt werden, im Systemstack: • Systemdienstkennzahl (Adresse für n) • aktueller Wert des Befehlszählers • aktueller Wert des PSW (Prozessorstatusregister) REI ist ein privilegierter Befehl, setzt die Register aus dem Stack in den alten Zustand zurück. • Systemaufruf: – bereitstellen gewisser Parameter (im Stack) – CHMK n – nach Rückkehr löschen der Parameter 2.4. TECHNIKEN DER SYSTEMPROGRAMMIERUNG 225 • Systemunterprogramm (Systemdienst): – PCB – Prozesskontrollblock – Systemdienst n ausführen – Auswählen eines Prozesses zur Fortsetzung – PCB laden – REI Das PCB umfasst: • Stackpegel im Systemmodus • Stackpegel im Benutzermodus • Registerinhalte • Anfangsadresse und Lage der Seitentabelle (s. später) für Benutzerprogramme und Benutzerstack • PSW Bedeutung einiger Bits im PSW: CM 6, 7 current mode: 00 Systemmodus, LL Nutzermodus PM 8, 9 previous mode N 28 negative Z 29 zero V 30 overflow C 31 carry IV 26 integer overflow Privilegierte Befehle zum Retten bestimmter Registerinhalte: SPPCB speichern des PCB Anfangsadresse im Register PCBADDR. LPCB laden des PCB 2.4.2 Koordination und Synchronisation Um die Synchronisation in der MI zu bewerkstelligen, wird ein bestimmter Befehl benötigt, in dem das Lesen und Schreiben bzw. Umsetzen des Befehlszählers als unteilbare Aktion erfolgt. JBSSI a b c (jump on bit set and set interlocked) Die Ausführung von JBSSI a b c bewirkt: • Setzen des Befehlszählers auf den durch c gekennzeichneten Wert, falls b[a] = L. • Setzen von b[a] = L, falls b[a] = 0. Dies ist analog zu: 1 2 4 6 await true then if b[a] = L then goto c else b[a] = L fi endwait Idee zur Nutzung: (busy wait) 226KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG 1 2 4 6 c: await true then if b[a] = L then goto c else b[a] = L fi endwait JBCCI (jump on bit cleared and clear interlocked): analog zu JBSSI, aber Rolle von 0 und L vertauscht. Bei der obigen Realisierung von Semaphoren durch busy wait hängt es vom Scheduler ab, ob die P- und V-Operationen fair ablaufen. Bei dieser Lösung bleibt dem Betriebssystem verborgen, dass der Prozess vor einem Semaphor wartet. Geschickter ist es, dem Betriebssystem mitzuteilen, dass ein Prozess an einem Semaphor wartet. Dazu werden Semaphore auf Betriebssystemebene verwaltet. Wir geben ein einfaches Beispiel an, wie dann P und V realisiert werden. 1 2 4 6 8 10 12 14 16 18 20 22 24 26 proc P = (var bool b, var nat s): |- a: await true then if b = 0 then goto a else b := 0 fi endwait; if s > 0 then s := s-1; await true then if b = L then goto error else b := L fi endwait else await true then if b = L then goto error else b := L fi endwait; goto a fi -| 28 30 32 34 36 38 proc V = (var bool b, var nat s): |- a: await true then if b = 0 then goto a else b := 0 fi endwait; s := s+1; await true then 2.4. TECHNIKEN DER SYSTEMPROGRAMMIERUNG if b = L then goto error else b := L fi endwait 40 42 44 227 -| Dies ist ein Beispiel dafür, wie selbst bei einem fairen Scheduling eine dynamische Verklemmung auftreten kann, falls s = 0 gilt, ein Prozess eine P-Operation versucht und ein anderer eine V-Operation. Wird P immer in dem Zustand unterbrochen, in dem er b auf 0 gesetzt hat, so kann weder die P- noch die V-Operation erfolgreich abgeschlossen werden. Ausweg (wie oben angesprochen): Semaphore auf Betriebssystemebene. • P(s) wird realisiert durch CHMK: Der Prozess geht in einen Wartezustand, falls s = 0 gilt (er wird in eine Warteschlange für s eingereiht). Sobald ein anderer Prozess eine V-Operation ausführt, wird der erste Prozess aus der Warteschlange fortgesetzt (falls die Warteschlange nicht leer ist). • V(S) wird realisiert durch CHMK: Weckt einen Prozess aus der Warteschlange auf, falls diese nicht leer ist, bzw. erhöht s. Bei dieser Technik behält das Betriebssystem den Überblick über die Zahl der wartenden Prozesse. Globale Verklemmungen (alle Prozesse warten) können erkannt werden. 2.4.3 Segmentierung Die Gesamtmenge der Speicherzellen (Adressen), auf die ein Benutzer(prozess) während seiner Ausführung exklusiv zugreift, nennen wir Benutzeradressraum. Das Betriebssystem trägt dafür sorge, dass dieser Benutzeradressraum dem Prozess ungestört und exklusiv zur Verfügung steht. In der Regel wird dieser Benutzeradressraum in Teilabschnitte, genannt Segmente, unterteilt. Aus Sicht des Nutzers ist ein Segment durch eine Nummer und seine Länge charakterisiert. Es bietet sich an, Inhalte (Speicherzellen) in Segmente über Anfangsadresse und relative Adresse im Segment anzusteuern. Vorteil: Die zufällige (vom Betriebssystem dynamisch festgelegte) Lage der Segmentinhalte im Speicher ist für den Benutzer unwichtig. Typisch in Betriebssystemen: • Der Adressraum eines Prozesses (Prozesspräskription) besteht aus einer Menge von Segmenten. • Für jedes Programm und jeden Datenbereich (file) wird ein Segment angelegt, für das Zugriffsrechte und -eigenschaften geregelt sind. • Ansteuerung der Segmentinhalte über Paare (B, β) aus Segmentnamen B und Relativadresse β. • Programmstücke in Segmenten sind als Unterprogramme organisiert. Technisch: • Jeder Benutzer bekommt eine Segmenttabelle für seine Segmente. • Für den Zugriff wird Hardwareunterstützung vorgegeben, durch spezielle Register für die Segmenttabelle und die Segmentnummer. 228KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG virtuelle Adresse Seitentabellenregister S W Seitennummer relative Adresse K Laengenregister K W physikalische Adresse Abbildung 2.8: virtuelle Adressierung Vorteile der Segmentierung: • einfache Zugrifssstruktur für den Benutzer • übersichtliche Organisation der Zugriffsrechte für das Betriebssystem mit Hardwareunterstützung Die Idee der Segmentierung lässt sich perfekt kombinieren mit der Idee des virtuellen Speichers. 2.4.4 Seitenaustauschverfahren Da der physikalische Hauptspeicher oft nicht ausreicht, alle Nutzeradressräume aufzunehmen, arbeitet man mit einem gedachten größeren Hauptspeicher (virtueller Speicher) und bildet diesen auf den physikalischen Hauptspeicher und den Hintergrundspeicher ab. Dazu wird der gedachte Hauptspeicher in Abschnitte gleicher Größe (sogenannte Seiten) zerschnitten. Gleichzeitig wird der physikalische Hauptspeicher (und ähnlich auch die Hintergrundspeicher) in gleich große Abschnitte (gleiche Größe wie die Seiten!) eingeteilt, die Kacheln. Die Seiten werden auf die Kacheln abgebildet (jede Kachel entspricht einer Seite, Seiten liegen entweder in Kacheln, auf dem Hintergrundspeicher oder sind nicht belegt). Beim gedachten Hauptspeicher arbeiten wir mit viruteller Adressierung. Die Seitentabelle enthält für jede Seite (gekennzeichnet durch die Seitennummer) die Kachelnummer. Vorteile des virtuellen Speichers: • flexible Erweiterung der Segmente um weiteren Speicherplatz • großer virtueller Adressraum • Speicherüberlagerung: effiziente Nutzung des Speichers durch mehrere Prozesse • Selbstregulierung der Ein-/Auslagerung von Seiten (Seiten nicht aktiver Prozesse werden schrittweise verdrängt) • exzellente Ergänzung mit der Segmentierung 2.4.5 Verschiebbarkeit von Programmen Bei der maschinennahen Programmierung wurde bereits betont, dass Programme so geschrieben werden sollten, dass ihre absolute Lage im Speicher für das Programm keine Rolle spielt (Stichwort: relative Adressierung). Durch die Segmentierung können wir den (virtuellen) Speicher / Adressbereich festlegen, in dem ein 2.4. TECHNIKEN DER SYSTEMPROGRAMMIERUNG Prozessadresse Segmenttabellenregister Laenge 229 SEG Ort S W Segmenttabelle Laenge Ort Zugriffseigenschaften Seitentabelle K fuer Organisationszwecke und Zugriffsrechte K W Speicheradresse Abbildung 2.9: Kombination von Seitenaustauschverfahren und Segmentierung Programm abgelegt ist. Dadurch kann überprüft werden, ob ein Programm unberechtigt auf den Speicher zugreift. Technisch kann dies durch Hardware unterstützt werden, indem man Register für Anfangs- und Endadresse vorsieht. Im Befehlszyklus wird überprüft, ob die angesprochenen Adressen außerhalb des zulässigen Bereichs liegen. Das Setzen dieser Register ist ein privilegierter Befehl. 2.4.6 Simultane Benutzbarkeit von Unterprogrammen Bei Mehrprogrammbetrieb kann es vorkommen, dass mehrere Programm gleichzeitig (bzw. zeitlich verzahnt) das gleiche Programm arbeiten. (beispielsweise im Rahmen des Aufrufs eines Systemdienstes). Es wäre ineffizient (Verschwendung von Speicher), dieses Programm dann mehrfach (in Kopien) im Speicher zu halten. Ein Programm (Unterprogramm), das im Durchschnitt unterschiedlicher Benutzeradressräume die korrekte Ausführung für mehrere Programme gleichzeitig ohne Maßnahmen des gegenseitigen Ausschlusses garantiert, heißt simultan benutzbar. Dies setzt natürlich voraus, dass in dem Bereich des Programms nichts während der Ausführung abgespeichert wird. Technisch erfordert das, dass die Operandenversorgung konsequent über Leitadressen erfolgt. Wesentlich: • keine Abspeicherungen in das Segment, das das Programm enthält • alle Speicherzellen, die sich ändern, werden über geeignete Register angesteuert • unter Umständen ist der Speicherschutz eingeschränkt • Laden des Registers ist ein privilegierter Befehl • Ansteuern der Operanden nach festen Regeln Wieder kann das hardwareseitig unterstützt werden (Translationsregister). 230KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG 2.4.7 Steuerung von E/A – Geräten Die Zuteilung und Verwaltung von E/A – Geräten erfolgt durch das Betriebssystem. Hintergrundspeicher und E/A – Geräte arbeiten elektromechanisch, Prozessor und Hauptspeicher (unter Umständen auch Bildschirme) arbeiten rein elektronisch. Es ist Aufgabe des Betriebssystems, die Brücke zwischen Elektronik und Mechanik herzustellen, den Nutzer zu entlasten (von zu detaillierten Kenntnisen über die Gerätespezifikationen) und die Geräte von unsachgemäßer Ansteuerung zu schützen. Bedingt durch die Unterschiede in der Verarbeitungsgeschwindigkeit arbeiten Prozessor und Geräte asynchron. Sie sind über Kanäle verbunden. Hardwaretechnisch sind Kanäle einfache programmierbare Einheiten, die Kanalprogramme ausführen. Kanalprogramme bestehen aus Befehlsfolgen, die den Datentransport zwischen Speicher und Gerät steuern. Kanäle sind aktive Einheiten mit stark eingeschränktem Befehlssatz. Die Steuerung der Kanäle (Auftragserteilung) erfolgt über den Prozessor durch: (a) Unterbrechungskonzept: Kanäle melden sich nach Auftragsabwicklung beim Prozessor zurück, bewirken eine Unterbrechung, um vom Prozessor / Betriebssystem einen neuen Auftrag zu erhalten. (b) wiederholtes Abfragen (polling): Der Prozessor frägt periodisch die Kanäle ab und erteilt gegebenenfalls neue Aufträge. Innerhalb des Betriebssystems gibt es bestimmte Programme / Prozesse / Tasks, die für die Steuerung der E/A – Geräte mit ihren E/A – Programmen zuständig sind. Prinzipien: • Jeder Gerätetyp benötigt im Prinzip einen entsprechenden Task. • Aus Sicht des Nutzers und der übrigen Teile des Betriebssystems sind die Besonderheiten eines Geräts (Gerätetyps) im Task gekapselt. Das übrige Betriebssystem hat einheitliche Zugriffsschnittstellen für den Task. Der geräteabhängige Code ist im Gerätetreiber gekapselt. Aus Sicht des Betriebssystems sind Gerätetreiber Tasks / Prozesse wie andere auch. Die Aufgaben des Gerätetreibers hängen stark von den Eigenheiten des Geräts und des E/A – Prozessors ab. • Ist der Registerumfang des Kanals / E/A – Kontrollers klein, so muss der Gerätetreiber unter Umständen Kommando für Kommando an den Kontroller übertragen. • Ist der Registerumnfag groß, so können Kommandosequenzen abgelegt und selbständig ausgeführt werden. Abhängig vom Gerät muss der Gerätetreiber warten (Unterbrechung), bis das Gerät das Kommando / die Kommandosequenz ausgeführt hat (z.B. Drucker), oder der Kontroller reagiert so schnell, dass der Gerätetreiber nicht unterbrochen werden muss, sondern direkt mit der Übertragung fortfahren kann (z.B. Bildschirm). Neben den geräteabhängigen Anteilen gibt es geräteunabhängige Codeanteile für Gerätesteuerung im Betriebssystems mit folgenden Aufgaben: • einheitliche Schnittstelle für Nutzer zur Übergabe von E/A – Aufträgen • Namensgebung • Schutz der Geräte / Synchronisation 2.4. TECHNIKEN DER SYSTEMPROGRAMMIERUNG Benutzerprozesse 231 E/A−Aufrufe Daten zur E/A ablegen E/A−Anforderung gerateunabhaengige Software fuer E/A im Betriebssystem Geraetetreiber Namen E/A zuordnen Daten in Bloecke aufteilen und puffern Speicher zuordnen Geraeteregister setzen Status pruefen Unterbrechungskonzept Treiber aufwecken, verwalten E/A−Kontroller, Kanaele Hardware E/A−Geraet steuern Abbildung 2.10: Geräteverwaltung • Schaffung geräteunabhängiger Blockgrößen für E/A • Pufferung von Aufträgen • Speicherzuordnung • Verwaltung exklusiv genutzter Geräte • Fehlerberichte Fazit: Nutzerprozesse dürfen und brauchen nicht direkt auf E/A – Geräte zugreifen. Es ergibt sich das Bild (2.10). Vorteile: • Schutz der Geräte • Betriebssystem hat volle Kontrolle • Nutzer braucht technische Details nicht kennen • Einbringen neuer Geräte, portieren der Lösung mit minimalem Aufwand möglich Das ist ein wichtiges Beispiel für Schichtenarchitektur (layered architecture, Softwarearchitektur). Vorteil der Schichtenarchitektur: Sauberes Trennen der unterschiedlichen Aspekte. Vorteile: • Experten für die unterschiedlichen Aspekte können sich auf diese konzentrieren. • Teile können unabhängig erstellt und geändert werden. • Test der Teile kann unabhängig erfolgen. • Zusammenhang des Gesamtsystems nach klaren Vorlagen. 232KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG Nutzerschnittstelle Monitor Anwendungsschicht Zugriffsschicht DB Systemdienste Abbildung 2.11: typische Schichtenarchitektur Technisch essentiell: Festlegung der Schnittstellen der Bausteine / Schichten (Komponenten). Die Schnittstelle beschreibt, wie die Komponenten mit ihrer Umgebung zusammenarbeiten. Literaturhinweis zu Betriebssystemen: Andrew S. Tanenbaum, Betriebssysteme: Entwurf und Realisierung 2.4.8 Kommunikationsdienste Die Kommunikation (Datenaustausch) zwischen Prozessen auf unterschiedlichen Rechnern erfolgt in der Regel auch durch Unterstützung durch das Betriebssystem. Eine einfache Unterstützung der Kommunikation zwischen Prozessen sind sogenannte Sockets. Socket: Kommunikationsendprodukt (vom Betriebssystem aufgebaut), an das ein Nutzerprozess Daten übergeben kann (zum Senden) oder Daten übernehmen kann (zum Empfangen). Diese Daten werden von Systemdiensten über das darunterliegende Netz übertragen. Sockets sind im Umfeld von UNIX entstanden, finden sich heute aber in vielen Betriebssystemen. Sockets stellen ein API (application programming interface) zur Verfügung, über das die Kommunikationsdienste des Sockets angesprochen werden. Typische Methoden / Prozeduren: • socket: erzeugt ein neues Socket und schafft Protokoll – Port – Nummer • bind: bindet ein Socket an eine Socketadresse (die Socketadresse erlaubt zu bestimmen, welche Nachrichten für das Socket bestimmt sind) • listen: zeigt an, dass eine Verbindung gewünscht wird und erzeugt (Verwaltungsinfrastruktur für) eine Warteschlange • accept: warten auf eine Verbindungsanfrage und schaffen eines neuen Sockets für eine Verbindung • connect: Gegenstück zu accept, Verbindungsanfrage, baut Verbindung zum Server auf 2.4. TECHNIKEN DER SYSTEMPROGRAMMIERUNG server socket 233 client socket bind listen accept connect send receive close close Abbildung 2.12: einfache Kommunikation über Sockets sender A receiver B connection request A to B connection confirmation A to B Verbindungsaufbau data transfer A to B acknowledgement iteriert, Datentransport disconnection request A to B disconnection indication A to B Verbindungsabbau Abbildung 2.13: Abracadabra – Protokoll • send: Senden von Nachrichten • receive: Empfangen von Nachrichten • close: Auflösen der Verbindung Die eigentliche Übertragung und das Ineinandergreifen der Methoden- / Prozeduraufrufe in einer sinnvollen Weise wird durch ein Protokoll geregelt. Ein Protokoll legt fest, nach welchen Regeln und in welchen Formaten eine Kommunikation (einschließlich Verbindungsaufbau) abläuft. Ein Beispiel für Protokolle ist das Abracadabra – Protokoll, das in Abbildung (2.13) (stark vereinfacht) dargestellt wird. Da bei der Übertragung von Nachrichten viele unterschiedliche Aspekte zu berücksichtigen sind, baut man die Protokolle in Schichten auf. Das ISO/OSI – Schichtenmodell ISO: International Standard Organisation OSI: Open System Interconnection Protocol • Physische Schicht: Übertragung von 0 und L auf Schaltungsebene • Datenverbindungsschicht: Fehlerüberprüfung durch Paritätsbits • Netzwerkschicht: Festlegung des Übertragungsweges (routing), Übertragung in Paketen ohne direkte Verbindung (z.B. IP: internet protocol) 234KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG SAP application protocol service access point SAP presentation protocol session protocol transport protocol network protocol data link protocol physical protocol network Abbildung 2.14: ISO/OSI Schichtenmodell • Transportschicht: Fehlerbehandlung bei verlorenen Paketen (z.B. TCP: transmition control protocol) • Sessions- und Präsentationsschicht: Festlegung der Sender- und Empfängerrollen, Synchronisation der Kommunikation, Anlaufunterstützung bei Systemfehlern, Festlegung der Bedeutung der übertragenen Bits • Anwendungsschicht: Übertragung auf Anwendungsebene (z.B. HTTP, FTP) Routing von Datenpaketen: • Statisches Routing nutzt statisch vorgegebene Routingtabellen. Der Router empfängt das Datenpaket und leitet es an den Rechner weiter, an den anhand seiner Tabelle das Paket gehen soll. • Dynamisches Routing verwendet Tabellen, die über Informationen, die über das Netz eintreffen, aktualisiert werden. Probleme: • keine garantierten Übertragungszeiten (QoS) • Pakete nehmen unterschiedliche Routen und kommen unter Umständen nicht in der Reihenfolge an, in der sie abgesendet werden Jitter: Varianz bei der Verzögerung in der Übertragung Unicast: Adressieren eines einzelnen Rechners Multicast: Adressieren einer Gruppe von Rechnern Anycast: Adressieren über Gemeinsamkeiten in der Adresse 2.5. BETRIEBSSYSTEM – STRUKTUREN 235 Beispiel: TCP – Nachrichtenformat 0 15 16 31 source port destination port sequence number acknowledge number (technische Angaben) checksum urgent pointer Optionen Nutzerdaten In Netzen können Pakete verloren gehen. Diese müssen dann erneut gesendet werden. Problempunkte: (1) Fehler (Paketverlust) feststellen: Grundsätzlich ist es bei asynchroner Übertragung nicht einfach, festzustellen, dass ein Paket nicht angekommen ist. Dies kann geschehen, indem: (a) Der Empfänger darüber informiert ist (Teil des Protokolls!), dass ein Paket kommen sollte und nach einiger Zeit dem Sender mitteilt, dass es nicht eingetroffen ist. (Problem: Auch diese Mitteilung kann verloren gehen!) (b) Der Empfänger bestätigt alle eingehenden Pakete dem Sender. Der Sender wiederholt das Senden von Paketen, falls nach einer bestimmten Zeitspanne keine Bestätigung eingetroffen ist. Achtung: Bei (b) kann ein Paket beim Empfänger mehrfach eintreffen; deshalb müssen Pakete so gekennzeichnet werden (Sequenznummer), dass Kopien erkannt und aussortiert werden können. Literatur: • Tanenbaum, van Stean: Distributed Systems • Comer: Computernetzwerke und Internets 2.5 Betriebssystem – Strukturen Betriebssysteme sind heute oft sehr umfangreiche Softwaresysteme. Deshalb ist es extrem wichtig, gute Softwarestrukturierungsprinzipien anzuwenden (Stichword: Softwarearchitektur). Beispiele: OS/360 BS2000 UNIX IBM Siemens Bell Labs Großrechnerbetriebssystem für Mehrprozessbetrieb Großrechnerbetriebssystem für Mehrprozessbetrieb Timesharing – Betriebssystem für mittelgroße Rechner, Arbeitsplatzrechner, Linux für PCs PC – Betriebssystem PC – Betriebssystem MS-DOS Microsoft Windows Microsoft Mac-OS Apple Windows NT Microsoft netzwerkfähiges PC – Betriebssystem Für eingebettete Softwaresysteme existieren eigenständige Betriebssystemfamilien. 236KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG Applikation OSEK Time OSEK/VDX application layer message filter Time Service fault ident. layer Netzwerk− management comm. subsystem CNI driver Anwendungsmodule OSEK bus driver I/O−System Mikrocontroller bus communication hardware Abbildung 2.15: Struktur von OSEK running wait preemt start waiting release terminate suspended activate ready Abbildung 2.16: Task State Modell in OSEK Beispiel: OSEK im Automobilbereich (vgl. Abb. (2.15)) Die Echtzeitbetriebssysteme in mobilen Geräten (PDA, mobile Telefone etc.) verwenden ebenfalls die Konzepte allgemeiner Betriebssysteme, allerdings oft in eingeschränkter Form. Abschließende Diskussion zu Betriebssystemen Was sind gute Modelle für Betriebssysteme? ➥ monolithischer Ansatz: Das Betriebssystem besteht aus einer Familie von Prozeduren, die sich gegenseitig aufrufen. Nachteil: Software des Betriebssystems ist sehr unstrukturiert. ➥ Betriebssystem als Familie von virtuellen Zustandsmaschinen. ➥ Schichtenmodell: Betriebssystem wird aus Betriebssystemkern in Schritten erweitert. Vorteil: klare Strukturierung, einfache Portierung Erweiterung: • Prozesshierarchien 2.5. BETRIEBSSYSTEM – STRUKTUREN • Auftraggeber / Auftragnehmer – Modell 237 238KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG Kapitel 3 Interpretation und Übersetzung von Programmiersprachen Arten von Programmiersprachen: • höhere, problemorientierte Sprachen: Struktur, Konzepte der Sprache sind ausgerichtet auf die Probleme, die durch die Sprache beschrieben / gelöst werden sollen. Beispiele: Java, Pascal, Ada, Lisp • maschinennahe Sprachen: Struktur und Konzepte sind von der Hardware bestimmt. Vorteil: Maschinensprachen können unmittelbar auf der Hardware ausgeführt werden. Hardware definiert Maschinensprachen Um eine höhere Programmiersprache auf einer Maschine zur Ausführung zu bringen, bieten sich zwei Konzepte an: ➥ Interpretation: Durch ein Programm — den Interpreter (in der Maschinensprache geschrieben) — wird ein Programm der höheren Programmiersprache interpretiert, d.h. schrittweise ausgeführt. ➥ Übersetzung Durch ein Programm (in der Maschinensprache geschrieben) — Übersetzer / Compiler — wird ein Programm der höheren Programmiersprache in ein maschinennahes Programm übersetzt. Bei der Übersetzung können drei Programmiersprachen eine Rolle spielen: • Quellsprache (Sprache, in der das zu übersetzende Programm geschrieben ist) • Zielsprache (Sprache, in die das Programm zu übersetzen ist) • Sprache, in der der Compiler geschrieben ist Eine Programmiersprache ist syntaktisch durch eine formale Sprache gegeben. Eine formale Sprache ist eine (nicht notwendigerweise endliche) Menge von Zeichenfolgen über einem Zeichensatz Z, L ∈ Z ∗ . Wie natürliche Sprachen sind Programmiersprachen in der Regel Folgen von Wörtern. Wir sprechen von einer lexikalischen Struktur. Programmiersprachen werden in der Regel syntaktisch wie oben angedeutet in zwei Stufen beschrieben. 239 240 KAPITEL 3. INTERPRETATION UND ÜBERSETZUNG . . . > if a <id>nat then b <id>nat a else b fi <id>nat <id>nat <op>nat <exp> nat <exp> nat <exp> nat <exp> nat <exp> bool <if_exp>nat <op>nat Abbildung 3.1: Syntaxbaum mit Attributen • Lexikalischer Anteil: Zugelassene Wörter werden teilweise über Lexika (endliche Menge von Wörtern) oder über einfache Beschreibungsmittel (endliche Automaten, reguläre Grammatiken; siehe später) beschrieben. • Grammatikalischer Anteil: Die Reihenfolgen, in denen die zugelassenen Wörter aufeinander folgen dürfen. Darüber hinaus verwenden wir zusätzliche Regeln (Kontextbedingungen), um die Zusammensetzung weiter einzuschränken. Syntaktische Behandlung: Quellprogramm Fehlermeldung, falls ↓ lexikalische Analyse (scanner) inkonsistente Wörter auftreten Symbol- und Konstantentabelle (Tabelle aller Wörter im Quellprogramm) Indexliste1 Fehlermeldung, falls syntaktische ↓ grammatikalische / SyntaxanalyStruktur nicht korrekt se (parser) Syntaxbaum2 Fehlermeldung, falls ↓ Attributierung und Prüfung von Nebenbedingungen nicht erfüllt Kontextbedingungen Syntaxbaum mit Attributen3 In der syntaktischen Behandlung eines Programms wird überprüft, ob der Text den syntaktischen Anforderungen genügt; falls nicht, werden Fehlermeldungen erzeugt; falls doch, wird ein attributierter Syntaxbaum generiert. Problem: Fehleranalyse und Hinweise auf mögliche Fehler geben. Ausgehend von dem Ergebnis der syntaktischen Behandlung kann dann die Übersetzung oder Interpretation erfolgen. • Übersetzer Syntaxbaum mit Attributen ↓ Zwischencodeerzeugung Codeinterndarstellung ↓ Codeoptimierung optimierter Code in Interndarstellung ↓ Zielprogramm Dabei ist nur der letzte Schritt von der Maschinensprache abhängig. 1 Sequenz der Wörter, codiert dargestellt durch die Indexwerte aus der Tabelle Darstellung der grammatikalischen Struktur 3 strukturierte Darstellung der grammatikalischen Struktur mit zusätzlichen Angaben 2 strukturelle 3.1. LEXIKALISCHE ANALYSE 241 • Interpreter Syntaxbaum mit Attributen ↓ Interpreterprogramm Aufrufsequenz von Operationen Beispiel: 1 2 4 Codeoptimierung while x > y do z := x + 1; y := y + z od In dieser while – Anweisung bleibt der Wert von x konstant, d.h. z ändert sich nach der ersten Anweisung nicht mehr. Übersetzer- und Interpretertechniken sind kombinierbar (s. Java: Übersetzung in Bytecode, der dann vom Bytecodeinterpreter interpretiert wird). Sonderfall: inkrementelle Übersetzung: Der Übersetzungsvorgang benötigt nicht das gesamte Quellprogramm (bzw. den vollständigen attributierten Codebaum), sondern übersetzt bereits Teile (Anfangsstück des Quellprogramms), bevor das gesamte Quellprogramm vorliegt. Inkrementelle Interpretation: Der Interpretationsvorgang beginnt bereits, ohne dass das gesamte Quellprogramm vorliegt. 3.1 Lexikalische Analyse Es existiert eine Menge W ⊆ V ∗ von Wörtern unserer Programmiersprache. Dabei existieren in der Regel eine Reihe von Trennzeichen Z ⊆ V , die in Wörtern aus W nicht vorkommen, d.h. W ⊆ (V \ Z)∗ . Beispiele für Trennzeichen: • Zeilenvorschübe • Leerzeichen Diese Trennzeichen dienen der Abspaltung von Wörtern. haber nicht heutei ∈ V ∗ haberi , hnichti , hheutei ∈ (V \ Z)∗ , Z = { } Frage: Sind Sonderzeichen (+, >, ≥, . . . ) in frei gewählten Bezeichnern (Identifikatoren) zulässig (z.B. a+, b-)? Wenn ja, dann müssen die Regeln erzwingen, dass Formeln sauber in Wörter / Symbole aufgespalten werden. Aufgabe: (1) Beschreiben, welche Wörter in W liegen. (2) Von gegebenem Text in V ∗ von links nach rechts durchgehend eine Folge von Wörtern abspalten. Die Beschreibung erfolgt in der Regel durch einfachste Mittel wie endliche Automaten oder reguläre Grammatiken (s. später). Idee der lexikalischen Analyse: Der vorliegende Text wird zeichenweise von links nach rechts gelesen; sobald ein Wort erkannt wurde, wird das in die Symboltabelle eingetragen und der Lesevorgang wird fortgesetzt. Trennzeichen werden unterdrückt. 242 KAPITEL 3. INTERPRETATION UND ÜBERSETZUNG . . . Wenn Fehler auftreten, d.hh. wenn eine Zeichenfolge auftritt, die nicht im Wortschatz ist, wird eine Fehlermeldung abgesetzt. Über Trennzeichen kann die lexikalische Analyse auch nach einem Fehler wieder aufgesetzt werden. Beispiel für Fehler: unzulässige Zeichenkombinationen Die lexikalische Analyse ist eine in der Regel einfache Aufgabe. Scanner werden meist nicht von Hand geschrieben, sondern aus standardisierten Beschreibungen des Wortschatzes automatisch generiert. 3.2 Zerteilen von Programmen Durch die lexikalische Analyse ist unser Programmtext in eine Folge von Wörtern / Symbolen (technisch repräsentiert duch Indexwerte aus einer Tabelle) umgewandelt worden. Diese Folge wird im Zerteilvorgang vom Parser (Zerteiler) in einen Baum umgewandelt. Die Struktur des Baumes folgt der syntaktischen Beschreibung der Programmiersprache. hexpi ::= hif expi | hexpi hopi hexpi |(hexpi)| . . . hif expi ::= if hexpi then hexpi else hexpi f i Der Zerteilbaum zeigt an, wie sich das vorliegende Wort aus der BNF – Beschreibung der Sprache ableiten lässt. Im Zerteilbaum sind die Blätter die Terminalzeichen und die Markierungen im Inneren die Nichtterminalzeichen. Zerteilbäume können in Programmiersprachen wie allgemein repräsentiert werden. fct parser = (seq index s, table t) tree Frage: Welche Datenstruktur für den Zerteilbaum? Zwei Mglichkeiten: (1) universelle Baumstruktur (2) spezielle Baumstruktur für die Sprache Zu (2): Wir wandeln unsere BNF – Syntax in eine Folge von Sortenvereinbarungen um: 1 2 4 sort exp = | | | | ifexp (exp con, exp tb, exp eb) opexp (exp le, op p, exp re) klam (exp e) idexp (id i) ... Jedes Programm (d.h. jede Zeichenfolge, die der BNF – Beschreibung genügt) kann in ein Datenelement der entsprechenden Sorte umgewandelt werden. hexpi definiert eine formale Sprache, exp definiert eine Sorte, d.h. eine Menge von Datenelementen (Bäume). Wir können auch eine allgemeine Sorte definieren, die beliebige Bäume darzustellen erlaubt. sort tree = paar (index k, seq tree t) Beispiel: 1 2 if – Ausdruck paar ("ifexp", <paar ("opexp", <paar ("id", <"a">), paar ("op", <">">), paar ("id", <"b">)>), paar ("id", <"a">), paar ("id", <"b">)>); 3.3. KONTEXTBEDINGUNGEN 3.3 243 Kontextbedingungen Zerteilbäume sind Datenstrukturen. Diese erweitern wir um zusätzliche Attribute. sort att exp = pair (exp e, expsort s) Attributierung erfolgt durch das Ausrechnen der Werte der Attribute über den Zerteilbäumen. Das Berechnen der Attribute auf den Zerteilbäumen erfolgt über das Durchreichen von Informationen (Kontext) nach unten und das Berechnen gewisser Werte von unten (also von den Blättern zur Wurzel hin). (Genaueres in der Vorlesung Compilerbau). 3.4 Semantische Behandlung Im Interpreter oder Compiler müssen wir die Semantik des Programms korrekt wiedergeben. 244 KAPITEL 3. INTERPRETATION UND ÜBERSETZUNG . . . Teil IV 245 247 • formale Sprache ↔ Beschreibungsmächtigkeit • Berechenbarkeit ↔ Grenze der Algorithmik • Komplexität ↔ Aufwand in Berechnungen für Probleme • effiziente Algorithmen ↔ aufwändige Algorithmen optimieren • (Logikprogrammierung) • effiziente Datenstrukturen • Datenbankmodelle 248 Kapitel 1 Formale Sprachen Zeichensatz A A∗ Menge der Wörter über A L ⊆ A∗ formale Sprache Frage: Mit welchen formalen Mitteln können wir Sprachen L ⊂ A∗ beschreiben? 1.1 1.1.1 Relation, Graphen Zweistellige Relationen Gegeben: Grundmenge M zweistellige (binäre, dyadische) Relation: R ⊆ M × M zur Notation: (x, y) ∈ R gleichwertig xRy Beispiel: ≤⊆ N × N (1, 3) ∈≤ 1≤3 Operationen auf Relationen: R, R1 , R2 ⊆ M × M • Durchschnitt: R1 ∩ R 2 • Vereinigung: R1 ∪ R 2 • Komplement: R− = (M × M ) \ R • konverse Relation: RT = {(x, y) ∈ M × M : (y, x) ∈ R} • Relationenprodukt: R1 ◦ R2 = {(x, z) ∈ M × M : ∃y ∈ M : (x, y) ∈ R1 ∧ (y, z) ∈ R2 } Monotonie: R1 ⊆ R10 ∧ R2 ⊆ R20 ⇒ R1 ◦ R2 ⊆ R10 ◦ R20 Frage: Wie Relationen definieren? • durch Aufzählung: {(3, 7), (7, 25), . . . } 249 250 KAPITEL 1. FORMALE SPRACHEN 7 25 3 32 Abbildung 1.1: graphische Relationsdefinition • logisch: {(x, y) : P (x, y)} (P . . . Prädikat) • graphisch (vgl. Abb. 1.1) • boolesche 3 3 0 7 0 25 0 32 0 Matrizen: 7 25 32 L 0 L 0 L 0 0 0 L L 0 0 Spezielle Relationen über M : • Nullrelation: 0M = ∅ • vollständige Relation: LM = (M × M ) • Identität: IM = {(x, x) ∈ M × M } Eigenschaften von Relationen R ⊆ M × M : • reflexiv: IM ⊆ R • symmetrisch: R = RT • antisymmetrisch: R ∩ RT ⊆ IM • asymmetrisch: R ∩ RT = 0M • transitiv: R ◦ R ⊆ R • irreflexiv. IM ∩ R = 0M • linkseindeutig R ◦ RT ⊆ IM (injektiv) • rechtseindeutig RT ◦ R ⊆ IM • linkstotal IM ⊆ R ◦ RT • rechtstotal IM ⊆ RT ◦ R (surjektiv) Relationsarten: Quasiordnung, Präordnung transitiv, reflexiv Striktordnung transitiv, asymmetrisch partielle Ordnung transitiv, antisymmetrisch, reflexiv lineare Ordnung transitiv, antisymmetrisch, reflexiv, R ∪ RT = LM Äquivalenzrelation transitiv, reflexiv, symmetrisch partielle Funktion rechtseindeutig totale Funktion rechtseindeutig, linkstotal Für eine gegebene Relation R ⊂ M × M heißt ein Element x ∈ M : 1.1. RELATION, GRAPHEN 251 • maximal ⇔ ∀y ∈ M : xRy ⇒ x = y • größtes Element ⇔ ∀y ∈ M : yRx Eine zweistellige Relation nennen wir auch gerichteten Graph, eine symmetrische zweistellige Relation auch ungerichteten Graph. Wir können für einen Graph R • Kantenmarkierungen: β : R → S • Knotenmarkierungen: α : M → S 0 einführen. 1.1.2 Wege in Graphen, Hüllenbildung Iteriertes Relationenprodukt: R ⊆ M × M R0 = IM Ri+1 = Ri ◦ R Ein Weg im Graph R von x0 nach xn der Länge n ist eine Folge von Knoten x0 , . . . , xn ∈ M mit ∀i, 1 ≤ i ≤ n : xi−1 Rxi Satz: Ein Weg von x nach y der Länge n existiert genau dann, wenn xRn y. Beweis: durch Induktion über n. Ist R eine partielle Ordnung, dann heißen Wege auch Ketten. Wir lassen dann auch unendliche Wege zu. R heißt Noethersch, wenn es keine unendlichen Wege gibt, in denen sich Knoten nicht wiederholen (streng aufsteigende Ketten). Beispiel: Graphen über N: (a) 0 → 1 → 2 → 3 → . . . nicht Noethersch (b) · · · → 3 → 2 → 1 → 0 Noethersch Hüllenbildung Gegeben sei eine Relation R ⊆ M × M . • reflexive Hülle Rrefl = R ∪ IM = \ {T ⊆ M × M : R ⊆ T ∧ T reflexiv} • transitive Hülle R+ = \ {T ⊆ M × M : R ⊆ T ∧ T transitiv} • reflexiv – transitive Hülle \ R∗ = {T ⊂ M × M : R ⊆ T ∧ T transitv ∧ T reflexiv} = (R ∪ IM )+ • symmetrische Hülle Rsym = R ∪ RT = \ {T ⊆ M × M : R ⊆ T ∧ T symmetrisch} • symmtrisch – reflexiv – transitive Hülle \ R∗ = {T ⊂ M × M : R ⊆ T ∧ T transitv ∧ T reflexiv ∧ T symmetrisch} 252 KAPITEL 1. FORMALE SPRACHEN a b x d f e c z g h Abbildung 1.2: Berechnungswege Zur Notation: Relation →, wir schreiben: ∗ → für die transitiv – reflexive Hülle ∗ ↔ für die symmetrisch – reflexiv – transitive Hülle. Eine Relation → heißt konfluent, wenn gilt: ∗ ∗ ∗ ∗ ∀x, y1 , y2 ∈ M : (x → y1 ∧ x → y2 ) ⇒ ∃z ∈ M : y1 → z ∧ y2 → z ∗ ∗ Relationenalgebra: RT ◦ R∗ ⊆ R∗ ◦ RT Sei R konfluent und Noethersch, dann gilt: jeder Weg (jede Berechnung) ausgehend von x endet in einem Punkt z (vgl. Abb. 1.2). 1.2 Grammatiken Formale Sprachen sind in der Regel unendlich und können deshalb nicht durch einfaches Aufzählen beschrieben werden. Deshalb brauchen wir Regeln (endliches Regelsystem), die die Wörter einer Sprache charakterisieren. Wir verwenden Textersetzungsregeln x, y ∈ V ∗ × V ∗ (V Zeichenmenge). x→y →⊆ V ∗ × V ∗ → ist eine endliche Menge von Textersetzungsregeln. Wir gewinnen aus → eine in der Regel unendliche Relation durch Regelanwendung (α, ω ∈ V ∗ ): x→y ⇒α◦x◦ω α◦y◦ω : algebraische Hülle, Halbgruppenhülle ∗ ω1 ω2 : ω1 wird in ω2 durch eine Folge von Ersetzungsschritten überführt. (V ∗ , ) heißt Semi – Thue – System (ist → symmetrisch, dann auch Thue – System). Beispiel: V = {a, b, c} (1) ba → ab ca → ac cb → bc + Noethersch? ja + konfluent? ja Jedes Wort wird duch eine terminierende Berechnung auf die Form ai bk cj gebracht. ∗ ist eine Äquivalenzrelation 1.2. GRAMMATIKEN 253 (2) zusätzliche Regeln zu (1): aa → a bb → b cc → c (3) zusätzliche Regeln zu (1): aa → a bc → a Normalformen: abi ack a bi ck ab → b ac → c Normalformen: a bi ck ∗ Für jedes Semi – Thue – System (V ∗ , ) erhalten wir durch eine Äquivalenzrelation. [ω] bezeichnet die Äquivalenzklasse. ∗ V ∗ / bildet mit Konkatenation als Verknüpfung eine Halbgruppe mit neutralem Element [ε], genannt Regelgrammatikmonoid. 1.2.1 Reduktive und generative Grammatiken Eine Grammatik über einer (endlichen) Zeichenmenge M ist ein Tripel (M, →, z), wobei →∈ M ∗ × M ∗ endlich ist. z ∈ M heißt Axiom (Wurzel). G = (M, →, z) heißt auch Semi – Thue – Grammatik. Grammatiken beschreiben formale Sprachen. generativ: Ein Wort w ∈ M ∗ ist in der durch G beschriebenen formalen Sprache, ∗ wenn hzi w gilt. Generativer Sprachschatz: n o ∗ Lg (G) = w ∈ M ∗ : hzi w reduktiv: Ein Wort w ∈ M ∗ ist in der durch G beschriebenen formalen Sprache, ∗ wenn w hzi gilt. Reduktiver Sprachschatz: n o ∗ Lr (G) = w ∈ M ∗ : w hzi Sprechweise: w ∈ Lg (G): w wird durch G generiert (erzeugt). w ∈ Lr (G): w wird durch G erkannt. Eine generative Grammatik heißt ε – frei oder ε – produktionsfrei, wenn die rechte Seite jeder Regel nicht ε ist. Analog heißt eine reduktive Grammatik ε – frei oder ε – produktionsfrei, wenn die linke Seite jeder Regel nicht ε ist. Semi – Thue – Grammatiken haben enge Grenzen in der Ausdrucksmächtigkeit. 254 KAPITEL 1. FORMALE SPRACHEN Beispiel: Mn= {L} o k Sprache: S = L2 : k ∈ N ⊆ M ∗ Behauptung: Diese Sprache ist durch Semi – Thue – Grammatiken nicht beschreibbar. Beweis: Jede Regel hat die Form Li → Lj . Wir betrachen den reduktiven Fall. Wir benötigen mindestens eine Regel der Form Li → L mit i > 1. L muss also Axiom sein. S 63 Li Li−1 Li L Chomsky – Grammatiken strukturieren den Zeichensatz: M = T ∪N mit T ∩N = ∅. T : Terminalzeichen — Zeichen in Wörtern der formalen Sprache N : Nichtterminalzeichen — Hilfszeichen für die Ableitung Eine Chomsky – Grammatik ist ein Quadrupel G = (T, N, →, z) mit (T ∪ N, → , z) Semi – Thue – Grammatik, z ∈ N und T ∩ N = ∅. Lg (G) = Lg (T ∪ N, →, z) ∩ T ∗ Lr (G) = Lr (T ∪ N, →, z) ∩ T ∗ Beispiel: Reduktive Chomsky – Grammatik zur Beschreibung der Sprache S = k {L2 : k ∈ N}. T = {L} N = {z, A, B, C} z Axiom Regeln: L→A ε→B AAB → CB AAC → CA BC → BA BAB → z Wir betrachen den Sprachschatz der Semi – Thue – Grammatik, die wir erhalten, indem wir die ersten zwei Regeln weglassen. hBABi ist im Sprachschatz. (1) Jedes Wort im Sprachschatz hat die Form hBi ◦ x ◦ hBi mit x ∈ {AC}∗ . (2) In Rückwärtsableitung entstehen Cs immer am linken Rand. Um die Cs wieder zu entfernen, müssen sie nach rechts geschoben werden und verdoppeln dabei die Anzahl der As, d.h. die Wörter aus {AB}∗ im Sprachschatz haben die k Form BA2 B. Zwei reduktive (bzw. generative) Grammatiken heißen äquivalent, wenn sie die gleiche formale Sprache beschreiben. Eine reduktive Grammatik heißt wortlängenmonoton, wenn für jede ihrer Regeln w → w0 gilt |w| ≥ |w0 |. Gilt sogar |w| > |w0 |, so heißt die Grammatik strikt wortlängenmonoton. Konsequenz für Ableitungen: w0 w1 . . . wn |w0 | ≥ |w1 | ≥ . . . ≥ |wn | Eine Ersetzungsregel heißt separiert, wenn w0 ∈ N + ist. 1.2.2 Die Sprachhierarchie nach Chomsky Chomsky – Grammatiken lassen sich nach der äußeren Form ihrer Regeln klassifizieren. 1.2. GRAMMATIKEN 255 Chomsky – 0 – Sprachen sind formale Sprachen, die durch Chomsky – Grammatiken beschrieben werden können. Wir beschränken uns im Folgenden auf reduktive Grammatiken (generative analog). Eine Ersetzungsregel der Form u ◦ a ◦ v → u ◦ hbi ◦ v mit u, v ∈ (T ∪ N )∗ , a ∈ (T ∪ N )+ , b ∈ N heißt kontextsensitiv. Eine Chomsky – Grammatik heißt kontextsensitiv oder ε – produktionsfreie Chomsky – 1 – Grammatik, wenn alle ihrer Regeln kontextsensitiv sind. Trivialerweise gilt für alle Chomsky – 1 – Grammatiken: Alle Regeln sind wortlängenmonoton. Eine formale Sprache heißt kontextsensitiv oder Chomsky – 1 – Sprache, wenn es eine Chomsky – 1 – Grammatik gibt, die die Sprache beschreibt. Ist eine Sprache S kontextsensitiv, so wird S ∪ {ε} auch kontextsensitiv genannt. Eine Regel a → hbi mit a ∈ (T ∪ N )∗ und b ∈ N heißt kontextfrei. Sind alle Regeln der Grammatik kontextfrei, so heißt die Grammatik Chomsky – 2 – Grammatik oder kontextfrei. Jede Sprache, die durch eine Chomsky – 2 – Grammatik beschreibbar ist, heißt kontextfrei. Hinweis: BNF entspricht Chomsky – 2 – Grammatiken. Eine kontextfreie Regel der Form m ◦ hai ◦ n → hbi mit a, b ∈ N und m, n ∈ T + heißt beidseitig linear. Gilt jedoch n = ε, so heißt die Regel rechtslinear bzw. für m = ε linkslinear. Eine Regel der Form w → hbi mit w ∈ T + und b ∈ N heißt terminal. Eine Chomsky – 2 – Grammatik, deren sämtliche Regeln terminal oder rechtslinear (bzw. terminal oder linkslinear) sind, heißt Chomsky – 3 – Grammatik oder regulär. Eine Sprache heißt regulär oder Chomsky – 3 – Sprache, wenn sie durch eine reguläre Grammatik beschreibbar ist. C0 L Menge der Chomsky – 0 – Sprachen CSL Menge der Chomsky – 1 – Sprachen CF L Menge der Chomsky – 2 – Sprachen REG Menge der Chomsky – 3 – Sprachen Satz: REG ( CF L ( CSL ( C0 L 1.2.3 Strukturelle Äquivalenz von Ableitungen Ableitung über einer Grammatik: w0 w1 w2 . . . wn Fragen von Interesse: ∗ (1) Was ist ableitbar? () (2) Welche Struktur hat eine Ableitung? hBAAAABi → hBAACBi → hBCABi → hBAABi → . . . Die Anwendung von Regeln t1 → t01 und t2 → t02 in einem Wort α ◦ t1 ◦ β ◦ t2 ◦ γ α ◦ t01 ◦ β ◦ t2 ◦ γ α ◦ t01 ◦ β ◦ t02 ◦ γ mit α, β, γ ∈ (T ∪ N )∗ heißt vertauschbar. Bemerkung: Formal ist jede Ableitung ein Wort aus (T ∪ N ∪ {})∗ . Vertauschbarkeit definiert eine Relation auf der Menge der Ableitungen. Wir können zu dieser Relation die transitive symmetrische reflexive Hülle bilden. Stehen zwei Ableitungen in dieser Relation, dann heißen sie strukturell äquivalent. Bei Chomsky – 2 – Grammatiken können wir aufgrund der einfachen Struktur ihrer Regeln jeder Ableitung auf das Axiom einen Strukturbaum zuordnen. 256 KAPITEL 1. FORMALE SPRACHEN z z z z z z z z (a * a) + a + a Abbildung 1.3: Strukturbaum Beispiel Regeln: N = {z}, T = {(, ), +, ∗, a}, Axiom z. z+z →z z∗z →z a→z (z) → z Strukturbaum: s. Abb. (1.3). Wichtig: Zwei Ableitungen sind genau dann strukturell äquivalent, wenn sie den gleichen Strukturbaum darstellen, d.h. Strukturbäume sind Normalformen in der Klasse der strukturell äquivalenten Ableitungen. Ein Strukturbaum für eine Chomsky – 2 – Grammatik ist ein endlich verzweigender Baum: (1) Die Wurzel ist das Axiom. (2) Jeder Teilbaum hat folgende Eigenschaft: • Er ist genau ein Terminalzeichen und hat keine Teilbäume oder • er hat als Wurzel ein Nichtterminalzeichen x; dann gilt: die Wurzeln seiner Teilbäume können zu dem Wort w konkateniert werden und w → x ist eine Regel der Grammatik. ∗ Eine Grammatik heißt eindeutig, wenn für jedes Wort w ∈ T ∗ mit w z (z Wurzel) alle Ableitungen auf z strukturell äquivalent sind. Dann existiert genau ein Strukturbaum für jedes Wort im Sprachschatz. Feststellung: Die Grammatik aus obigem Beispiel ist nicht eindeutig. 1.2.4 Sackgassen, unendliche Ableitungen Chomsky – 2 – Grammatiken werden praktisch eingesetzt, um: (1) eine formale Sprache zu definieren (2) jedem Wort im Sprachschatz einen Strukturbaum (am besten eindeutig) zuzuordnen. 1.2. GRAMMATIKEN 257 ? E E E R R R R a * b + c E E R R R a * b + c Abbildung 1.4: Ableitung mit Sackgasse Ein Wort w heißt im Sprachschatz bzw. ∗ Grammatik akzeptiert, wenn w z gilt. Aufgaben für eine gegebene Grammatik: reduzierbar oder auch von der (1) Stelle fest, ob ein gegebenes Wort im Sprachschatz ist. (2) Ermittle (falls es im Sprachschatz ist) den Strukturbaum. Naive Idee: Wende auf das gegebene Wort Regeln an. Wir erhalten eine Ableitung w → w1 → w2 → . . . . Wir erhalten zwei Möglichkeiten: (1) Die Ableitung führt zu einem Wort, auf das keine Regel mehr anwendbar ist. (2) Die Ableitung bricht nicht ab und kann unendlich fortgesetzt werden. Falls (1) gilt und das Wort, auf das keine Regel mehr anwendbar ist, das Axiom ist, so ist w im Sprachschatz. Falls (1) gilt und das Wort, auf das keine Regel mehr anwendbar ist, nicht das Axiom ist, dann können wir daraus nicht schließen, dass s nicht im Sprachschatz ist. Wir sprechen von einer Sackgasse. Eine unendliche Ableitung (Fall (2)) sagt auch nichts darüber aus, ob das Wort im Sprachschatz ist. Beispiel: Einfache arithmetische Ausdrücke als Chomsky – 2 – Grammatik T = {a, . . . , z, +, ∗, (, )}, N = {R, E}, Wurzel: E Regeln: a→R .. . z→R R∗R→R E+R→R R→E (E) → R Ableitungen: s. Abb. (1.4). 258 KAPITEL 1. FORMALE SPRACHEN Neue Grammatik: T wie vorher, N = {F, R, A, E}, Axiom E. Regeln: a→F .. . z→F F ∗R→R R+A→A F →R R→A A→E (E) → F Diese Grammatik ist eindeutig, aber wieder nicht sackgassenfrei. In der Menge aller Ableitungen für ein Wort über einer Grammatik können wir eine sogenannte Linksableitung (analog Rechtsableitung) auszeichnen. Dies ist eine Ableitung, in der jeweils so weit links wie möglich eine Regel angewandt wird. Achtung: (1) In der Regel sind Linksableitungen nicht eindeutig. (2) In der Menge der strukturell äquivalenten Ableitungen sind Linksableitungen eindeutig. Eine Linksableitung führt für ein Wort im Sprachschatz nicht unbedingt zum Axiom. Akzeptierende Linksableitung: Regeln werden so weit links wie möglich angewendet, Regeln, die nicht zum Axiom (also in Sackgassen) führen, werden übersprungen. 1.3 Chomsky – 3 – Sprachen, endliche Automaten, reguläre Ausdrücke Wir betrachten drei Beschreibungsmittel für formale Sprachen: • reguläre Ausdrücke • endliche Automaten • Chomsky – 3 – Grammatiken Wir werden zeigen: Alle drei Beschreibungsmittel haben die gleiche Beschreibungsmächtigkeit. Sie beschreiben die gleiche Klasse formaler Sprachen, genannt reguläre Sprachen. 1.3.1 Reguläre Ausdrücke Gegeben sei eine Zeichenmenge T . Die Syntax und Semantik der regulären Ausdrücke ist wie folgt gegeben: (1) einfache reguläre Ausdrücke • x mit x ∈ T ist regulärer Ausdruck mit Sprache {hxi}. • ε ist regulärer Ausdruck mit Sprache {ε}. • {} ist regulärer Ausdruck mit Sprache ∅. (2) zusammengesetzte reguläre Ausdrücke (seien X, Y reguläre Ausdrücke) 1.3. CHOMSKY – 3 – SPRACHEN, ENDLICHE AUTOMATEN, . . . 259 • [X|Y ] ist regulärer Ausdruck mit Sprache LX ∪ LY . • [XY ] ist regulärer Ausdruck mit Sprache {x ◦ y : x ∈ LX ∧ y ∈ LY }. • X ∗ ist regulärer Ausdruck mit Sprache {x1 ◦· · ·◦xn : n ∈ N∧x1 , . . . , xn ∈ LX }. (wobei LX die formale Sprache zum regulären Ausdruck X und LY die formale Sprache zum regulären Ausdruck Y sei). X + = XX ∗ Gesetze der regulären Ausdrücke: • [[X|Y ]|Z] = [X|[Y |Z]] • [[XY ]Z] = [X[Y Z]] • [X|Y ] = [Y |X] • [[X|Y ]Z] = [[XZ]|[Y Z]] • [X[Y |Z]] = [[XY ]|[XZ]] • {}∗ = ε • Xε = X • X{} = {} • X ∗ = XX ∗ |ε • X ∗ = [X|ε]∗ 1.3.2 Endliche Automaten Endlicher Automat A = (S, T, s0 , SZ , δ): S endliche Menge von Zuständen T endliche Menge von Eingangszeichen s0 ∈ S Anfangszustand SZ ⊆ S Menge der Endzustände δ : S × (T ∪ {ε}) → 2S (= ℘(S) [Potenzmenge von S]) δ(s, t) Menge der Nachfolgezustände zu s ∈ S, t ∈ T ∪ {ε} Deterministischer Automat: δ(s, ε) ⊆ {s} |δ(s, t)| ≤ 1 ∀s ∈ S, t ∈ T Partieller Automat: ∃t ∈ T, s ∈ S : δ(s, t) = ∅ ε – freier Automat: δ(s, ε) = ∅ ∀s ∈ S 260 KAPITEL 1. FORMALE SPRACHEN a a s1 s0 s2 b Abbildung 1.5: endlicher Automat Beispiel: A = ({s0 , s1 , s2 } , {a, b} , s0 , {s1 } , δ) (s. Abb. 1.5) δ ε a b s0 ∅ {s1 } ∅ s1 ∅ {s2 } ∅ ∅ {s1 } s2 ∅ a Notation: Wir schreiben s1 → s2 , falls s2 ∈ δ(s1 , a). w∗ Jedem Wort w ∈ T ∗ ordnen wir eine Relation → auf Zuständen zu: ∗ ε ∗ ε → = → hai ∗ ε ∗ a ε ∗ → =→ ◦ → ◦ → w1 ◦w2 ∗ → w ∗ w ∗ =→1 ◦ →2 w1 , w2 ∈ T ∗ w∗ Wir schreiben δ ∗ (s, w) für {s0 : s → s0 }. Damit können wir die Sprache charakterisieren, die durch einen Automaten A beschrieben wird: n o w∗ L(A) = w ∈ T ∗ : ∃z ∈ SZ : s0 → z 1.3.3 Äquivalenz der Darstellungsformen Zunächst beantworten wir die Frage, ob Automaten mit spontanen Übergängen beschreibungsmächtiger sind als Automaten ohne spontane Übergänge. Satz: Zu jedem endlichen Automaten existiert ein ε – freier endlicher Automat, der die gleiche Sprache akzeptiert. Beweisidee: Wir konstruieren zu einem gegebenen endlichen Automaten einen ε – freien Automaten mit gleicher Sprache: ε ∗ ε ∗ (1) → – Zyklen: Wir fassen Knoten, die durch → – Zyklen verbunden sind, zu ε ∗ einem Knoten zusammen. Es entsteht ein Automat ohne → – Zyklen (ε – Schlingen können ohnehin weggelassen werden). ε ∗ (2) → – Wege (zyklenfrei): Durchschalten von ε – Übergängen (vgl. Abb. 1.6). Satz: Zu jedem ε – freien nichtdeterministischen Automaten existiert ein ε – freier deterministischer Automat mit gleichem Sprachschatz. Beweisidee: Wir konstruieren zu dem Automaten mit Zustandsmenge S einen Automaten mit Zustandsmenge 2S (vgl. Abb. 1.7). 1.3.4 Äquivalenz von regulären Ausdrücken, endlichen Automaten und Chomsky – 3 – Grammatiken Satz: Zu jedem endlichen Automaten existiert ein regulärer Ausdruck, der die gleiche Sprache beschreibt. Beweis: (konstruktiv) 1.3. CHOMSKY – 3 – SPRACHEN, ENDLICHE AUTOMATEN, . . . 261 a a a Abbildung 1.6: ε – Übergänge durchschalten a s1 s0 a s2 a s0 {s 1, s2} Abbildung 1.7: Übergang von nichtdeterministischen zu deterministischen Automaten Wir geben zu einem gegebenen endlichen Automaten einen regulären Ausdruck mit gleichem Sprachschatz an. Zustandsmenge: S = {s1 , . . . , sn } O.B.d.A. sei der endliche Automat ε – frei. Wir konstruieren eine Familie formaler Sprachen L(i, j, k) ⊆ T ∗ , wobei i, j ∈ {1, . . . , n}, k ∈ {0, . . . , n}, und die dazugehörigen regulären Ausdrücke E(i, j, k), die jeweils die formale Sprache L(i, j, k) beschreiben. Dabei sei w L(i, j, k) = w ∈ T ∗ : si → sj k w In → betrachten wir dabei nur Pfade in Übergangsgraphen mit Zwischenzuständen k sl mit l ≤ k. L(i, j, 0) = {hai : sj ∈ δ(si , a)} a E(i, j, 0) = a1 | . . . |aq (Menge aller a ∈ T mit si → sj ) L(i, j, k + 1) = L(i, j, k) ∪ {u ◦ v ◦ w : u ∈ L(i, k + 1, k)∧ v ∈ L(k + 1, k + 1, k)∗ ∧ w ∈ L(k + 1, j, k)} E(i, j, k + 1) = E(i, j, k)|E(i, k + 1, k)E(k + 1, k + 1, k)∗ E(k + 1, j, k) Die Sprache der endlichen Automaten sind die Wege vom Anfangszustand s1 zu den Endzuständen SZ = {sZ1 , . . . , sZp }. Dies entspricht den regulären Ausdrücken E(1, Z1 , n)| . . . |E(1, Zp , n). Satz: Zu jedem regulären Ausdruck lässt sich ein endlicher Automat angeben, der die gleiche Sprache beschreibt. Beweis: Wir geben die Konstruktionsprinzipien an. 262 KAPITEL 1. FORMALE SPRACHEN x (a) (b) (c) Abbildung 1.8: Endliche Automaten zu elementaren regulären Ausdrücken X Y Abbildung 1.9: (1) Für elementare reguläre Ausdrücke: Sei x ∈ T , dann ist x regulärer Ausdruck mit einer Sprache, die durch den endlichen Automaten aus Abb. (1.8(a)) beschrieben wird. ε ist regulärer Ausdruck mit endlichem Automaten (1.8(b)). (1.8(c)) ist Automat für die leere Sprache. (2) Seien X und Y reguläre Ausdrücke, die durch die endlichen Automaten aus Abb. (1.9) beschrieben werden. Für [X|Y ] akzeptiert (1.10(a)) die gleiche Sprache. Für XY akzeptiert der endliche Automat (1.10(b)) die gleiche Sprache. Automat zu X ∗ ist (1.10(c)). Die Überlegungen zeigen: Zu jedem regulären Ausdruck können wir einen endlichen Automaten angeben, der die gleiche formale Sprache beschreibt — vorausgesetzt, wir finden bei zusammengesetzten regulären Ausdrücken endliche Automaten für die Unterausdrücke. Vollständige Induktion über die Anzahl der Symbole in einem regulären Ausdruck liefert die Behauptung. Satz: Zu jedem deterministischen ε – freien endlichen Automaten können wir eine Chomsky – 3 – Grammatik angeben, die die gleiche Sprache beschreibt. Beweis: Gegeben sei der endliche Automat A = (S, T, s0 , SZ , δ). OBdA gehe keine Kante nach s0 . Jeder Kante (d.h. jedem Übergang δ(s, a) = s0 ) ordnen wir eine Regel der Grammatik zu. Wir verwenden die Zustände als Nichtterminalzeichen. Für Übergänge vom Anfangszustand s0 aus: δ(s0 , a) = si verwenden wir die Regel a → si Für Übergänge δ(si , a) = sj führen wir die Regel si a → sj ein. Zu den Zuständen als Nichtterminalzeichen nehmen wir noch ein Sonderzeichen, die Wurzel Z (oBdA sei Z ∈ / S) hinzu. Für jeden Zustand si ∈ SZ nehmen wir die Regel si → Z hinzu. Die entstehende Grammatik akzeptiert die gleiche Sprache. 1.3. CHOMSKY – 3 – SPRACHEN, ENDLICHE AUTOMATEN, . . . X Y 263 (a) X Y (b) X (c) Abbildung 1.10: Endliche Automaten zu zusammengesetzten regulären Ausdrücken 264 KAPITEL 1. FORMALE SPRACHEN Satz: Zu jeder Chomsky – 3 – Grammatik existiert ein endlicher Automat, der die gleiche Sprache akzeptiert. Beweis: OBdA sei die Chomsky – 3 – Grammatik linkslinear und alle linkslinearen Regeln von der Form sa → s0 mit a ∈ T . Wir geben einen endlichen Automaten an: Zustände S = N ∪ {s0 } Eingabezeichen T Anfangszustand s0 Endzustand {Z} Übergangsfunktion ∀s ∈ N, a ∈ T : δ(s, a) = {s0 ∈ S : sa → s0 ist Regel der Grammatik} ∀s ∈ N : δ(s, ε) = {s0 ∈ S : s → s0 ist Regel der Grammatik} ∀a ∈ T : δ(s0 , a) = {s0 ∈ S : a → s0 ist Regel der Grammatik} Es entsteht ein Automat, der die gleiche Sprache wie die Grammatik beschreibt: Jede Ableitung in der Grammatik entspricht einer Folge von Zustandsübergängen im Automaten und umgekehrt. Fazit • Die Beschreibungsmittel – endliche Automaten – reguläre Ausdrücke – Chomsky – 3 – Grammatiken sind gleichmächtig (beschreiben die gleiche Klasse formaler Sprachen). • Die Übergänge sind konstruktiv: Zu jedem endlichen Automaten (regulären Ausdruck, Chomsky – 3 – Grammatik) können wir systematisch (durch einen Algorithmus) einen regulären Ausdruck (Chomksy – 3 – Grammatik, endlichen Automaten) angeben, der die gleiche formale Sprache beschreibt. Solche Sprache heißen regulär. Wir wenden uns nun der Frage zu, wie wir für eine vorgegebene formale Sprache erkennen können, dass sie regulär ist. Wir beginnen mit einem einfachen Beispiel für eine nichtreguläre Sprache: Lab = {an bn : n ∈ N \ {0}} Diese Sprache können wir ohne Probleme durch eine Chomksy – 2 – Grammatik beschreiben: T = {a, b}, N = {Z}, Z Wurzel ab → Z aZb → Z Behauptung: Die Sprache Lab ist nicht regulär. Beweis: Wir zeigen, dass kein endlicher Automat existiert, der Lab akzeptiert. (1) Der endliche Automat hat nur endlich viele Zustände (sei h die Anzahl der Zustände). 1.3. CHOMSKY – 3 – SPRACHEN, ENDLICHE AUTOMATEN, . . . 265 (2) an bn und am bm sind im Sprachschatz, d.h. es existieren Zustände sn , sm und sz , so dass gilt: an ∗ bn ∗ s0 → sn ∧ sn → sz am ∗ bm ∗ s0 → sm ∧ sm → sz Falls sn = sm gilt, werden auch Wörter an bm bzw. am bn akzeptiert. Da nur endlich viele Zustände (genaugenommen k) existieren, muss es Zahlen n und m geben mit n 6= m und sn = sm . Widerspruch. Problem bei endlichen Automaten: Endliche Automaten können nur eine endliche Information speichern, können nicht unbeschränkt zählen. Sprachen mit Klammerstrukturen (beliebiges Nesten von Öffnenden und schließenden Klammern) sind nicht regulär. Satz (Pumping – Lemma für reguläre Sprachen): Zu jeder regulären Sprache L existiert eine Zahl n ∈ N, so dass für alle Wörter w ∈ L mit |w| ≥ n gilt: w lässt sich in Wörter x, y, z ∈ T ∗ zerlegen: w = x ◦ y ◦ z mit |y| ≥ 1, |x ◦ y| ≤ n und x ◦ y i ◦ z ∈ L für alle i ∈ N \ {0}. Beweis: L ist nach Voraussetzung regulär. Es existiert ein deterministischer endlicher ε – freier Automat, der L akzeptiert. Sei n die Anzahl seiner Zustände und w∗ w ∈ L. Es gilt s0 → sz . Dabei durchläuft der Automat |w| + 1 Zustände. Gilt |w| ≥ n, so tritt ein Zustand mindestens zwei Mal auf, d.h. x∗ y ∗ x∗ yi z ∗ s0 → sh ∧ sh → sh ∧ sh → sz Dann gilt auch ∗ z ∗ s0 → sh ∧ sh → sh ∧ sh → sz Damit lässt sich auch zeigen: Lab ist nicht regulär. 1.3.5 Minimale Automaten Wir zeigen nun, wie wir für eine reguläre Sprache einen minimalen Automaten (d.h. einen Automaten mit einer minimalen Anzahl von Zuständen) angeben können. Sei also L ⊆ T ∗ . Wir definieren eine Äquivalenzrelation ∼L durch (seien v, w ∈ L): v ∼L w ⇔def ∀u ∈ T ∗ : v ◦ u ∈ L ⇔ w ◦ u ∈ L Dadurch erhalten wir Äquivalenzklassen [v]L = {w ∈ T ∗ : w ∼L v} Satz (Myhill – Nerode): L ist genau dann regulär, wenn die Menge der Äquivalenzklassen endlich ist. Beweisidee: Jede Äquivalenzklasse definiert einen endlichen Automaten, der die Sprache akzeptiert. Umgekehrt: Jeder Zustand in einem endlichen Automaten entspricht einer Äquivalenzklasse. Die Beweisidee liefert bereits einen Hinweis, wie wir einen ε – freien deterministischen endlichen Automaten mit totaler Übergangsfunktion konstruieren können, der die Sprache L akzeptiert und eine minimale Anzahl von Zuständen hat. Wir zeigen nun, wie wir aus einem gegebenen endlichen ε – freien deterministischen Automaten einen minimalen Automaten konstruieren, indem wir bestimmte Zustände zusammenlegen. • 1. Schritt: Wir entfernen alle Zustände, die vom Anfangszustand aus nicht erreichbar sind. 266 KAPITEL 1. FORMALE SPRACHEN • 2. Schritt: Wir konstruieren eine Folge von Prädikaten auf Zuständen: pi : S 0 × S 0 → B: w∗ w∗ pi (s1 , s2 ) = ∀w ∈ T ∗ : |w| ≤ i ⇒ [(∃z ∈ SZ : s1 z) ⇔ (∃z ∈ SZ : s2 z)] pi lässt sich induktiv definieren: p0 (s1 , s2 ) = (s1 ∈ SZ ⇔ s2 ∈ SZ ) pi+1 (s1 , s2 ) = pi (s1 , s2 ) ∧ ∀x ∈ T : pi (δ(s1 , x), δ(s2 , x)) Da der Automat endlich ist, existiert ein k mit pk = pk+1 . Das Prädikat pk definiert uns dann, welche Zustände des Automaten äquivalent sind und zu einem Zustand im minimalen Automaten verschmolzen werden können. Anwedung von regulären Ausdrücken: z.B. in grep, sed, vi, vim, emacs, lex, flex 1.4 Kontextfreie Sprachen und Kellerautomaten Chomsky – 3 – Grammatiken genügen im Allgemeinen nicht, um komplexe formale Sprachen wie Programmiersprachen zu beschreiben (s. z.B. Klammerstrukturen wie in Lab = {an bn : n ∈ N\{0}}). Es gilt REG ( CF L, d.h. die Klasse der kontextfreien Sprachen ist mächtiger als die der regulären Sprachen. Chomsky – 3 – Grammatiken, reguläre Ausdrücke und endliche Automaten eignen sich nicht, um kontextfreie Sprachen zu beschreiben. Analog zu regulären Ausdrücken, endlichen Automaten und Chomsky – 3 – Grammatiken bei regulären Sprachen sind in der Klasse der kontextfreien Sprachen die folgenden Beschreibungsformen gleichmächtig: • Chomsky – 2 – Grammatiken • BNF – Ausdrücke • Kellerautomaten Diese Mittel werden bei der Spezifikation und der Syntaxanalyse von Programmiersprachen in Übersetzern und Interpretern eingesetzt. 1.4.1 BNF – Notation BNF ist eine Erweiterung der regulären Ausdrücke um: (1) Hilfszeichen (Nichtterminale) (2) (rekursive) Gleichungen für Hilfszeichen Sei T eine Menge von Terminalzeichen und N eine Menge von Nichtterminalzeichen. Eine BNF – Beschreibung einer Sprache L hat die Form x1 ::= E1 .. . xn ::= En mit x1 , . . . , xn ∈ N und E1 , . . . , En reguläre Ausdrücke über T ∪ N . 1.4. KONTEXTFREIE SPRACHEN UND KELLERAUTOMATEN 267 Beispiel: hZi ::= ab|a hZi b beschreibt die kontextfreie Sprache {an bn : n ∈ N \ {0}}. Für eine BNF – Beschreibung kann für jedes xi (1 ≤ i ≤ n) eine Chomsky – 2 – Grammatik angegeben werden. Die regulären Ausdrücke Ei werden in Chomsky – 3 – Grammatiken (mit neuem Nichtterminalzeichen Zi als Axiom) umgeformt und für die BNF – Regel xi ::= Ei wird die Regel Zi → xi in die Grammatik aufgenommen. Anmerkung: Es gibt zahlreiche Stile und Erweiterungen von BNF. Die Klammern [. . .] dienen oft der Kennzeichnung optionaler Anteile, z.B. x[4] für x|x4. {x}m n bezeichnet die n- bis m-malige Wiederholung von x. 1.4.2 Kellerautomaten Ein Kellerautomat ist ein 7-Tupel KA = (S, T, K, δ, s0 , k0 , SZ ) mit: • einer endlichen Menge von Zuständen S • einer endlichen Menge von Eingabezeichen T • einer endlichen Menge von Kellerzeichen K • einer endlichen Übergangsrelation δ : S × (T ∪ {ε}) × K → 2S×K ∗ • einem Anfangszustand s0 ∈ S • einem Kellerstartsymbol k0 ∈ K • und Endzuständen SZ ⊆ S Prinzip: Ein Kellerautomat verarbeitet ein Wort w ∈ T ∗ , indem er w von links nach rechts liest und in jedem Schritt, abhängig vom aktuellen Zustand s, vom Eingangszeichen a und vom obersten Kellerzeichen k entsprechend der Übergangsrelation δ • in einen neuen Zustand s0 ∈ S übergeht, • das oberste Zeichen k vom Keller entfernt und • eine (ggf. leere) Sequenz u ∈ K ∗ auf den Keller legt. Der Kellerautomat akzeptiert w ∈ T ∗ , wenn die vollständige Verarbeitung von w von s0 aus zu einem Endzustand s ∈ SZ führt. Darstellung: als Graphen, analog zu endlichen Automaten: Knoten s ∈ S, Kanten (a, k, u) ∈ (T ∪ {ε}) × K × K ∗ . Eine Kante (a, k, u) von s nach s0 existiert genau dann, wenn (s0 , u) ∈ δ(s, a, k) ist. Beispiel: Kellerautomat für {(n )n : n ∈ N \ {0}}: KA = ({s0 , s1 , s2 } , {(, )} , {|, 0} , δ, s0 , 0, {s2 }) δ(s0 , (, 0) = {(s0 , h|0i)} δ(s0 , (, 1) = {(s0 , h||i)} δ(s0 , ), 1) = {(s1 , ε)} δ(s1 , ), 1) = {(s1 , ε)} δ(s1 , ε, 0) = {(s2 , ε)} 268 KAPITEL 1. FORMALE SPRACHEN Arbeitsweise formal: Wir betrachten Kellerkonfigurationen (s, w, v) ∈ S × T ∗ × K ∗ mit Kontrollzustand s, (Rest-)Eingabewort w und Kellerwort v. Ein Kellerautomat induziert eine Übergangsrelation → auf Konfigurationen vermöge (s1 , w, hki ◦ v) → (s2 , w, u ◦ v) falls (s2 , u) ∈ δ(s1 , ε, k) (s1 , hai ◦ w, hki circv) → (s2 , w, u ◦ v) falls (s2 , u) ∈ δ(s1 , a, k) und Ein Wort w ∈ T ∗ wird vom Kellerautomaten akzeptiert, wenn es ein s ∈ SZ und ein v ∈ K ∗ gibt, so dass ∗ (s0 , w, hk0 i) → (s, ε, v) gilt. Die vom Kellerautomaten akzeptierte Sprache bezeichnen wir mit L(KA). Anmerkung: Akzeptieren durch leeren Keller ist gleichmächtig, d.h. w ∈ T ∗ ∗ wird vom Kellerautomaten genau dann akzeptiert, wenn (s0 , w, ε) → (s, ε, ε) mit s ∈ S gilt. Durch Kellerautomaten erhalten wir für beliebige kontextfreie Sprachen unmittelbar einen (in der Regel sehr ineffizienten) Erkennungsalgorithmus. Im Gegensatz zu endlichen Automaten sind deterministische Kellerautomaten nicht äquivalent zu nichtdeterministischen Kellerautomaten. 1.4.3 Äquivalenz von Kellerautomaten und kontextfreien Sprachen Satz: Ist L eine kontextfreie Sprache, dann gibt es einen Kellerautomaten K, so dass L(K) = L ist. Idee: Gegeben sei eine kontextfreie Grammatik G = (T, N, →, Z). Gesucht ist ein Kellerautomat K mit L(K) = L(G). K = (S, T, T ∪ N ∪ {#}, δ, ε, #, {se }) Dabei setzt sich S aus ε, se , sv und der Menge aller (Teil-)Sequenzen der linken Seiten der Regeln zusammen. Sei he1 . . . en i, ei ∈ N ∪ T , linke Seite einer Regel he1 . . . en i → A. Wir konstruieren die Übergangsrelation δ: (1) Kellern: (ε, haki) ∈ δ(ε, a, k) a ∈ T (2) Regelerkennung: (hei−1 . . . ej i , ε) ∈ δ(hei . . . ej i , ε, ei−1 ) (hei . . . ej+1 i , hki) ∈ δ(hei . . . ej i , ej+1 , k) (3) Regelanwendung: (ε, hAki) ∈ δ(he1 . . . en i , ε, k) (4) Akzeptanz: (sv , ε) ∈ δ(ε, ε, Z) (se , ε) ∈ δ(sv , ε, #) K ist nicht deterministisch und ineffizient! K kellert die Eingabe, bis er an einer beliebigen Stelle mit dem Aufbau der linken Seite einer Regel beginnt. 1.4. KONTEXTFREIE SPRACHEN UND KELLERAUTOMATEN 269 Beispiel: Chomsky – 2 – Grammatik G = ({a, b}, {Z}, {aZb → Z, ZZ → Z, ab → Z}) S = {ε, sv , se , hai , hbi , hZi , haZi , hZbi , hZZi , haZbi , habi} δ(ε, x, k) = {(hxi , hki), (ε, hxki)} k 6= # ⇒δ(ε, ε, k) = {(hki , ε)} δ(hZi , b, k) = {(hZbi , hki), (hZZi , hki)} δ(ε, ε, Z) = {(sv , ε)} Beobachtung: K ist nichtdeterministisch und hat Sackgassen (z.B. beim Ableiten von haabbi), d.h. er ist sehr ineffizient. Satz: Zu jedem Kellerautomaten K (mit ε ∈ / L(K)) existiert eine kontextfreie Grammatik G mit L(K) = L(G). Beweis: (Literatur) Vorgehensweisen bei der Reduktion von Wörtern kontextfreier Sprachen durch Kellerautomaten: (1) Top-Down: Der Keller beinhaltet zu Beginn das Axiom. Ableitung eines (Teil)Worts v ∈ T ∗ im Keller und Vergleich mit der Resteingabe. Bei Übereinstimmung Kürzung des Eingabeworts und Entfernen von v aus dem Keller. (2) Bottom-Up: Der Keller ist am Anfang leer. Schrittweises lesen / kellern der Eingabe, Reduktion von Teilwörtern auf dem Keller. Erfolg, wenn das Axiom im Keller und die Eingabe leer ist (oben angewendet). Beide Vorgehensweisen sind i.a. nichtdeterministisch und wegen der Suche im Ableitungsbaum ineffizient. Verbesserungsansatz: Eliminieren des Nichtdeterminismus durch Beschränkung der Regelauswahl. Der Erfolg ist dabei abhängig von der kontextfreien Sprache. 1.4.4 Greibach – Normalform Eine kontextfreie Grammatik G = (T, N, →, Z) ist in Greibach – Normalform, wenn jede Ersetzungsregel folgende Gestalt hat: hai ◦ w → x mit a ∈ T , w ∈ N ∗ und x ∈ N , d.h. Verarbeitung genau eines a ∈ T links bei jeder Ersetzung. Satz: Jeder von einer ε – freien, kontextfreien Grammatik erzeugte Sprachschatz kann auch durch eine kontextfreie Grammatik in Greibach – Normalform erzeugt werden. Beweis: (Literatur) Satz: Zu jeder kontextfreien Grammatik G existiert ein Kellerautomat K mit L(G) = L(K). Beweis: G = (T, N, →, Z) sei o.B.d.A. in Greibach – Normalform. K = ({s0 } , T, N, δ, s0 , Z, {s0 }) ∀k ∈ N : δ(s0 , a, k) = {(s0 , w) : hai ◦ w → k} Es gilt für u ∈ T ∗ und x ∈ N ∗ : ∗ ∗ u x ⇔ (s0 , u, x) → (s0 , ε, ε) Es genügt also eine einelementige Zustandsmenge. 270 KAPITEL 1. FORMALE SPRACHEN Beispiel: kontextfreie Grammatik G = ({a, b} , {Z, U } , {aU → Z, aZU → Z, b → U } , Z) K = ({s0 } , {a, b} , {Z, U } , δ, s0 , Z, {z0 }) n o δ(s0 , a, Z) = (s0 , hU i)[1a] , (s0 , hZU i)[1b] n o δ(s0 , b, U ) = (s0 , ε)[2] Reduktion von haabbi: [1a] (s0 , haabbi , hZi) → (s0 , habbi , hU i) Sackgasse [1b] [1a] [2] (s0 , haabbi , hZi) → (s0 , habbi , hZU i) → (s0 , hbbi , hZU i) → [2] (s0 , hbi , hU i) → (s0 , ε, ε) Dieser Kellerautomat ist also bereits stark vereinfacht, aber nach wie vor nichtdeterministisch. Benötigt aus praktischer Sicht: eingeschränkte kontextfreie Sprachen, die beschränkten Nichtdeterminismus verursachen und Sackgassen eliminieren. 1.4.5 LR(k) – Sprachen (left – rightmost: Eingabe von links nach rechts, Rechtsableitungen, k Zeichen Vorschau) Idee: Bottom-Up: Eingabewort von links nach rechts lesen und kellern, bis eine Regel anwendbar ist. Bestimmung der anzuwendenden Regel anhand von höchstens k Zeichen des Restworts (bzw. rechts der Anwendungsstelle); Rechtskontext. In einer LR(k) – Sprache kann bei jeder akzeptierenden Linksreduktion durch Betrachtung der k nächsten Zeichen rechts der Anwendungsstelle die anzuwendende Regel eindeutig bestimmt werden. Bei bestimmten Grammatiken kann die Auswahl einer Regel der Form he1 . . . en i → A für w = x ◦ he1 . . . en i ◦ y durch Betrachtung eines endlichen Präfixes von y gesteuert werden. Kontextbedingung ist die Menge von Wörtern, die die Präfixe von y festlegt, welche die Regelanwendung zulassen. Kontextfreie Grammatiken heißen LR – deterministisch, wenn es für alle Regeln endliche Kontextbedingungen gibt, so dass der von links nach rechts arbeitende Kellerautomat für alle w ∈ L(G) und alle akzeptierenden Reduktionen in jedem Schritt genau eine Regel findet, die Reduktion vollzieht und Sackgassen vermeidet. Satz: Jede LR – deterministische, kontextfreie Grammatik ist eindeutig. Beweis: Gemäß der Definition von LR – deterministischen, kontextfreien Grammatiken gibt es eine eindeutige Einschränkung des Kellerautomaten, der die Ableitung erzeugt. Definition: LR(k): w[i : k] bezeichne das Teilwort hwi . . . wk i von w = hw1 . . . wn i (i, k, n ∈ N, 1 ≤ i ≤ k ≤ n). Eine azyklische kontextfreie Grammatik (T, N, →, Z) heißt LR(k) – Grammatik, wenn für jedes Paar von Ableitungen in Linksnormalform u1 ◦ a1 ◦ x1 u1 ◦ hB1 i ◦ x1 . . . Z u2 ◦ a2 ◦ x2 u2 ◦ hB2 i ◦ x2 . . . Z 1.4. KONTEXTFREIE SPRACHEN UND KELLERAUTOMATEN 271 mit {a1 → B1 , a2 → B2 } ⊆→ gilt: u1 ◦ a1 ◦ x1 [1 : k] = u1 ◦ a2 ◦ x2 [1 : k] ⇒ a1 = a2 ∧ B1 = B2 ∧ u1 = u2 d.h. durch x1 [1 : k] ist die anzuwendende Regel eindeutig festgelegt. Beispiel: (LR(0)) G = ({a, b}, {Z, X}, {ZX → Z, X → Z, aZb → X, ab → X}, Z) Ableitung von haabbabi: haabbabi → haXbabi → haZbabi → hXabi → hZabi → hZXi → hZi Kellerautomat: Wort kellern, bis Regel anwendbar; dann Regeln anwenden, solange möglich. Keller Restwort ε aabbab abbab a aa bbab aab bab aX bab aZ bab aZb ab ab X Z ab Za b Zab ε ZX ε Z ε Der Keller beinhaltet also immer das Präfix des zu reduzierenden Wortes. Bei LR(0) ist kein Kontext notwendig, um die richtigen Regeln zu wählen. Satz: Jede LR(k) – Grammatik ist eindeutig. Beweis: LR(k) – Grammatiken sind LR – deterministisch. Beispiel: (LR(1)) G = ({Z, A, P, E}, {(, ), +, −, ∗, /, a}, →, Z) Regel A→Z E→A A+E →A A−E →A P →E E∗P →E E/P → E (A) → P a→P Kontextbedingung ε +, −, ), ε +, −, ), ε +, −, ), ε t∈T t∈T t∈T t∈T t∈T ha + a ∗ ai . . . hA + E ∗ ai hA + E ∗ P i . . . Z LR(k) – Grammatiken erlauben die Reduktion von Wörtern durch Kellerautomaten mit akzeptablem Aufwand. Insbesondere LR(1) – Grammatiken werden in der Praxis eingesetzt. Anmerkung: Es gibt keinen Algorithmus, der für eine beliebige Grammatik G entscheidet, ob sie LR(k) ist. 272 1.4.6 KAPITEL 1. FORMALE SPRACHEN LL(k) – Grammatiken (left leftmost) Gegensatz zu LR(k): Top-Down – Produktion der Ableitungen, startend vom Axiom Z, dadurch Linksableitung (bzw. Rechtsreduktion) statt Linksreduktion. Idee: (Kellerautomat) (1) Produktion von v ∈ T ∗ ausgehend von Nichtterminalen auf dem Keller und abhängig vom Rechtskontext. (2) Vergleich des erzeugten v mit Präfix des (Rest-)Eingabeworts. (3) Bei Übereinstimmung: Entfernen von v aus dem Keller und dem Eingabewort. Definition: LL(k): Eine azyklische, kontextfreie Grammatik G = (T, N, →, Z) heißt LL(k) – Grammatik (mit k ∈ N), wenn für alle Paare von Ableitungen in Rechtsnormalform (a1 , a2 , v, u1 , u2 ∈ (T ∪ N )∗ , w ∈ T ∗ , B ∈ N ) ∗ ∗ ∗ ∗ ∗ ∗ v ◦ u1 v ◦ a1 ◦ w v ◦ hBi ◦ w Z v ◦ u2 v ◦ a2 ◦ w v ◦ hBi ◦ w Z mit {a1 → B, a2 → B} ⊆→ gilt: u1 [1 : k] = u2 [1 : k] ⇒ a1 = a2 d.h. die Zerteilung ist mit u1 [1 : k] eindeutig festgelegt. Beispiel: LL(1) – Grammatik G = ({z}, {a, +, −}, {a → z, −z → z, +zz → z}, z) Reduktion von h+-a+aai durch Kellerautomaten: LL – Technik (top-down) LR – Technik (bottom-up) Keller Resteingabe Keller Resteingabe Z +-a+aa ε +-a+aa ZZ+ +-a+aa + -a+aa ZZ -a+aa +a+aa ZZ-a+aa +-a +aa ZZ a+aa +-Z +aa .. .. .. .. . . . . ε ε Z ε Jede LL(k) – Grammatik ist auch eine LR(k) – Grammatik, d.h. u.a. jede LL(k) – Grammatik ist eindeutig. 1.4.7 Rekursiver Abstieg Der rekursive Abstieg ist ein klassisches Verfahren der Programmierung eines TopDown – Kellerautomaten zur Zerteilung von Wörtern einer kontextfreien Sprache, orientiert an BNF: • Für jedes Nichtterminal xi ∈ N der linken Seiten der Regeln xi ::= E1 | . . . |En wird eine Prozedur implementiert, die (rekursiv) eine vollständige Falluntersuchung der rechten Seite der zu xi gehörenden Regel durchführt. In der Regel erfolgt ebenfalls Steuerung über das Präfix des Restworts. 1.4. KONTEXTFREIE SPRACHEN UND KELLERAUTOMATEN 273 N0 Ni Nj u v w x y Abbildung 1.11: Zerlegung von z 1.4.8 Das Pumping – Lemma für kontextfreie Sprachen Eine kontextfreie Grammatik G = (T, N, →, Z) ist in CNF, falls alle Regeln folgende Gestalt haben: a → A oder BC → A für a ∈ T und A, B, C ∈ N . Satz: Zu jeder kontextfreien Grammatik G mit ε ∈ / L(G) gibt es eine äquivalente kontextfreie Grammatik in CNF. (ohne Beweis) Satz (Pumping – Lemma für kontextfreie Sprachen): Zu jeder kontextfreien Sprache L existiert ein n ∈ N, so dass sich alle z ∈ L mit |z| ≥ n in u, v, w, x, y ∈ T ∗ zerlegen lassen: z =u◦v◦w◦x◦y mit (1) |v ◦ x| ≥ 1 (2) |v ◦ w ◦ x| ≤ n (3) ∀i ≥ 0 : u ◦ v i ◦ w ◦ xi ◦ y ∈ L Beweis: Sei G = (T, N, →, N0 ) eine CNF – Grammatik mit L(G) = L. Weiter seien k = |N |, n = 2k und z ∈ L mit |z| ≥ n. Der Ableitungsbaum T für z ist ein Binärbaum der Höhe h + 1 mit |z| ≥ n Blättern: • Knoten mit genau einem Sohn (a → A) oder genau zwei Söhnen (BC → A) • h ≥ ld |z| ≥ ld n = k Im Pfad p0 , . . . , ph in T von der Wurzel p0 bis zum Blatt ph ist die Markierung N0 , . . . , Nh der Knoten p0 , . . . , ph eine Folge von h + 1 ≥ k + 1 Nichtterminalen. Es gibt also i, j ∈ {0, . . . , h}, so dass Ni = Nj =: N , i < j und Ni+1 , . . . , Nh paarweise verschieden sind (vgl. Abb. 1.11). (1) Da G in CNF ist, ist v 6= ε oder x 6= ε, d.h. |v ◦ x| ≥ 1. ∗ (2) Der Teilbaum T 0 ab Knoten pi ist Ableitungsbaum für Ni v ◦ w ◦ x. Da Ni+1 , . . . , Nh paarweise verschieden sind, folgt: T 0 hat die Höhe h − i ≤ k, d.h. |v ◦ w ◦ x| ≤ 2h−i ≤ 2k = n (3) Die Ableitungen für u ◦ v i ◦ w ◦ xi ◦ y ergeben sich aus den Kombinationen der Möglichkeiten: ∗ N0 u ◦ N ◦ y, ∗ N0 w, ∗ N v◦N ◦x 274 1.5 KAPITEL 1. FORMALE SPRACHEN Kontextsensitive Grammatiken Kontextsensitive Grammatiken sind definitionsgemäß wortlängenmonoton, d.h. für ∗ jeden Schritt x y gilt |x| ≥ |y|. Kontextsensitive Grammatiken sind sehr allgemein; jede wortlängenmonotone Grammatik ist strukturäquivalent zu einer kontextsensitiven Grammatik. Es ist möglich, für eine kontextsensitive Sprache L einen (ineffizienten) Algorithmus anzugeben, der für Wörter w ∈ T ∗ entscheidet, ob w ∈ L ist. Begründung: Bei einer wortlängenmonotonen Grammatik ist für jedes w ∈ T ∗ die Menge der durch Reduktionen von w entstehenden Wörter endlich; mögliche unendliche Reduktionen können aufgrund der Wortlängenmonotonheit erkannt werden. Für eine Chomsky – 0 – Grammatik G0 existiert im Allgemeinen kein solcher Algorithmus. Für G0 existiert lediglich ein Algorithmus, der für jedes w ∈ L(G0 ) mit Resultat true terminiert. Für w ∈ / L(G0 ) kann die Terminierung nicht garantiert werden. Kapitel 2 Berechenbarkeit Es gibt mathematisch exakt beschreibbare Probleme, für die es keine Lösungsalgorithmen gibt. Typisches Problem: Aufgabe: Berechnung einer gegebenen Funktion f Lösung: Algorithmus, der f berechnet. Definition: Berechenbarkeit Eine Funktion f heißt berechenbar, wenn es einen Algorithmus gibt, der für jedes Argument x (Eingabe) den Wert f (x) (Ausgabe) berechnet. Nicht berechenbare Funktionen können nicht durch Programme in einer Rechenanlage beschrieben werden. Wir betrachten o.B.d.A. n-stellige Funktionen f : Nn → N, wobei lediglich Repräsentationen von N zur Verfügung stehen (siehe Teil I). Wir verwenden die Zeichenmenge T und die injektive, umkehrbare Abbildung rep : N → T ∗ Demnach existiert auch abs : {t ∈ T ∗ : ∃n ∈ N : rep(n) = t} → N so dass abs ◦ rep = id = rep ◦ abs gilt. Statt abstrakter Funktionen f : Nn → N betrachten wir ein konkretes f : (T ∗ )n → T ∗ mit f (x1 , . . . , xn ) = abs f (rep(x1 ), . . . , rep(xn )) Konkrete Algorithmen arbeiten auf Repräsentationen (von N). Für einen realistischen Berechenbarkeitsbegriff müssen diese Repräsentationen handhabbar sein. Wir setzen deshalb u.a. voraus, dass T endlich ist. 2.1 Hypothetische Maschinen Wir suchen eine Möglichkeit zur Präzisierung des Begriffs Algorithmus: hypothetische Maschinen. Dies sind mathematische Nachbildungen des Zustandsraums und der Übergangsfunktion realer Maschinen mit dem Ziel der Präzisierung und der Vereinfachung. Endliche Automaten und Kellerautomaten genügen nicht; u.a. können Kellerautomaten für kontextfreie Sprachen, aber nicht für kontextsensitive Sprachen L berechnen, ob w ∈ L ist. Wir betrachten allgemeine Modelle als Kellerautomaten mit unbeschränkten Mengen von Zuständen. Mögliche Kritik: unrealistisch weil technisch nicht realisierbar etc. 275 276 KAPITEL 2. BERECHENBARKEIT 2.1.1 Turing – Maschinen (nach A. M. Turing, 1936) Eine Turing – Maschine besteht aus: • einem unendlichen Band von Zellen zur Speicherung von Zeichen, • einem Schreib- / Lesekopf und • einer Steuereinheit Ein endlicher Abschnitt des Bandes trägt relevante Information, alle anderen Zellen enthalten das Symbol # für die leere Information (o.B.d.A. sei # ∈ / T ). Prinzip: Die Turing – Maschine liest in jedem Schritt ein Zeichen t ∈ T ∪ {#} unter dem Kopf. Abhängig vom Zustand wird das Zeichen überschrieben, ein neuer Zustand eingenommen und der Kopf um höchstens eine Position nach links oder rechts bewegt. Eine Turing – Maschine T M = (T, S, δ, s0 ) umfasst: • eine endliche Menge T von Zeichen, mit denen das Band beschrieben wird (# ∈ / T) • eine endliche Menge S von Zuständen • eine endliche Übergangsrelation δ : S × (T ∪ {#}) → 2S×(T ∪{#})×{,,↓} und bewirken dabei eine Verschiebung des Kopfes um eine Zelle nach links bzw. rechts, ↓ das Beibehalten der Position. Gilt für eine Turing – Maschine ∀t ∈ T ∪ {#}, s ∈ S : |δ(s, t)| ≤ 1, so heißt sie deterministisch. Beispiel: Prüfung, ob die Anzahl der L in einem Wort hw1 . . . wn i mit n ≥ 1 und wi ∈ {0, L} gerade ist. Zu Beginn sei . . . #w1 . . . wn # . . . auf dem Band und der Kopf auf wn positioniert. Die Turing – Maschine hält mit Bandinhalt . . . #L# . . . an, falls die Anzahl der L gerade war und mit . . . #0# . . . sonst. T M = ({0, L}, {s0 , s1 , s2 , s3 }, δ, s0 ) Übergangsfunktion: δ 0 L # s0 (s0 , 0, ) (s0 , L, ) (s1 , #, ) s1 (s1 , #, ) (s2 , #, ) (s3 , L, ↓) s2 (s2 , #, ) (s1 , #, ) (s3 , 0, ↓) s3 ∅ ∅ ∅ Das Verhalten einer Turing – Maschine kann auch graphisch durch einen Automaten dargestellt werden — s. Abb. (2.1). Die Markierung 0 # für einen Zustandsübergang von s nach s0 bedeutet, dass dieser Übergang im Zustand s ausgeführt werden kann, falls 0 unter dem Lesekopf steht; dann wird # geschrieben und der Lesekopf bewegt sich nach links. Eine Konfiguration der Turing – Maschine beschreibt den Berechnungszustand. Wir verwenden ein 4 – Tupel (s, l, a, r) ∈ S × (T ∪ {#})∗ × (T ∪ {#}) × (T ∪ {#})∗ Da das Band nach links und nach rechts unendlich fortgesetzt ist, sind folgende Konfigurationen äquivalent: • (s, l, a, r) 2.1. HYPOTHETISCHE MASCHINEN 277 # # << s0 0 0 >> L L >> 0 # << s1 #Lv L # << L # << s3 s2 #0v 0 # << Abbildung 2.1: Graphische Darstellung einer Turing – Maschine • (s, h#i ◦ l, a, r) • (s, l, a, r ◦ h#i) Die Berechnung einer Turing – Maschine (ausgehend von einer Konfiguration, d.h. einem Anfangszustand, einer (endlichen) Anfangsbandbelegung und Stellung des Lesekopfs) besteht aus einer endlichen oder unendlichen Folge von Konfigurationen K0 → K1 → K2 → · · · → . . . Wir definieren eine Relation auf Konfigurationen (s, l, a, r) → (s0 , l0 , a0 , r0 ) wie folgt: Es gilt genau eine der folgenden Aussagen: (1) z =↓ ∧l = l0 ∧ r = r0 ∧ a0 = x (Kopf bleibt stehen) (2) z = ∧l = l0 ◦ ha0 i ∧ r0 = hxi ◦ r (Kopf nach links) (3) z = ∧l0 = l ◦ hxi ∧ r = ha0 i ◦ r0 (Kopf nach rechts) wobei (s0 , x, z) ∈ δ(s, a). Eine Konfiguration k heißt terminal, wenn keine Nachfolgekonfiguration existiert. Die Turing – Maschine bleibt stehen, die Berechnung endet. Eine Berechnung heißt vollständig, wenn sie unendlich ist oder endlich ist und die letzte Konfiguration terminal ist. Bemerkung: Jede Turing – Maschine lässt sich in einer der gebräuchlichen Programmiersprachen (C, Java, . . . ) simulieren. Wir stützen nun auf Turing – Maschinen den Begriff der Berechenbarkeit ab (genauer: Turing – Berechenbarkeit). Eine partielle Funktion f : T ∗ → T ∗ heißt Turing – berechenbar, wenn es eine deterministische Turing – Maschine gibt, so dass für jedes Wort t ∈ T ∗ folgende Aussagen gelten: (1) f (t) hat einen Bildpunkt (f (t) ist definiert; genauer: der Wert von f für das Argument t ist definiert) und es existiert eine vollständige Berechnung (s0 , t, #, ε) → · · · → (se , r, #, ε) wobei f (t) = r ist. (2) f ist für das Argument t nicht definiert und es existiert eine unendliche Berechnung (s0 , t, #, ε) → · · · → . . . 278 KAPITEL 2. BERECHENBARKEIT Dieser Berechenbarkeitsbegriff lässt sich auf partielle Abbildungen f : Nn → N übertragen. Dazu müssen wir nun eine Zahldarstellung festlegen. Wir stellen natürliche Zahlen durch Strichzahlen dar und verwenden t als Trennzeichen. f heißt berechenbar, wenn eine Funktion g : {|, t}∗ → {|, t}∗ existiert, die berechenbar ist und ∀x1 , . . . , xn ∈ N gilt: f (hti ◦ |x1 ◦ hti ◦ · · · ◦ hti ◦ |xn ◦ hti) = hti ◦ |y ◦ hti genau dann, wenn f (x1 , . . . , xn ) = y ist. Beispiel: einige Turing – berechenbare Funktionen (1) Konstante: c : Nn → N, c(x1 , . . . , xn ) = k für gegebenes k ∈ N (2) Nachfolgefunktion: succ : N → N mit succ(n) = n + 1 (3) Projektionen: πin : Nn → N mit πin (x1 , . . . , xn ) = xi (4) Vorgänger: ( x−1 x>0 pre : N → N total: pre(x) = 0 x=0 ( x−1 x>0 pred : N → N partiell: pred(x) = undef x = 0 Komplexere Funktionen können wir durch Komposition (Funktionskomposition) gewinnen. Wir definieren eine verallgemeinerte Komposition für partielle Funktionen: Gegeben seien g : Nn → N hi : N m → N 1≤i≤n Wir definieren f : Nm → N durch die Gleichung f (x1 , . . . , xm ) = g (h1 (x1 , . . . , xm ) , . . . , hn (x1 , . . . , xm )) falls h1 , . . . , hn für x1 , . . . , xm und f für h1 (x1 , . . . , xm ), . . . , hn (x1 , . . . , xm ) definiert ist; sonst ist f für x1 , . . . , xm undefiniert. Satz: f ist Turing – berechenbar, wenn g, h1 , . . . , hn Turing – berechenbar sind. 2.1. HYPOTHETISCHE MASCHINEN 279 Notation: Wir schreiben dann für f auch g ◦ [h1 , . . . , hn ] Es gilt: Addition, Multiplikation, Division (d.h. die übliche Arithmetik) ist Turing – berechenbar. Frage: Gibt es Funktionen, die nicht Turing – berechenbar sind? Ja — siehe später. Bemerkungen: (1) Ob wir Turing – Maschinen mit mehreren Bändern, nur einseitig unendliche Turing – Maschinen verwenden, ändert nichts am Begriff der Turing – Berechenbarkeit. (2) Andere Berechenbarkeitsbegriffe haben sich als äquivalent erwiesen. Dazu folgen zwei Beispiele. 2.1.2 Registermaschinen Eine Registermaschine ist (wie eine Turing – Maschine) eine hypothetische Maschine (d.h. mathematisch definiert), die unseren gebräuchlichen Rechnern entspricht. Eine Registermaschine mit n Registern (n – Registermaschine) besitzt n Register (Speicherplätze für natürliche Zahlen) und ein Programm (ein n – Registermaschinen – Programm). Diese Programme haben eine extrem einfache Syntax: (1) ε ist ein n – Registermaschinen – Programm (leeres Programm). (2) succi mit 1 ≤ i ≤ n ist ein n – Registermaschinen – Programm. (3) predi mit 1 ≤ i ≤ n ist ein n – Registermaschinen – Programm. (4) Sind M1 und M2 n – Registermaschinen – Programme, so ist M1 ; M2 ein n – Registermaschinen – Programm. (5) Ist M ein n – Registermaschinen – Programm, so ist whilei (M ) mit 1 ≤ i ≤ n ein n – Registermaschinen – Programm. Semantik von Registermaschinen durch Zustandsübergangsbeschreibung Konfigurationen einer n – Registermaschine: (s, p) ∈ Nn × n-PROG Dabei bezeichnet n-PROG die Menge aller Programme für n – Registermaschinen. Zustands- bzw. Konfigurationsübergangsfunktion (für n – Registermaschinen mit i ∈ N, 1 ≤ i ≤ n): (s, succi ) → ((s1 , . . . , si−1 , si + 1, si+1 , . . . , sn ), ε) (s, predi ) → ((s1 , . . . , si−1 , k, si+1 , . . . , sn ), ε) mit ( si − 1 falls si > 0 k= 0 sonst (s, (p1 ; p2 )) → (s0 , (p01 ; p2 )) falls (s, p1 ) → (s0 , p01 ) (s, (ε, p2 )) → (s, p2 ) ( (s, (p; whilei (p))) falls si > 0 (s, whilei (p)) → (s, ε) sonst 280 KAPITEL 2. BERECHENBARKEIT Bemerkung: Die Konfigurationen der Form (s, ε) sind terminal, d.h. es existiert keine Nachfolgekonfiguration (das Programm terminiert). Wie gehabt definieren wir Berechnungen (endliche / unendliche) als Folgen von Konfigurationen in der → – Relation. Beispiel: Registermaschinen – Programme (1) whilei (predi ) entspricht while si > 0 do si := si - 1 od (si wird auf 0 gesetzt). si := k entspricht whilei (predi ); succi ; . . . ; succi (mit k Aufrufen von succi ). (2) Folgende Funktionen können in Registermaschinen programmiert werden: • übliche Arithmetik • übliche boolesche Algebra Eine Funktion f : Nn → Nn heißt Registermaschinen – berechenbar, wenn es ein n – Registermaschinenprogramm p gibt, so dass gilt: falls f (s1 , . . . , sn ) = (s01 , . . . , s0n ) dann gilt ∗ (s, p) → (s0 , ε) (mit s = (s1 , . . . , sn ) und s0 = (s01 , . . . , s0n )). Auch g : Nk → N mit 1 ≤ k ≤ n und (für ein i ∈ N, 1 ≤ i ≤ n) g(x1 , . . . , xk ) = πin (f (x1 , . . . , xk , xk+1 , . . . , xn )) für gegebene xk+1 , . . . , xn heißt Registermaschinen – berechenbar. 2.2 Rekursive Funktionen Die beiden bisher betrachteten Berechnungsmodelle (Turing – Maschinen, Registermaschinen) sind hypothetische Maschinen. Jetzt betrachten wir ein stärker durch Beschreibung charakterisiertes Berechnungsmodell: rekursiv definierte Funktionen. Auch dafür können wir Algorithmen zur Auswertung angeben (vgl. Termersetzung). 2.2.1 Primitiv rekursive Funktionen Wir betrachten n – stellige (totale oder partielle) Funktionen f : Nn → N. Wir nennen folgende Funktionen Grundfunktionen: succ : N → N zero (0) mit succ(n) = n + 1 :→ N (1) zero : N → N πin : Nn → N i-te Projektion, 1 ≤ i ≤ n 2.2. REKURSIVE FUNKTIONEN 281 Aus einer gegebenen Menge von Funktionen gewinnen wir weitere durch Komposition g ◦ [k1 , . . . , kn ] oder durch Anwendung des Schemas der primitiven Rekursion: Gegeben seien Funktionen g : Nk → N h : Nk+2 → N Wir definieren f : Nk+1 → N wie folgt: ∀x1 , . . . , xk , n ∈ N: f (x1 , . . . , xk , 0) = g(x1 , . . . , xk ) f (x1 , . . . , xk , n + 1) = h(x1 , . . . , xk , n, f (x1 , . . . , xk , n)) Beispiel: add(x, 0) = x add(x, n + 1) = add(x, n) + 1 = h(x, n, add(x, n)) = succ(π33 (x, n, add(x, n))) Dies entspricht einer induktiven Definition von f . Fakt: Sind g, h totale Funktionen, so ist durch das Schema f eindeutig bestimmt und total. Beweis: Induktion Bemerkung: Sind g, h partielle Funktionen, so ist f auch eindeutig festgelegt, aber unter Umständen partiell. Die Menge der primitiv rekursiven Funktionen (P R) ist induktiv wie folgt definiert: (1) Die Grundfunktionen sind in P R. (2) Funktionen, die durch Komposition von Funktionen aus P R gewonnen werden können, sind in P R. (3) Funktionen, die durch das Schema der primitiven Rekursion über Funktionen g, h ∈ P R gewonnen werden können, sind in P R. Wir können — wie für die Komposition — auch für das Schema der primitiven Rekursion eine kompakte Notation einführen: pr : (Nk → N) × (Nk+2 → N) → (Nk+1 → N) mit f = pr(g, h) Es gilt: (1) Alle arithmetischen Funktionen (totale Division, totale Subtraktion) sind in P R. (2) Alle Funktionen in P R sind total. (3) Alle Funktionen in P R sind Turing – berechenbar. (4) Alle Funktionen in P R sind Registermaschinen – berechenbar. 282 KAPITEL 2. BERECHENBARKEIT (5) Da alle Funktionen in P R total sind, existieren trivialerweise Funktionen, die Turing – berechenbar bzw. Registermaschinen – berechenbar, aber nicht in P R sind. Wir zeigen nun, dass es eine totale Funktion gibt, die offensichtlich Turing – und Registermaschinen – berechenbar ist, aber nicht in P R: ack : N2 → N n=0 m + 1 ack(n, m) = ack(n − 1, 1) n > 0, m = 0 ack(n − 1, ack(n, m − 1)) sonst Feststellungen: (1) Durch diese Gleichung ist ack eindeutig bestimmt und total. (2) Wir definieren eine Schar von Funktionen Bn : N → N durch Bn (m) := ack(n, m). Wir erhalten: B0 (m) = m + 1 B1 (m) = m + 2 B2 (m) = 2m + 3 B3 (m) = 2m+2 − 3 .. . Alle Funktionen Bn sind primitiv rekursiv. Bn+1 kann durch das Schema der primitiven Rekursion aus Bn definiert werden. Satz: ack ∈ / PR Beweis (Skizze): Durch Induktion über die Anzahl der Anwendungen des Schemas der primitiven Rekursion können wir zeigen: Zu jeder Funktion g ∈ P R, g : Nn → N existiert eine Konstante c ∈ N, so dass gilt ! n X g(x1 , . . . , xn ) < ack c, xi i=1 für alle x1 , . . . , xn ∈ N. Anmerkung: Angenommen, ack ∈ P R. Dann wäre h : N → N mit h(n) = ack(n, n) auch in P R. Es würde eine Zahl c ∈ N geben mit ∀n ∈ N : ack(n, n) = h(n) < ack(c, n) Mit n = c erhalten wir den Widerspruch ack(c, c) = h(c) < ack(c, c) In anderen Worten: ack wächst schneller als alle Funktionen in P R. Fazit: Es existieren totale Funktionen, die Turing – berechenbar, Registermaschinen – berechenbar, aber nicht in P R sind. ack ist offensichtlich durch Rekursion definierbar, also ist der Begriff der primitiven Rekursion zu eng. 2.2.2 µ – rekursive Funktionen Jetzt betrachten wir auch partielle Funktionen, verwenden alle Funktionen aus P R (und alle Konstruktionsprinzipien) und zusätzlich eine weitere Konstruktion, die der µ – Rekursion. 2.2. REKURSIVE FUNKTIONEN Beispiel: 283 rekursive Definition ulam : N → N ( 1 n≤1 ulam(n) = ulam(g(n)) n > 1 ( falls n gerade b n2 c g(n) = 3n + 1 sonst Feststellung: g ist primitiv rekursiv. Frage: Terminiert ulam für alle Argumente n ∈ N? Antwort: unbekannt, aber Vermutung: Antwort ist ja. Falls ulam immer terminiert, gilt ulam(n) = 1; ulam ist dann primitiv rekursiv. Das Schema der µ – Rekursion: Gegeben sei eine partielle Funktion f : Nk+1 → N Wir definieren µ(f ) : Nk → N durch µ(f )(x1 , . . . , xk ) = min {y ∈ N : f (x1 , . . . , xk , y) = 0} falls ∃y ∈ N : f (x1 , . . . , xk , y) = 0∧∀z ∈ N : 0 ≤ z ≤ y ⇒ f ist definiert für (x1 , . . . , xk , z) Gilt diese Bedingung nicht, so definieren wir µ(f ) für (x1 , . . . , xk ) als nicht definiert. Verfahren zur Berechnung von y = µ(f )(x1 , . . . , xk ): (1) Berechne y0 = f (x1 , . . . , xk , 0). (a) y0 = 0 ⇒ y = 0 (b) y0 > 0: gehe zu (2) (c) Berechnung terminiert nicht: µ(f ) ist nicht definiert für (x1 , . . . , xk ). (2) analog für f (x1 , . . . , xk , 1) ... Achtung: Auch wenn f total ist, kann µ(f ) partiell sein. Die Menge der µ – rekursiven Funktionen M R definieren wir induktiv wie folgt: 1. Alle primitiv rekursiven Funktionen sind in M R. 2. Funktionen, die durch Komposition oder das Schema der primitiven Rekursion aus Funktionen in M R gebildet werden können, sind in M R. 3. Falls f ∈ M R, dann ist µ(f ) ∈ M R. Beispiel: µ – rekursive Definition der partiellen Subtraktion: ( . a−b a≥b a−b= undef sonst Wir definieren . a − b = µ(h0 )(a, b) 284 KAPITEL 2. BERECHENBARKEIT mit geeignetem h0 : N3 → N. Wir wählen h0 (a, b, y) = sub(b + y, a) + sub(a, b + y) wobei ( a−b a≥b sub(a, b) = 0 sonst sei. Fälle a≥b sub(b + y, a) =0 a≥b sub(a, b + y) =0 >0 b>a 2.2.3 + Fälle y =a−b≥0 y <a−b y+b<a >0 Allgemeine Bemerkungen zur Rekursion In der Informatik existieren viele Spielarten von Rekursion zur Definition von Funktionen, Prozeduren, Methoden, Datentypen, Mengen, formalen Sprachen usw. In der rekursiven Definition einer Funktion verwenden wir Gleichungen der Form f (x) = E, wobei f in E auftritt, oder f (D) = E mit eingeschränktem Ausdruck D. Beispiel (strukturelle Rekursion, induktive Definition) ack(0, 0) = 1 ack(0, m + 1) = ack(0, m) + 1 ack(n + 1, 0) = ack(n, 1) ack(n + 1, m + 1) = ack(n, ack(n + 1, m)) 2.3 Äquivalenz der Berechenbarkeitsbegriffe Wir haben vier Begriffe der Berechenbarkeit eingeführt: • Turing – Maschinen • Registermaschinen • primitive Rekursion • µ – Rekursion Primitive Rekursion ist schwächer als Turing – Maschinen, Registermaschinen, µ – Rekursion. Wir zeigen nun die Äquivalenz von Turing – Maschinen, Registermaschinen und µ – Rekursion. 2.3.1 Äquivalenz von µ – Berechenbarkeit und Turing – Berechenbarkeit Der Logiker Kurt Gödel hat eine Idee zur Darstellung von Zeichenfolgen durch Zahlen entwickelt → Gödelisierung (1933). Eine Gödelisierung ist eine Abbildung f : A∗ → N wobei A eine endliche Menge von Zeichen sei, mit folgenden Eigenschaften: 2.3. ÄQUIVALENZ DER BERECHENBARKEITSBEGRIFFE 285 i. f ist injektiv. ii. f ist berechenbar (Turing – berechenbar). iii. Es ist berechenbar, ob eine Zahl n ∈ N im Bildbereich von f liegt (d.h. ∃w ∈ A∗ : f (w) = n). iv. Es existiert ein Algorithmus, der zu jeder Zahl im Bildbereich das zugehörige Wort berechnet (f ist algorithmisch umkehrbar). ∼ Satz: T M = M R Beweis: 1. f ∈ M R ⇒ f ∈ T M Beweisidee: Für jede Grundfunktion in M R (bzw. P R) können wir eine Turing – Maschine angeben. Das Schema der Komposition, der primitiven Rekursion und der µ – Rekursion können wir durch Turing – Maschinen nachbauen. 2. f ∈ T M ⇒ f ∈ M R Konfigurationen von Turing – Maschinen entsprechen Wörtern aus A∗ (mit geeignetem A). Wir verwenden eine Gödelisierung rep : A∗ → N (einfach zu konstruieren). Es zeigt sich, dass die Funktion g:N→N mit k0 → k1 ⇔ g(rep(k0 )) = rep(k1 ) primitiv rekursiv ist (d.h. die Nachfolgefunktion auf Konfigurationen entspricht einer primitiv rekursiven Funktion auf der Darstellung der Konfigurationen durch natürliche Zahlen). Ferner definieren wir eine primitiv rekursive Funktion h : N2 → N durch ( 0 h(n, m) = 1 falls g m (n) eine terminalen Konfiguration entspricht sonst Die Funktion it : N2 → N sei spezifiziert durch it(n, 0) = n it(n, m + 1) = it(g(n), m) it beschreibt die Ausführung von m Schritten der Turing – Maschine. Die Funktion tm : N → N sei definiert durch tm(n) = it(n, µ(h)(n)) wobei µ(h)(n) die Anzahl der Schritte der Turing – Maschine bis zur Terminierung für die Eingangskonfiguration, dargestellt durch n, angibt, falls die Turing – Maschine terminiert. 286 2.3.2 KAPITEL 2. BERECHENBARKEIT Äquivalenz von Registermaschinen- und Turing – Berechenbarkeit Jede Registermaschine lässt sich in eine Turing – Maschine übersetzen. Dazu brauchen wir eine Darstellung der Konfigurationen der Registermaschine durch eine Turing – Maschine: 1. Die Registerinhalte werden auf dem Band der Turing – Maschine dargestellt (z.B. als Strichzahlen). 2. Aus Registermaschinen – Programmen konstruieren wir den endlichen Automaten, der die Turing – Maschine steuert. Zu einer gegebenen Turing – Maschine kann eine Registermaschine konstruiert werden, die die Turing – Maschine simuliert. Idee: Konfigurationen der Turing – Maschine werden gödelisiert und in den Registern dargestellt. Die Fahrbewegungen und Zustandsübergänge der Turing – Maschine werden durch Programmschritte der Registermaschine simuliert. 2.3.3 Churchs These (Alazo Church, 1936): Jede intuitiv berechenbare Funktion ist µ – rekursiv. 2.4 Entscheidbarkeit Wir haben eine Reihe von Berechenbarkeitsbegriffen kennengelernt, die jeweils auf das gleiche Konzept von berechenbarer Funktion führen (Churchsche These). Wir wenden uns nun der Frage zu, welche Funktionen berechenbar / nicht berechenbar und welche Prädikate entscheidbar (d.h. durch Algorithmen eindeutig mit ja oder nein beantwortbar) sind. 2.4.1 Nicht berechenbare Funktionen Jeder Formalismus zur Berechenbarkeit (Turing – Maschinen, Registermaschinen, µ – Rekursion) definiert Programme / Algorithmen durch Wörter über einem Zeichensatz, d.h. in jedem Fall existiert eine formale Sprache P RG ⊆ A∗ mit geeignetem Zeichensatz A, so dass gilt: Jeder Algorithmus wird dargestellt durch ein Wort p ∈ P RG. Für jede berechenbare Funktion f existiert ein p ∈ P RG, das f berechnet. Die Menge der Funktionen Nn → N ist überabzählbar (Ergebnis der Mengenlehre). N ist abzählbar. A∗ ist abzählbar, falls A endlich ist. P RG ist ebenfalls abzählbar. Dies ergibt sofort: Es gibt viel mehr Funktionen Nn → N als Programme in P RG, d.h. fast jede Funktion ist nicht berechenbar. Wir können Programme durch Zahlen codieren, d.h. es gibt eine Gödelisierung code : P RG → N Satz: Es existiert eine totale Funktion g : N → N, die nicht berechenbar ist. Beweis: Wir spezifizieren g durch (∀n ∈ N): ( fp (n) + 1 falls ∃p ∈ P RG : code(p) = n und fp definiert für Argument n g(n) = 0 sonst 2.4. ENTSCHEIDBARKEIT 287 Dabei sei für p ∈ P RG die Funktion fp : N → N die durch p berechnete partielle Funktion. Achtung: Die Spezifikation von g ist konsistent und eindeutig, da code eine injektive Funktion ist. Annahme: g ist berechenbar. Dann existiert ein Programm q ∈ P RG mit fq = g. Mit code(q) = m gilt g(m) = fq (m) + 1 = g(m) + 1 Widerspruch. Bemerkung: g ist zunächst nur von theoretischem Interesse. 2.4.2 (Nicht) entscheidbare Prädikate Eine Ja – Nein – Frage entspricht logisch einem Prädikat. Ein Prädikat p:M →B kann auch als Funktion p0 : M → {0, 1} ⊂ N aufgefasst werden. Damit können wir das Konzept der Berechenbarkeit auf Prädikate übertragen. Ein berechenbares totales Prädikat heißt auch entscheidbar. Ein Prädikat p : M → B heißt: entscheidbar: wenn es einen Algorithmus gibt, der für jedes Argument m ∈ M terminiert und korrekt 1 für p(m) = true und 0 für p(m) = false ausgibt. positiv semientscheidbar: wenn es einen Algorithmus gibt, der für jedes Argument m ∈ M terminiert und korrekt 1 ausgibt, falls p(m) = true gilt und falls p(m) = false gilt entweder 0 ausgibt oder nicht terminiert. negativ semientscheidbar: falls ¬p positiv semientscheidbar ist. Beispiele 1. entscheidbare Funktionen • Gleichheit zweier Zahlen in Binärdarstellung • Primzahleigenschaft • Zugehörigkeit eines Wortes zum Sprachschatz einer Chomsky – 3 – Grammatik • Existenz der Nullstellen eines Polynoms über den ganzen Zahlen 2. positiv semientscheidbare (aber nicht allgemein entscheidbare) Funktionen • Terminierung einer Turing – Maschine für eine bestimmte Eingabe (Halteproblem) • Zugehörigkeit eines Wortes zum Sprachschatz einer Chomsky – 0 – Sprache 3. negativ semientscheidbar • Gleichheit zweier durch primitive Rekursion gegebener Funktionen 4. unentscheidbare Prädikate (weder positiv noch negativ semientscheidbar) • Terminierung einer durch µ – Rekursion beschriebenen Funktion für alle Eingaben 288 KAPITEL 2. BERECHENBARKEIT Satz: Das Terminierungsproblem für µ – rekursive Funktionen ist nicht entscheidbar. Beweis: Sei µ−P RG die Menge der Wörter, die µ – rekursive Programme darstellen. Widerspruchsannahme: Das Terminierungsproblem ist entscheidbar. Dann existiert ein µ – rekursives Programm p ∈ µ − P RG, das eine Funktion fp berechnet, so dass gilt ( 0 falls ∃q ∈ µ − P RG : code(q) = x ∧ fq (x) für x nicht definiert fp (x) = undef sonst Wir erhalten für m = code(p): ( 0 fp (m) = undef falls fp (code(p)) = undef sonst Widerspruch. 2.4.3 Rekursion und rekursiv aufzählbare Mengen Wir betrachten jetzt eine Obermenge U sowie Teilmengen M ⊆ U . Ein Prädikat auf U p:U →B (charakteristisches Prädikat zu M ⊆ U ) ist dann gegeben durch ( true p(x) = false falls x ∈ M falls x ∈ /M Eine Menge heißt rekursiv, wenn das charakteristische Prädikat entscheidbar ist. Satz: Jede Chomksy – 1 – Sprache ist rekursiv. Eine totale Abbildung e:N→U heißt Aufzählung der Menge M , falls M = {e(n) : n ∈ N} = [ {e(n)} n∈N M heißt rekursiv aufzählbar, falls e berechenbar ist. Jede rekursiv aufzählbare Menge hat ein passendes charakteristisches Prädikat. Bemerkung: In einer Aufzählung einer Menge M e(0), e(1), e(2), . . . können Elemente beliebig oft vorkommen, aber jedes Element muss mindestens einmal vorkommen. Beispiele: 1. Die Menge der primitiv rekursiven Funktionen ist rekursiv aufzählbar, aber nicht rekursiv. 2. Die Menge der Argumente, für die eine µ – rekursive Funktion (Turing – Maschine, Registermaschine) terminiert, ist rekursiv aufzählbar, aber nicht rekursiv. 2.4. ENTSCHEIDBARKEIT 289 Satz: Jede Chomsky – Sprache ist rekursiv aufzählbar. Beweis: Die Grammatik der Sprache definiert einen Baum von Ableitungen, aus dem wir ein Aufzählungsverfahren gewinnen. Satz: Die Menge der Argumente einer berechenbaren Funktion f , für die f nicht definiert ist (der Algorithmus nicht terminiert) ist i.a. nicht rekursiv aufzählbar. Beweis: Folgt direkt aus dem Halteproblem. Satz: Eine Menge S ⊆ T ∗ ist genau dann rekursiv, wenn S und T ∗ \ S rekursiv aufzählbar sind. Beweis: Wir zählen S und T ∗ \ S parallel auf. Irgendwann tritt jedes Element in einer der Aufzählungen auf. Dann liegt fest, ob es in S oder T ∗ \ S liegt. Dieses Verfahren terminiert für jedes Element aus T ∗ . 290 KAPITEL 2. BERECHENBARKEIT Kapitel 3 Komplexitätstheorie Jeder Algorithmus verbraucht bei der Ausführung gewisse Betriebsmittel, erfordert einen Berechnungsaufwand. Fragen: i. Bei einem gegebenen Algorithmusbegriff (Turing – Maschine, Registermaschine, µ – Rekursion) können wir nun für ein gegebenes Problem (z.B. Multiplikation) fragen, welcher Algorithmus den geringsten Berechnungsaufwand hat. ii. Wie hängt die Klassifizierung des Berechnungsaufwands von der Wahl des Algorithmenbegriffs ab? Wir werden die Aufwandsschätzung für Algorithmen und für Probleme an Turing – Maschinen orientieren. 3.1 Komplexitätsmaße Wir wählen zwei einfache Maßzahlen für den Berechnungsaufwand eines Algorithmus: • Anzahl der Schritte in der Berechnung (→ Zeitkomplexität) • Anzahl der benötigten (Hilfs –)Speicherzellen (→ Bandkomplexität) 3.1.1 Zeitkomplexität Wir betrachten k – Band – Turing – Maschinen. Für jedes Band existiert ein Lese- / Schreibkopf. Für einen gegebenen Eingabewert (etwa auf Band 1) führt die Turing – Maschine eine endliche oder unendliche Anzahl von Schritten aus. Terminiert die Turing – Maschine für ein Wort w, so bezeichne b(w) die Anzahl der Schritte. Gilt für eine Funktion T : N → N für alle Worter w ∈ Z ∗ der Länge n = |w| stets b(w) ≤ T (n), so heißt die Turing – Maschine (der Algorithmus) T (n) – zeitbeschränkt. Achtung: Diese Definition schließt nichtdeterministische Turing – Maschinen mit ein. Dann müssen alle Berechnungen für w durch T (n) beschränkt sein. Mehrband – Turing – Maschinen vermeiden den Aufwand von Kopfbewegungen, um zwischen Argumenten hin- und herzufahren und liefern realistischere Abschätzungen. Die Definition von Zeitbeschränktheit lässt sich von Algorithmen auf Probleme übertragen: Ein Problem1 heißt T (n) – zeitbeschränkt, wenn es einen Algorithmus gibt, der das Problem berechnet und T (n) – zeitbeschränkt ist. 1 Ein Problem entspricht immer der Aufgabe, eine Funktion / Prozedur zu berechnen 291 292 KAPITEL 3. KOMPLEXITÄTSTHEORIE Beispiel: Das Problem, zu entscheiden, ob ein Wort in der Sprache L = {ak cbk : k ∈ N} ist, ist (n + 1) zeitbeschränkt. Dafür ist nur zu zeigen, dass ein Algorithmus (eine Turing – Maschine) existiert, der in n + 1 Schritten für ein Wort w der Länge n entscheidet, ob w ∈ L ist. Wir sagen, die Turing – Maschine ist linear beschränkt. Addition: Seien die Zahlen a, b ∈ N dargestellt durch Binärzahlen; eine Turing – Maschine für die Addition benötigt log2 (b + a) + 2 + 2 log2 (b) + 1 + 1 Schritte. Sprechweise: Das Problem ist log – zeitbeschränkt. Anmerkung: Wir betrachten nur Probleme mit T (n) ≥ n+1, d.h. wir betrachten nur Problemstellungen, wo die gesamte Eingabe für den Algorithmus relevant ist. 3.1.2 Bandkomplexität Bandkomplexität bewertet den Speicheraufwand eines Algorithmus. Wir messen die Bandkomplexität wieder durch Turing – Maschinen über die Anzahl der im Laufe einer Berechnung beschriebenen Speicherzellen. Wir betrachten Turing – Maschinen mit folgender Charakteristik: • ein Eingabeband, das nicht beschrieben wird und eine Endmarkierung enthält • k einseitig unendliche Bänder Für ein Eingabewort w ∈ Z ∗ verwendet die Turing – Maschine bi (w) ∈ N Speicherzellen auf ihrem i – ten Band (1 ≤ i ≤ k). Gilt für eine Abbildung S:N→N für alle Wörter w mit n = |w|, alle i ,1 ≤ i ≤ k bi (w) ≤ S(n) so heißt die Turing – Maschine S(n) – bandbeschränkt. Wir sagen: Die Turing – Maschine hat Bandkomplexität S(n). Beispiel: Sprache L = {an cbn : n ∈ N} Wenn wir die Zahlen in Binärschreibweise notieren, dann benötigen wir 1 + log2 (n) Speicherzellen, um die an Zeichen zu zählen. Wir erhalten einen logarithmisch bandbeschränkten Algorithmus. Definition: Ein Problem heißt S(n) – bandbeschränkt, wenn es einen Algorithmus (eine Turing – Maschine) gibt, der das Problem löst und S(n) – bandbeschränkt ist. Wenn wir von Bandkomplexität S(n) sprechen, meinen wir stets max{1, S(n)}. Wir sind bei Abschätzungen von Band- / Zeitkomplexität in der Regel nicht an exakten Zahlen interessiert, sondern an Größenordnungen. Dazu verwenden wir Abbildungen f, g : N → N um das assymptotische Verhalten abzuschätzen. Wir sagen: Die Funktion g wächst mit der Ordnung Θ(f (n)), wenn gilt: ∃c, d ∈ N \ {0}, n0 ∈ N : ∀n ∈ N : n ≥ n0 ⇒ f (n) ≤ dg(n) ≤ cf (n) 3.1. KOMPLEXITÄTSMASSE Beispiel: Zeit zur Lösung eines Problems der Größenordnung führung des Lösungsalgorithmus O(n) Mikrosekunden benötigt. n log2 n n n log2 n n2 10 0, 000003 sec 0, 00001 sec 0, 00003 sec 0, 0001 sec 102 0, 000007 sec 0, 0001 sec 0, 0007 sec 0, 01 sec 103 0, 000010 sec 0, 001 sec 0, 01 sec 1 sec 104 0, 000013 sec 0, 01 sec 0, 13 sec 1, 7 min 105 0, 000017 sec 0, 1 sec 1, 7 sec 2, 8 Std Diese Tabelle zeigt den Unterschied zwischen 293 n, wenn die Aus2n 0, 001 sec 1016 Jahre astronomisch astronomisch astronomisch • prinzipieller (theoretischer) Berechenbarkeit und • praktischer Berechenbarkeit. Auch die Bandkomplexität lässt sich auf nichtdeterministische Turing – Maschinen anwenden. Eine nichtdeterministische Turing – Maschine hat Bandkomplexität S(n), falls für jedes Wort w ∈ Z ∗ mit |w| = n alle Berechnungen der Turing – Maschine höchstens S(n) Speicherzellen pro Band verwenden. 3.1.3 Zeit- und Bandkomplexitätsklassen Eine Problemstellung (Aufgabe: berechne Funktionswert / Prädikat) heißt deterministisch T (n) – zeitbeschränkt (bzw. deterministsich T (n) – bandbeschränkt), wenn es eine deterministische Turing – Maschine gibt, die das Problem löst und T (n) – zeitbeschränkt (bzw. T (n) – bandbeschränkt) ist. Analog kann nichtdeterministische T (n) – Zeit- / Bandbeschränktheit definiert werden. Abkürzungen: • DTIME (T (n)): Klasse der Probleme, die deterministisch T (n) – zeitbeschränkt sind • NTIME (T (n)): Klasse der Probleme, die nichtdeterministisch T (n) – zeitbeschränkt sind • DSPACE (T (n)): Klasse der Probleme, die deterministisch T (n) – bandbeschränkt sind • NSPACE (T (n)): Klasse der Probleme, die nichtdeterministisch T (n) – bandbeschränkt sind Wichtig: Hier brauchen uns nur die Größenordnungen zu interessieren (assymptotisches Verhalten), wie folgende Sätze zeigen: Satz (Bandkompression): Ist ein Problem S(n) – bandbeschränkt, so ist für jede Konstante c ∈ R mit c > 0 das Problem auch cS(n) – bandbeschränkt. Beweis: Konstruiere eine Turing – Maschine, die r Zeichen in ein Zeichen zusammenfasst. Korollar: Für alle c ∈ R mit c > 0 gilt NSPACE (S(n)) = NSPACE (cS(n)) DSPACE (S(n)) = DSPACE (cS(n)) Satz (Reduktion der Bänderzahl): Sei M eine Turing – Maschine, S(n) – bandbeschränkt mit k Bändern. Wir können eine Turing – Maschine konstruieren, die die k Bänder von M auf einem Band simuliert. Satz (linearer Speed – Up): Wird ein Problem von einer T (n) – zeitbeschränkten k – Band – Turing – Maschine gelöst, so wird es für jede reelle Zahl c ∈ R, c > 0 294 KAPITEL 3. KOMPLEXITÄTSTHEORIE auch durch eine cT (n) – zeitbeschränkte k – Band – Turing – Maschine gelöst, falls k > 1 und limn→∞ T (n) n = ∞. Korollar: Unter diesen Voraussetzungen gilt DTIME (T (n)) = DTIME (cT (n)) NTIME (T (n)) = NTIME (cT (n)) Satz (nach Savitch): Für jedes Akzeptanzproblem A (Erkennung, ob ein Wort in einer Sprache liegt), gilt: Ist A ∈ NSPACE (S(n)), dann gilt auch A ∈ DSPACE S 2 (n) , falls gilt (die Funktion S ist vollbandkonstruierbar): Es gibt eine Turing – Maschine, so dass gilt: Für alle n ∈ N existiert ein Eingabewort w mit |w| = n, so dass die Turing – Maschine tatsächlich S(n) Speicherzellen benötigt. 3.1.4 Polynomiale und nichtdeterministisch polynomiale Zeitkomplexität Ein Problem heißt (bezüglich Zeitkomplexität) polynomial deterministisch lösbar, wenn es in DTIME nk liegt mit k ∈ N. Analog definieren wir exponentielle Zeitkomplexität: DTIME (2n ). Besonders interessiert [ DTIME (2n ) \ DTIME nk k∈N Dies sind die Probleme, die für größere n theoretisch, aber nicht praktisch berechenbar sind. Wir diskutieren nun zwei Klassen: [ P := DTIME nk k∈N N P := [ NTIME nk k∈N Es gilt N P ⊆ DTIME (2n ). Frage: Gilt P = N P? Klarer ist diese Frage für die Band- / Speicherkomplexität beantwortet: [ PSPACE = DSPACE ni k≥1 N P SPACE = [ NSPACE ni k≥1 Nach dem Satz von Savitch: NSPACE ni ⊆ DSPACE n2i Dies ergibt sofort PSPACE = N P SPACE Es gilt NSPACE (log n) ⊆ P ⊆ N P ⊆ PSPACE Dies ergibt eine Klassifizierung von Problemen in solche, die praktisch lösbar sind und solche, die aufgrund des benötigten Aufwands (exponentielle Zunahme des Aufwandes abhängig von der Größe der Eingabe) praktisch nicht mehr lösbar sind. 3.1. KOMPLEXITÄTSMASSE 3.1.5 295 Nichtdeterminismus in Algorithmen — Backtracking Bisher hatten wir Nichtdeterminismus im Wesentlichen bei Turing – Maschinen studiert. Nun behandeln wir Nichtdeterminismus auf der Ebene von Programmiersprachen. Nichtdeterminismus liegt in einem System oder Algorithmus vor, wenn bei bestimmten Schritten Wahlmöglichkeiten gegeben sind. Achtung: Wir betrachten hier keine Annahmen über Wahrscheinlichkeiten für die Wahl der Schritte. Für funktionale Sprachen können wir Nichtdeterminismus durch folgende Konstruktion einführen: Wir betrachten für Ausdrücke E1 und E2 den nichtdeterministischen Ausdruck E1 E2 Dieser Ausdruck ergibt bei Auswertung den (einen) Wert von E1 oder den (einen) Wert von E2. Bemerkungen: • Diese Idee lässt sich auch auf Anweisungen übertragen. • Auch die Terminierung kann von der nichtdeterministischen Auswahl abhängen. Wir betrachten im Folgenden nichtdeterministische Algorithmen, bei denen die Terminierung gesichert ist. Beispiel: 1 2 4 6 8 10 12 Nichtdeterministische Erzeugung von Permutationen fct f = (nat n) seq nat: if n = 0 then <> else einf (f(n-1), n) fi fct einf = (seq nat s, nat n) seq nat: if s = <> then <n> else conc (<n>, s) [] conc (<first(s)>, einf(rest(s), n)) fi Behauptung: f (n) erzeugt nichtdeterministisch eine Permutation von {1, . . . , n}, wobei jede Permutation als Ergebnis auftreten kann. Beweis: Induktion über n ∈ N. 1. Fall: n = 0: f (0) liefert <>. 2. Fall: Annahme: f (n − 1) liefert eine beliebige Permutation über {1, . . . , n − 1}. n wird in f (n−1) an einer beliebigen Position eingefügt. Dies liefert eine Permutation von {1, . . . , n}; jede Permutation kann so erzeugt werden. Bisher hatten wir nur Beispiele betrachtet, wo jeder nichtdeterministische Berechnungspfad zu einem Ergebnis führt, das unseren Vorstellungen genügt. Oft studieren wir nichtdeterministische Berechnungen (z.B. Turing – Maschinen), wo gewisse Pfade nicht zu einem brauchbaren Ergebnis führen. Dazu definieren wir einen neuen Ausdruck failure, der darstellt, dass dieses Ergebnis nicht brauchbar ist. 296 KAPITEL 3. KOMPLEXITÄTSTHEORIE Beispiel: 1 2 4 6 8 fct sqrt = (nat n, nat y) nat: if y^2 > n then failure elif y^2 <= n <= (y+1)^2 then y else sqrt (n, 2y+1) [] sqrt (n, 2y) fi sqrt(n, 1) für n > 0 liefert die natürlichzahlige Wurzel, wenn wir vereinbaren, dass nichtdeterministische Berechnungen, die mit failure enden, nicht akzeptiert werden. Eine nichtdeterministische Berechnung darf nur mit failure enden, wenn alle Zweige auf failure führen. Beispiel: Erfüllbarkeit von aussagenlogischen Formeln. Wir betrachten eine logische Formel (boolescher Ausdruck), der genau x1 , . . . , xn als freie Identifikatoren der Sorte bool enthält. Der Ausdruck heißt erfüllbar, wenn es Werte b1 , . . . , bn ∈ B gibt, so dass für x1 = b1 , . . . , xn = bn der Ausdruck den Wert true liefert. Damit ist die Sequenz hb1 . . . bn i der Nachweis für die Erfüllbarkeit. Wir stellen den Ausdruck als Funktion dar: 1 fct ausdruck = (seq bool b) bool Ein nichtdeterministischer Algorithmus für die Erfüllbarkeit: 2 4 6 8 10 fct erfuellbar = (seq bool s) bool: if |s| = n then if ausdruck(s) then true else failure fi else erfuellbar (conc(s, <L>)) [] erfuellbar (conc(s, <0>)) fi erfuellbar(<>) liefert lauter failure – Resultate und damit insgesamt das Resultat failure, falls der gegebene Ausdruck nicht erfüllbar ist; sonst aber true. Für Programmiersprachen, die Konzepte wie nichtdeterministische Auswahl und failure anbieten, müssen spezielle Auswertungstechniken (Auswertung durch Suche) eingesetzt werden. Stichworte: • Logikprogrammierung • Constraint – Programmierung Dazu existieren eigene Theorien: (failure E) = E. Die Idee der nichtdeterministischen Auswertung lässt sich weiter verallgemeinern: Wir verwenden einen Auswahloperator some m x : q(x) wobei q ein Prädikat sei: fct q = (m x) bool: ... (3.1) 3.2. N P – VOLLSTÄNDIGKEIT 297 (3.1) liefert einen beliebigen Wert x, für den q(x) gilt. Falls für alle m x : ¬q(x) gilt, liefert (3.1) den Wert ⊥. Dieser Operator ist nützlich, um Algorithmen zu skizzieren. Beispiel: Hamiltonkreis Gegeben sei die Sorte {0, . . . , n − 1} node der Knoten im Graph. Kanten werden durch die Funktion fct g = (node, node) bool dargestellt (vgl. boolesche Matrix). Es ist g (i, k) = true genau dann, wenn eine Kante vom Knoten i zum Knoten k existiert. Ein Hamiltonkreis für einen Graph durch eine Teilmenge S von Knoten ist ein geschlossener Weg, in dem alle Knoten in S genau ein Mal auftreten. Rechenvorschrift für Hamiltonkreis: 1 2 4 6 8 10 12 14 fct hk = (set node s, seq node q) seq node: if s = emptyset then if g (last(q), first(q)) then q else failure fi else node x = some node z: z in s; if g (last(q), x) then hk (s\{x}, conc (q, <x>)) else failure fi fi Se x ∈ s ein beliebiger Knoten aus der nichtleeren Menge s. hk (s\{x}, <x>) liefert einen Hamiltonkreis, falls ein solcher existiert. 3.2 N P – Vollständigkeit Probleme in N P \ P sind nach heutigem Wissensstand nur exponentiell zu lösen. Es existieren Probleme Q in N P mit folgender Eigenschaft: Jedes andere Problem in N P kann in polynomialer Zeit auf Q zurückgeführt werden. Dann gilt: Q ∈ P ⇒ P = NP Solche Probleme Q heißen N P – vollständig (N P – hart). 3.2.1 Das Erfüllungsproblem Ein boolescher Ausdruck ist gegeben durch eine Menge von Identifikatoren x1 , . . . , xn der Sorte bool und die Verknüpfungen ¬, ∧, ∨, . . . . Natürlich können wir eine Darstellung von booleschen Ausdrücken auf Turing – Maschinen finden. Identifikatoren lassen sich durch Zahlen repräsentieren. Satz: Das Erfüllbarkeitsproblem LSAT für boolesche Ausdrücke ist N P – vollständig. Beweis: 1. LSAT ∈ N P: Wir können eine Turing – Maschine konstruieren, die nacheinander alle Werte für die Identifikatoren nichtdeterministisch festlegt und dann überprüft, ob damit der Ausdruck den Wert true hat. 298 KAPITEL 3. KOMPLEXITÄTSTHEORIE 2. Für alle R ∈ N P existiert eine Turing – Maschine, die R in polynomialer Zeit in das Problem LSAT überführt. Wir beschränken uns auf Probleme der formalen Sprachen, genauer auf das Wortproblem. Ausgangspunkt: Gegeben ist eine nichtdeterministische Turing – Maschine, die für ein gegebenes Wort in polynomialer Zeit entscheidet, ob das Wort in der Sprache ist. Beweisidee: Wir konstruieren eine Turing – Maschine U , die aus der gegebenen Turing – Maschine eine neue konstruiert, die dem Erfüllbarkeitsproblem entspricht, so dass der zugrundeliegende Ausdruck genau dann erfüllbar ist, wenn das Wort akzeptiert wird. Dazu konstruieren wir einen booleschen Ausdruck, der die Konfigurationen der Ausgangs – Turing – Maschine durch Wahrheitswerte charakterisiert sowie die Frage, ob es sich bei der Folge von Konfigurationen um eine Berechnung handelt, beantwortet. 3.2.2 N P – vollständige Probleme Mittlerweile ist für eine reiche Zahl von Problemen nachgewiesen, dass sie N P – vollständig sind. Gelingt es für eines dieser Probleme, einen deterministischen, polynomial beschränkten Algorithmus anzugeben, so ist P = N P bewiesen. Prominente Beispiele i. Clique Gegeben: • ungerichteter Graph: – V Menge der Knoten – R ⊆ V × V Menge der Kanten • k∈N Problem: Existiert eine Teilmenge C ⊆ V mit |C| = k und C × C ⊆ R? Dies entspricht der Frage, ob der Graph einen Teilgraph mit k Knoten enthält, in dem alle Knoten paarweise durch Kanten verbunden sind. ii. Ganzzahlige Programmierung / Optimierung Gegeben: • Matrix A ∈ Zn×n • Vektor v ∈ Zn Gesucht ist ein Vektor c ∈ {0, 1}n , so dass gilt: Ac ≥ v (elementweise). iii. Überdeckende Knotenmengen Gegeben: • ungerichteter Graph: – Knotenmenge V – Kantenmenge R ⊆ V × V mit R = RT • k∈N Gesucht ist eine Knotenmenge U ⊆ V mit |U | = k und U ist Überdeckung, d.h. ∀v, w ∈ V : (v, w) ∈ R ⇒ ¬(v ∈ U ⇔ w ∈ U ) Anschaulich: Gibt es eine Färbung der Knoten (U ), so dass jede Kante einen gefärbten und einen ungefärbten Knoten verbindet? 3.3. EFFIZIENTE ALGORITHMEN . . . 299 iv. Gerichteter Hamiltonkreis (siehe oben) v. Problem des Handlungsreisenden Gegeben: • Menge von Knoten V = {1, . . . , n} • dist : V × V → N • k∈N Es gelte die Dreiecksungleichung: ∀i, j, l ∈ V : dist(i, l) + dist(l, j) ≥ dist(i, j) Gesucht: Permutation x1 , . . . , xn der Knoten mit dist(xn , x1 ) + n−1 X dist(xi , xi+1 ) ≤ k i=1 Folgende verwandte Problemstellungen sind oft von annähernd gleicher Komplexität: • Stelle fest, ob für ein Problem Q eine Lösung existiert. • Berechne eine Lösung zu Q. • Berechne 3.3 optimale Lösung zu Q. Effiziente Algorithmen für N P – vollständige Probleme Auch wenn ein Problem nach heutigem Kenntnisstand nur mit exponentiellem Aufwand zu behandeln ist, ist für praktische Aufgaben die Reduzierung des Aufwands (und sei es nur um Faktoren) von großer Bedeutung. Im Folgenden studieren wir eine Reihe von Techniken zur Reduzierung des Aufwands. Für N P – vollständige Probleme müssen wir mit exponentiellem Aufwand rechnen. Dabei sind Reduktionen im Aufwand die einzige Möglichkeit, gewisse Probleme noch halbwegs vernünftig algorithmisch behandeln zu können. Dies führt auf die Frage, wie ein vorgegebener Algorithmus effizienter gemacht werden kann. Techniken zur Aufwandsreduzierung beim Rechnen mit großen Aufrufbäumen: 1. Branch and Bound (geschicktes Backtracking): Der Lösungsraum (als Baum angeordnet) wird geschickt organisiert: • Einfach auszuwertende Zweige werden zuerst durchlaufen. • Frühzeitig (ohne sie ganz zu durchlaufen) Zweige (Teilbäume) als uninteressant erkennen und nicht vollständig durchlaufen (pruning). 2. Approximative Verfahren: Oft sind optimale Lösungen für ein Problem gesucht (Rundreise von Handlungsreisenden mit minimaler Länge, Scheduling mit optimaler Verteilung etc.). In solchen Fällen sind oft Näherungslösungen auch akzeptabel. Statt des absoluten Optimums wird eine Näherung berechnet, die hinreichend gut ist. 300 KAPITEL 3. KOMPLEXITÄTSTHEORIE 3. Dynamisches Programmieren: Treten in Aufrufbäumen Aufrufe mit gleichen Parametern öfter auf, so können wir durch Tabellieren der Ergebnisse das mehrfache (aufwändige) Aufrufen vermeiden. Im Extremfall rechnen wir statt Top-Down (es wird eine Kaskade von rekursiven Aufrufen erzeugt) BottomUp: Wir erzeugen eine Tabelle mit Ergebnissen, in die wir die einfachen Fälle zuerst eintragen und dann schrittweise die Tabelle vervollständigen (Nachteile: hoher Speicheraufwand, komplexe Datenstrukturen). 4. Probabilistische Algorithmen: Diese Algorithmen verwenden Zufallszahlen (Zufallsgeneratoren). Varianten: • Algorithmen, die mit einer bestimmten Wahrscheinlichkeit ein korrektes oder optimales Ergebnis liefern. • Algorithmen, bei denen die Terminierung innerhalb einer Zeitschranke mit einer bestimmten Wahrscheinlichkeit gesichert ist. 5. Einschränkung der Problemklasse: Häufig können eigentlich N P – vollständige Problemstellungen durch Einschränkungen auf Teilprobleme zurückgeführt werden, die polynomial lösbar sind. 6. Heuristische Methoden: Dies sind Methoden, die ad-hoc eingesetzt werden, ohne genaue Angabe, warum oder wie gut sie funktionieren. Für eine angemessene Behandlung N P – vollständiger Probelme ist besonderes Geschick des Programmierers erforderlich. 3.3.1 Geschicktes Durchlaufen von Baumstrukturen Wir betrachten eine spezielle Problemstellung: Wir minimieren eine Funktion f :M →O wobei O eine linear geordnete Menge sei, über M . Dabei stellen wir uns vor, dass wir die Menge als Baum anordnen können: 1 2 fct baum = (m x) set m: if vollstaendig(x) then {x} 4 else union (i, baum(g(i, x)), 1, k(x)) 6 fi S wobei union (i, f, min, max) = min≤i≤max f sei. baum(x) erzeugt eine Menge, die baumartig angeordnet ist. 8 10 12 14 16 18 fct suchemin = (set m s) m: if |s| = 1 then some m x: x in s else m x = some m z: z in s m y = suchemin (s\{x}) if f(x) <= f(y) then x else y fi fi 3.3. EFFIZIENTE ALGORITHMEN . . . 301 Aufruf: suchemin(baum(x)) Dieses Verfahren ist sehr schematisch und in der Regel ineffizient. Durch geschicktes Verbinden von Suchen und erzeugen können wir den Aufwand reduzieren. Beispiel: Erfüllbarkeitsproblem Gegeben sei a : seq bool → bool bool, definiert auf Sequenzen der Länge n. Wir nehmen an, dass wir eine Approximationsfunktion fail(a, s) besitzen, die für alle booleschen Sequenzen s mit |s| ≤ n folgende Eigenschaft hat: fail(a, s) ∧ |s ◦ z| = n ⇒ a(s ◦ z) = f alse Folgender Algorithmus nutzt dies aus: 1 2 4 6 8 10 12 14 16 fct suche = (seq bool x) seq bool: if length(x) = n then x else seq bool s = conc (x, <L>) if fail (a, s) then suche (conc (x, <0>)) else seq bool y = suche (s) if a(y) then y else suche (conc (x, <0>)) fi fi fi Dieser Algorithmus setzt voraus, dass das gesuchte Element im Baum ist. Sonst kann über das Prädikat fail die Suche im letzten else – Zweig noch verkürzt werden. 3.3.2 Alpha / Beta – Suche Wir betrachten 2 – Personen – Spiele. Zwei Spieler α und β machen abwechselnd Züge, bis eine Endstellung erreicht ist. Wir nehmen an, dass das Spiel endlich ist, d.h. dass keine unendlichen Zugfolgen existieren. In jeder Endstellung ist ein Gewinn / Verlust für jeden Spieler festgelegt. Wir sind an einem optimalen Spiel interessiert. Wir bilden diese Situation in Informatikstrukturen wie folgt ab: 1. Sorten: • player: Sorte der Spieler (= {α, β}) • position: Sorte der Stellungen 2. Spielzüge: • fct Z = (player, position) set position Z (s, p) beschreibt die Menge der zulässigen Züge für Spieler s in Stellung p. 3. Endstellungen: 302 KAPITEL 3. KOMPLEXITÄTSTHEORIE • fct t = (player, position) bool t (s, p) legt fest, ob für Spieler s die Position p eine Endstellung ist. 4. Gewinn: • fct g = (player, position) bool g (s, p) legt fest, ob die Stellung p als Endstellung einen Gewinn für Spieler s darstellt. 5. Hilfsfunktion: • fct gegner = (player) player Es gilt gegner(α) = β und gegner(β) = α. Wir bezeichnen eine Stellung p als sicher für Spieler s, falls wir bei optimalem Spiel immer gewinnen. Rechenvorschrift: 1 2 4 6 fct sicher = (player s, position p) bool: if t (s, p) then g (s, p) else exists q in Z (s, p): not sicher (gegner (s), q) fi Es gilt (falls ¬t (gegner (s) , q)): ¬ sicher (gegner (s) , q) = ¬∃qi ∈ Z (gegner (s) , q) : ¬ sicher (gegner (gegner (s)) , q 0 ) = ∀q 0 ∈ Z (gegner (s) , q) : sicher (s, q 0 ) 0 Nun betrachten wir ein leicht verändertes Problem, indem wir annehmen, dass in jeder Endstellung p für einen Spieler s durch fct g = (player, position) int festgelegt wird, wie viel der Spieler gewinnt (bzw. verliert). 1 2 4 6 fct opt = (player s, position p) int: if t (s, p) then g (s, p) else max {-opt (gegner (s), q): q in Z (s, p)} fi Jetzt gilt für ¬t (gegner (s) , q): − opt (gegner (s) , q) = − max {− opt (gegner (gegner (s)) , q 0 ) : q 0 ∈ Z (gegner (s) , q)} = min {− opt (s, q 0 ) : q 0 ∈ Z (gegner (s) , q)} Wir betrachten nun Optimierungsmöglichkeiten. Wir unterstellen, dass wir eine Abschätzung für unseren Gewinn opt(s, p) ≤ maxopt(s, p) besitzen (Abschätzung / obere Schranke für den Gewinn). Idee: Wir suchen nun ein Spielergebnis relativ zu vorgegebenen Schranken. Dazu definieren wir eine Sorte eint mit Werten aus Z ∪ {−∞, +∞}. Festlegung: max ∅ = −∞ min ∅ = +∞ 3.3. EFFIZIENTE ALGORITHMEN . . . 303 Sei M eine Menge von Stellungen. Sei a = max{opt(s, p) : p ∈ M }. Wir definieren nun eine neue Funktion fct setopt = (player s, set position p, eint min, max) eint Dabei gelte (es sei min ≤ max) max falls max ≤ a setopt(s, M, min, max) = a falls min ≤ a ≤ max min falls a ≤ min Wir geben einen Algorithmus für setopt an, der effizienter ist als opt. Wichtig: opt(s, p) = setopt(s, {p}, −∞, +∞) 1 2 4 6 8 10 12 14 16 18 20 22 fct setopt = (player s, set position m, eint min, max) eint: if m = emptyset then min else position p = some position q: q in m if maxopt (s, p) <= min then setopt (s, m\{p}, min, max) else eint a = if t (s, p) then g (s, p) else -setopt (gegner(s), Z(s, p), -max, -min) fi if a >= max then max elif a > min then setopt (s, m\{p}, a, max) else setopt (s, m\{p}, min, max) fi fi fi 3.3.3 Dynamisches Programmieren Idee: Optimierung von Algorithmen mit Rekursions – Aufrufbäumen, in denen viele gleiche Aufrufe auftreten: Jeder Aufruf wird nur ein Mal ausgewertet, tabelliert und dann wird jeweils der ermittelte Wert verwendet. Nachteile: • hoher Speicherplatzbedarf • Algorithmen werden kompliziert Beispiel: 1 2 4 6 Handlungsreisenden – Problem fct perm = (seq node s, set node m) set seq node: if m = emptyset then {s} else node x = some node z: z in m; union (i, perm (insert (s, x, i), m\{x}), 0, length (s)) fi 304 KAPITEL 3. KOMPLEXITÄTSTHEORIE mintour (m, i) mintour (m\{i}, k1) ... mintour (m\{i}, kn) ... ... Abbildung 3.1: Rekursionsstruktur von mintour insert (s, x, i) fügt den Knoten x in die Sequenz s nach Position i ein. perm (ε, M) erzeugt die Menge aller Permutationen der Menge M. Es sei eine Abstandsfunktion gegeben: 1 2 fct d = (node i, j) nat: "Abstand zwischen i und j" Aufgabe des Handlungsreisenden: Minimiere sd(s) + d(first(s), last(s)) über s ∈ perm(ε, v) für die Knotenmenge v 6= ∅. 1 2 fct mintour = (set node m, node i: i in m) nat: min {sd (conc (<x0>, s)): s in perm (<>, m) and last (s) = i} mintour (m, i) berechnet von einem gegebenen Anfangsknoten x0 aus alle Wege von x0 nach i über alle Knoten in m, wobei i ∈ m sei. Problem des Handlungsreisenden für Knotenmenge m und Ausgangspunkt x0 ∈ / m: min {mintour(m, x) + d(x, x0 )} x∈m (Länge des kürzesten Wegs von x0 nach x über alle Knoten in m und Rückkehr zu x0 ). Rechenvorschrift für mintour: 1 2 4 6 fct mintour = (set node m, node i: i in m) nat: if |m| = 1 then d (x0, i) else min {mintour (m\{i}, k) + d (k, i): k in m\{i}} fi Dieser Algorithmus hat die Rekursionsstruktur (sei m = {k1 , . . . , kn }) aus Abbildung (3.1). Q Eine Analyse zeigt, dass in jeder Schicht t des Aufrufbaums nur 1≤i≤t n − i + 1 verschiedene Aufrufe auftreten. Dies liefert insgesamt n2 2n verschiedene Aufrufe, d.h. einen Berechnungsaufwand von O(n2 2n ) gegenüber O(n!) beim ursprünglichen Algorithmus. 3.3.4 Greedy – Algorithmen Greedy – Algorithmen arbeiten nach dem Prinzip der lokalen Optimierung. Idee: Wähle den nächsten Schritt (lokal) optimal. Dies führt nicht immer zu einem globalen Optimum. Für gewisse Problemklassen führt lokale Optimierung jedoch zu einer globalen Optimierung. 3.3. EFFIZIENTE ALGORITHMEN . . . 305 Beispiel: Dijkstras Algorithmus der kürzesten Wege Gegeben sind eine Knotenmenge V = {1, . . . , n} und eine Abstandsfunktion d : V × V → N ∪ {∞} Wir suchen die Länge des kürzesten Wegs zwischen zwei Punkten. Wir betten das Problem ein und betrachten folgende Aufgabe: Gegeben: • Knoten x, y • Knotenmenge m ⊆ V Gesucht: kürzester Weg von x nach y x → x1 → x2 → x3 → · · · → xn → y wobei x1 , . . . , xn ∈ m seien. 1 2 4 6 fct dijkstra = (set node m, node x, y) enat: if m = emptyset then d (x, y) else node c = some node z: z in m and p (z) min {dijkstra (m\{c}, x, y), d (x, c) + dijkstra (m\{c}, c, y)} fi Dabei sei p (z) = ∀ node b : b ∈ m ⇒ d(x, z) ≤ d(x, b). Eine effiziente Implementierung dieses Algorithmus arbeitet mit Techniken der dynamischen Programmierung auf einer Matrix, die jeweils die Wege zwischen Knoten speichert, soweit nur Knoten in einer bestimmten vorgegebenen Menge als innere Knoten verwendet werden. 306 KAPITEL 3. KOMPLEXITÄTSTHEORIE Kapitel 4 Effiziente Algorithmen und Datenstrukturen (s. gleichnamiges Buch von Niklas Wirth) Für gegebene Aufgabenstellungen ist es wichtig, geschickt Datenstrukturen zu wählen und Algorithmen, die darauf effizient arbeiten. Wichtig bei der Wahl der Datenstrukturen ist dabei die Frage, welche Zugriffsoperationen möglichst effizient ausgeführt werden sollen. 4.1 Diskussion ausgewählter Algorithmen Sortieren ist eine der wichtigsten Aufgaben in der betriebswirtschaftlichen Informatik. 4.1.1 Komplexität von Sortierverfahren Gängige Sortierverfahren: Insertsort Selectsort Mergesort Quicksort Heapsort Bubblesort Sortieren Sortieren Sortieren Sortieren Sortieren Sortieren durch durch durch durch durch durch Einsortieren Auswählen Mischen Zerteilen Auswahlbäume Vertauschen Aufwand n2 n2 n log n n log n n log n n2 Beispiel: Bubblesort Gegeben sei ein Feld var [1:n] array int a. Folgendes Programm sortiert die Elemente im Feld a aufsteigend. 1 2 4 6 8 var nat i := 1; while i < n do if a[i] > a[i+1] then a[i], a[i+1] := a[i+1], a[i]; if i > 1 then i := i - 1 else i := i + 1 fi 307 308 KAPITEL 4. EFFIZIENTE ALGORITHMEN UND DATENSTRUKTUREN 10 12 else i := i + 1 fi od Wir stellen uns nun die Frage, mit welchem Aufwand wir eine Sequenz sortieren können. Bubblesort hat im Allgemeinen einen Aufwand von n2 , ist also von O(n2 ). Wichtige Unterschiede in der Aufwandsschätzung: • Aufwand im ungünstigsten Fall (worst case) • durchschnittlicher Aufwand (bei gegebener Wahrscheinlichkeitsverteilung auf den Eingaben) Weiterer Aspekt: Speicheraufwand Aufwand von Quicksort: • worst case: n2 • Durchschnitt: n log n Welches Verfahren gewählt wird, hängt stark von Rahmenbedingungen ab: • Größe der Sequenz: Sind die Sequenzen sehr klein, so empfehlen sich einfache, speziell darauf zugeschnittene Verfahren. • Kenntnisse über die Anordnung der Elemente (Wahrscheinlichkeitsverteilung der Eingabe): Sind die Elemente in der zu sortierenden Sequenz nicht zufällig angeordnet, sondern beispielsweise schon weitgehend vorsortiert, so arbeiten bestimmte Verfahren gut, andere extrem schlecht. • Nebenbedingungen technischer Art: Wie sind die Sequenzen gespeichert? Wird im Hauptspeicher (internes Sortieren) oder im Hintergrundspeicher (externes Sortieren) sortiert? 4.1.2 Wege in Graphen Viele praktische Fragestellungen führen auf Probleme, die sich mit Wegen in Graphen beschreiben lassen. Wir behandeln stellvertretend einen Algorithmus zum Finden von Wegen in einem gerichteten Graphen: Warshalls Algorithmus. Gegeben: • Knotenmenge V = {1, . . . , n} • Kanten: c : V × V → B mit c(i, j) ∼ es gibt eine Kante von i nach j. Wir rechnen die transitive Hülle aus: 1 2 fct th = (nat i, j) bool: wth (i, j, n) Idee: wth (i, j, h) bestimmt die Antwort auf die Frage von i nach j über innere Knoten ≤ k? 4 6 8 existiert ein Weg in c fct wth = (nat i, j, k) bool: if k = 0 then c (i, j) else wth (i, j, k-1) or (wth (i, k, k-1) and wth (k, j, k-1)) fi 4.2. BÄUME 309 Diese Idee können wir in einen Algorithmus umwandeln, der auf einem zweidimensionalen Feld a (Adjazenzmatrix) arbeitet: {∀i, j : 1 ≤ i, j ≤ n ⇒ a[i, j] = c(i, j)} 1 2 4 6 for k := 1 to n do for i := 1 to n do for j := 1 to n do a[i,j] := a[i,j] or (a[i,k] and a[k,j]) od od od {∀i, j : 1 ≤ i, j ≤ n : a[i, j] = wth(i, j, n)} Komplexität: n3 4.2 Bäume Baumstrukturen sind ein Kernthema der Informatik. 4.2.1 Geordnete, orientierte, sortierte Bäume Es existieren zwei Blickweisen auf Bäume: 1. Bäume als Datenstrukturen mit rekursivem Aufbau und Zugriffsfunktionen 2. Bäume als Graphen Zu Bäumen als Graphen: Ein ungerichteter Graph G heißt nichtorientierter Baum, falls G • zyklenfrei und • zusammenhängend ist. Ein gerichteter Graph heißt orientierter Baum, falls • eine Wurzel k existiert (ein Knoten k) und • von k aus alle anderen Knoten auf genau einem Weg erreichbar sind. Ein geordneter Baum ist ein orientierter Baum, bei dem für jeden Knoten eine Reihenfolge für seine Teilbäume gegeben ist. sort ordtree = ordtree (m root, seq ordtree subtrees) Der Verzweigungsgrad ist die (maximale) Anzahl der Teilbäume pro Knoten. In einem vollständigen Baum haben alle Wege (zu Blättern von der Wurzel) die selbe Länge. 4.2.2 Darstellung von Bäumen durch Felder Üblicherweise werden Bäume durch verkettete Zeigerstrukturen in Rechnern dargestellt. Wir können vollständige Bäume auch statisch in Feldern ablegen. a Darstellung im Feld: [a, b, d, e, c, f, g] b c d ef g 310 KAPITEL 4. EFFIZIENTE ALGORITHMEN UND DATENSTRUKTUREN m1 m2 . . . . . . . . . mk . . . . . . Abbildung 4.1: B – Baum 4.2.3 AVL – Bäume Beim Arbeiten mit Bäumen sind wir am kurzen (effizienten) Zugriff auf die Elemente interessiert. Dies ist nur gewährleistet, wenn die Bäume nicht entarten, d.h. wenn die Höhe des Baumes logarithmisch mit der Anzahl der Knoten im Baum wächst. Dies könnte auf die Forderung hinauslaufen, dass alle Wege von der Wurzel zu den Blättern sich höchstens um 1 in der Länge unterscheiden. Allerdings wird es sehr aufwändig, beim Einfügen oder Löschen von Knoten die Ausgewogenheit zu erhalten. Ausweg: Statt völliger Ausgewogenheit fordern wir, dass sich die Höhe des linken und rechten Teilbaumes um maximal 1 unterscheiden. AVL – Baum: Sortierter Binärbaum, bei dem sich in allen Knoten die Höhen der Teilbäume nur um maximal 1 unterscheiden. Sortiert bedeutet dabei: Für alle Teilbäume gilt: Die Einträge im linken Teilbaum sind kleiner oder gleich der Wurzel und die Wurzel ist kleiner oder gleich allen Einträgen im rechten Teilbaum. Vorteil: Gesteuerte Suche ist möglich. Wichtig: Bei AVL – Bäumen können Knoten mit geringem Aufwand eingefügt oder gelöscht werden. 4.2.4 B – Bäume B – Bäume sind die Grundlage für die Speicherung von Informationen in Datenbanken. Sie wurden 1970 von R. Bayer vorgeschlagen. Sei n ∈ N eine gegebene Zahl. Ein B – Baum über einer linear geordneten Menge M hat die Gestalt aus Abb. 4.1. Ein B – Baum ist leer oder enthält zwischen n und 2n Teilbäume: n − 1 ≤ k ≤ 2n − 1. Ein B – Baum ist sortiert. Vorteile: 1. Es kann gezielt gesucht werden. 2. Die Höhe (Anzahl der Suchschritte) ist extrem klein. (Beim Speichern der B – Bäume auf Hintergrundmedien sind nur wenige Seitenzugriffe erforderlich.) 3. Einfügen und Löschen von Elementen ist extrem billig. Bemerkung: Die Wurzel des B – Baums kann weniger als n Einträge enthalten. In B – Bäumen haben alle Wege von der Wurzel zu Blättern die gleiche Länge. 4.3 Effiziente Speicherung großer Datenmengen Die Datenstruktur der (endlichen) Mengen wird in den meisten Programmiersprachen nicht direkt unterstützt. Im Folgenden betrachten wir Datenstrukturen für die effiziente Behandlung großer Mengen. 4.3. EFFIZIENTE SPEICHERUNG GROSSER DATENMENGEN 4.3.1 311 Rechenstruktur der Mengen (mit Zugriff über Schlüssel) Wir nehmen an, dass wir Datensätze der Sorte data speichern wollen. Für jeden Datensatz setzen wir einen Schlüssel voraus: fct key = (data) key Wir wollen eine endliche Menge von Daten speichern und über Schlüssel darauf zugreifen. Die Sorte store bezeichne die Menge der endlichen Mengen von Daten. Zunächst interessiert uns die Struktur der Elemente der Sorte store nicht, sondern die Operationen, die wir zur Verfügung haben: 1 2 4 fct fct fct fct emptystore = store get = (store, key) data insert = (store, data) store delete = (store, key) store Logische Festlegung der Wirkungsweise dieser Funktionen (Schnittstellenverhalten): k = key(d) ⇒ get (insert (s, d) , k) = d k 6= key(d) ⇒ get (insert (s, d) , k) = get(s, k) delete (emptystore, k) = emptystore k = key(d) ⇒ delete (insert (s, d) , k) = delete(s, k) k 6= key(d) ⇒ delete (insert (s, d) , k) = insert (delete(s, k), d) Zusätzliche Operationen: 6 fct isempty = (store) bool fct isentry = (store, key) bool isentry(emptystore, k) = false isentry(insert(s, d), k) = (k = key(d) ∨ isentry(s, k)) 4.3.2 Mengendarstellung durch AVL – Bäume Wir stellen die Elemente der Sorte store durch AVL – Bäume dar: sort store = tree data Damit sind bestimmte Funktionen wie root, left, right auf store vorgegeben. Die weiteren (eigentlichen) Funktionen auf store programmieren wir nun, abgestützt auf diese Grundfunktionen. 1 2 4 6 8 10 12 14 fct get = (store s, key k) data: if key (root (s)) = k then root (s) elif k < key (root (s)) then get (left (s), k) else get (right (s), k) fi fct insert = (store s, data d) store: if s = emptytree then cons (emptytree, d, emptytree) elif key (d) = key (root (s)) then cons (left (s), d, right (s)) elif key (d) < key (root (s)) then 312 KAPITEL 4. EFFIZIENTE ALGORITHMEN UND DATENSTRUKTUREN 16 18 cons (insert (left (s), d), root (s), right (s)) else cons (left (s), root (s), insert (right (s), d)) fi Achtung: insert liefert einen sortierten, aber nicht notwendigerweise balancierten Baum — selbst, wenn s balanciert ist. 1 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 fct balinsert = (store a, data d) store: if a = emptytree then cons (emptytree, d, emptytree) elif key (d) = key (root (a)) then cons (left (a), d, right (a)) elif key (d) < key (root (a)) then store b = balinsert (left (a), d); if hi (b) > 1 + hi (right (a)) then leftbalance (b, root (a), right (a)) else cons (b, root (a), right (a)) fi else store b = balinsert (right (a), d); if hi (b) > 1 + hi (left (a)) then rightbalance (left (a), root (a), b) else cons (left (a), root (a), b) fi fi fct leftbalance = (store b, data aw, store ar) store: if hi (left (b)) >= hi (right (b)) then cons (left (b), root (b), cons (right (b, aw, ar))) else cons (cons (left (b), root (b), left (right (b))), root (right (b)), cons (right (right (b)), aw, ar)) fi fct rightbalance = (store al, data aw, store b) store: if hi (right (b)) >= hi (left (b)) then cons (cons (al, aw, left (b)), root (b), right (b)) else cons (cons (al, aw, left (left (b))), root (left (b)), cons (right (left (b)), root (b), right (b))) fi fct baldelete = (store a, key k) store: if a = emptytree then emptytree elif key (root (a)) = k then delete_root (a) elif k < key (root (a)) then store b = baldelete (left (a), k); if 1 + hi (b) < hi (right (a)) then rightbalance (b, root (a), right (a)) else 4.3. EFFIZIENTE SPEICHERUNG GROSSER DATENMENGEN 48 50 52 54 56 313 cons (b, root (a), right (a)) fi else store b = baldelete (right (a), k); if 1 + hi (b) < hi (l (a)) then leftbalance (left (a), root (a), b) else cons (left (a), root (a), b) fi fi 58 60 62 64 66 68 70 72 74 76 78 fct delete_root = (store a) store: if left (a) = emptytree then right (a) elif right (a) = emptytree then left (a) else data e = greatest (left (a)); store b = baldelete (left (a), key (e)) if 1 + hi (b) < hi (right (a)) then rightbalance (b, e, right (a)) else cons (b, e, right (a)) fi fi fct greatest = (store a) data: if right (a) = emptytree then root (a) else greatest (right (a)) fi Das Beispiel der AVL – Bäume ist typisch für Datenstrukturen, die einer größeren Grundsorte entstammen (im Beispiel Binärbäume), aber noch zusätzliche Eigenschaften (im Beispiel Balanciertheit und Sortiertheit) aufweisen. Diese Eigenschaften heißen auch Datenstrukturinvarianten. Prinzip einer Datenstrukturinvarianten: Alle zur Verfügung gestellten Operationen haben die Eigenschaft, dass — falls die Invariante für die Eingangsparameter gilt — diese auch für das Ergebnis gilt. Diese Version von AVL – Bäumen hat linearen Aufwand (O(n), wobei n die Anzahl der Elemente im Baum ist). Idee: Wir speichern im Baum zusätzlich zur jeweiligen Wurzel die Höhe. Ergebnis: O(log(n)) – Algorithmen für das Einfügen und Löschen. Sortendeklaration: sort btree = cons (btree left, data root, btree right, nat hi) Weitere Optimierung: Statt der Höhe speichern wir eine Zahl aus {−1, 0, 1} für linker Teilbaum höher / ausgewogen / rechter Teilbaum höher. Beim Einfügen gilt: Wenn ein Mal balanciert wird, hat der entstehende Baum die gleiche Höhe wie der ursprüngliche Baum vor dem Einfügen. Dies zeigt, dass höchstens ein Balancierschritt pro Einfügung erforderlich ist. Dieses Argument gilt nicht für das Löschen. Hier kann im ungünstigsten Fall in jedem rekursiven Aufruf ein Balancierschritt auftreten. 314 KAPITEL 4. EFFIZIENTE ALGORITHMEN UND DATENSTRUKTUREN 4.3.3 Streuspeicherverfahren Streuspeicherverfahren (hashing) erlauben die Speicherung von Daten unter ihren Schlüsseln in einem linearen Feld der Länge z (wobei z deutlich kleiner ist als die Anzahl der verschiedenen Schlüssel). Wir benötigen eine Streufunktion h : key → [0, z − 1] Die benötigten Operationen emptystore, get, insert, delete werden nun für ein Feld realisiert: sort store = [0:z-1] array data Dabei nehmen wir an, dass data ein Element empty umfasst, das als Platzhalter dient für Feldelemente, die nicht belegt sind. emptystore entspricht dem Feld, in dem alle Einträge den Wert empty haben. Um ein neues Element d in die Streuspeichertabelle (hashtable) einzutragen, berechnen wir zum Schlüssel key(d) den Index h (key (d)). Falls das entsprechende Feldelement leer ist, erfolgt der Eintrag; falls nicht, sprechen wir von einer Kollision und nutzen ein Verfahren zur Kollisionsauflösung. Aufgaben bei Streuspeichertabellen: • Größe des Feldes festlegen • Wahl der Streufunktion • Bestimmung eines Verfahrens zur Kollisionsauflösung Erfahrungswerte: • Tabellengröße so wählen, dass die Tabelle höchstens zu 90% gefüllt ist. • Streufunktion so wählen, dass sie gut streut (starke Abhängigkeit von Daten und Häufigkeit von Daten). Einfache Wahl (seien die Schlüssel Zahlen): h(i) = i mod z. Dabei sollte z als Primzahl gewählt werden. • Folgende Verfahren zur Kollisionsauflösung: – offene Adressierung: Bei Kollisionen wird das kollidierende Element ebenfalls im Feld gespeichert. – geschlossene Adressierung: Die Elemente werden in einem gesonderten Bereich gespeichert. Das Suchen eines freien Platzes bei offener Adressierung nennen wir Sondieren. • Lineares Sondieren besteht darin, dass wir in gleich langen Schritten nach einem freien Platz suchen: h(k), h(k) + j, h(k) + 2j, . . . • quadratisches Sondieren: h(k), h(k) + 1, h(k) + 4, h(k) + 9, . . . (natürlich jeweils mod z). Erfahrungswert: Bei 90% Füllung sind im Mittel 2, 56 Sondierungsschritte erforderlich.