Westfälische Wilhelms-Universität Münster Ausarbeitung Graphalgorithmen Im Rahmen des Seminars Parallele Programmierung im SS 2003 Jochen Olejnik Themensteller: Prof. Dr. Herbert Kuchen Betreuer: Prof. Dr. Herbert Kuchen Institut für Wirtschaftsinformatik Praktische Informatik in der Wirtschaft Inhaltsverzeichnis 1 Einleitung................................................................................................................... 1 2 Verbundene Komponenten ........................................................................................ 1 3 2.1 Suchalgorithmen ............................................................................................... 1 2.2 Transitive Hülle ................................................................................................ 2 2.3 Hirschbergs Algorithmus.................................................................................. 2 2.3.1 Die Idee......................................................................................................... 2 2.3.2 Parallelisierung ............................................................................................. 2 2.3.3 Komplexität .................................................................................................. 3 Single Source Shortest Path....................................................................................... 6 3.1 4 Moores Algorithmus ......................................................................................... 6 3.1.1 Die Idee......................................................................................................... 6 3.1.2 Parallelisierung ............................................................................................. 8 3.1.3 Terminierungsüberlegungen ....................................................................... 10 3.1.4 Probleme ..................................................................................................... 10 Minimum Cost Spanning Tree................................................................................. 12 4.1 Sollins Algorithmus ........................................................................................ 12 4.1.1 Die Idee....................................................................................................... 12 4.1.2 Parallelisierung ........................................................................................... 13 4.1.3 Komplexität ................................................................................................ 14 4.2 Kruskal............................................................................................................ 15 4.2.1 Die Idee....................................................................................................... 15 4.2.2 Der Heap als Datenstruktur......................................................................... 15 4.2.3 Heapaufbau ................................................................................................. 16 4.2.4 Der parallele Algorithmus .......................................................................... 17 5 Schlusswort.............................................................................................................. 17 6 Literaturverzeichnis ................................................................................................. 18 II Kapitel 1: Einleitung 1 Einleitung Viele Probleme der Praxis lassen sich durch Graphen beschreiben. Als Beispiele seien hier die optimale Anordnung von Chips auf einer Rechnerplatine, das TravelingSalesman-Problem sowie diverse andere Wegfindungsprobleme und das kostengünstigste Verlegen von Versorgungsleitungen genannt. Diese Probleme werden im Alltag immer komplexer, wodurch herkömmliche Algorithmen an ihre Grenzen stoßen. Es wurde versucht, bereits bekannte Algorithmen für die wichtigsten Problemgruppen auf Parallelrechner anzupassen, um dem steigenden Rechenbedarf entgegenzutreten. In den einzelnen Kapiteln dieser Ausarbeitung werden nun beispielhaft parallele Lösungen für die wichtigsten Graphenprobleme vorgestellt. Für das Verbundene-Komponenten-Problem wird Hirschbergs Algorithmus näher betrachtet. Danach wird dann ein Lösungsansatz des Single-Source-Shortest-Path Problems anhand des Algorithmus von Moore präsentiert. Das 4. Kapitel beschäftigt sich schließlich mit den Algorithmen von Sollin und Kruskal, welche dem Minimum-Cost-Spanning-Tree Problem zuzuordnen sind. Zunächst wird in den Kapiteln auf die Funktionsweise der einzelnen Algorithmen eingegangen. Danach werden deren Laufzeit und einige Implementierungsbeispiele genauer unter die Lupe genommen. Es wird auch auf Probleme eingegangen, die durch die Parallelisierung von ursprünglich sequentiellen Algorithmen entstehen können. 2 Verbundene Komponenten Beim Verbundene Komponenten Problem geht es darum, alle verbundenen Knoten eines Graphen zu finden. Für verbundene ungerichtete Graphen gibt es dafür 3 Lösungsansätze. 2.1 Suchalgorithmen Wenn man mittels Tiefen- oder Breitensuche den kompletten Graphen durchsucht, dann löst man dies Problem automatisch. 1 Kapitel 2: Single Source Shorest Path 2.2 Transitive Hülle Hat man die Adjazenzmatrix eines Graphen, so beschreibt die transitive Hülle alle verbundenen Komponenten. Man berechnet die transitive Hülle indem man auf die Adjazenzmatrix log n Plus-Min-Multiplikationen anwendet. Eine Plus-Min-Multiplikation ist eine Matrixmultiplikation bei der Skalarmultiplikationen durch Skalaradditionen und Additionen durch die Minimumoperation ersetzt werden. Durch diese Strukturanalogie ( ) hat dieser Lösungsansatz die Komplexität Θ n log 2 n , da n Plus-Min-Multiplikationen ( ) durchgeführt werden und eine solche Multiplikation die Komplexität Θ log 2 n hat. [Dekel et al.]. 2.3 Hirschbergs Algorithmus 2.3.1 Die Idee Die Grundidee hinter Hirschbergs Algorithmus ist, dass Knoten zu Knotengruppen zusammengefasst werden, bis alle verbundenen Knoten gefunden sind. Dabei gehört jeder Knoten zu genau einer Knotengruppe, welche am Anfang nur aus ihm selbst besteht. Eine Knotengruppe wird durch ihre Wurzel (hier: ihr kleinstes Element) identifiziert. Die zugrundeliegende Datenstruktur ist die Adjazenzmatrix. 2.3.2 Parallelisierung Der parallele Algorithmus durchläuft in jeder Iteration folgende 3 Schritte: 1. Schritt: Zu jedem Knoten wird die angrenzende Knotengruppe mit der kleinsten Wurzel gesucht. 2. Schritt: Die Wurzeln der Knotengruppen, die sich in Schritt 1 gefunden haben, werden verbunden. 3. Schritt: Alle so verbundenen Knotengruppen werden zu jeweils einer Gruppe zusammengefasst. 2 Kapitel 2: Single Source Shorest Path Die Startsituation: 1 4 9 2 10 11 Knoten 1 2 3 4 5 6 7 8 9 10 11 7 8 3 6 5 Knoten1 2 3 4 5 6 7 8 9 10 11 gruppe 1. Iteration 2. Iteration Schritt 1: Schritt 1: 1 4 9 2 7 8 3 6 10 11 1 4 9 2 5 7 8 3 6 10 11 5 Schritt 2: Schritt 2: 1 4 9 2 7 8 3 6 10 11 1 4 9 2 5 7 8 3 6 10 11 5 Schritt 3: Schritt 3: 1 4 9 2 7 8 3 6 10 11 1 4 9 2 5 7 8 3 6 10 11 5 Ergebnis: Ergebnis: Knoten 1 2 3 4 5 6 7 8 9 10 11 Knoten1 2 2 1 5 2 1 2 2 5 5 gruppe Knoten 1 2 3 4 5 6 7 8 9 10 11 Knoten1 1 1 1 5 1 1 1 1 gruppe 5 5 Abbildung 1: Hirschbergs Algorithmus, ein Beispiel 2.3.3 Komplexität Der Algorithmus benötigt log n Iterationen, weil sich die Anzahl der Knotengruppen pro Iteration mindestens halbiert. Dazu werden n2 Prozessoren benötigt, da maximal n benachbarte Knotengruppen pro Knoten verglichen werden müssen. Die Anzahl der ( ) 2 Prozessoren sowie die Laufzeit von Θ log n lassen sich mit ein paar Zusatzüberle- gungen verbessern: 3 Kapitel 2: Single Source Shorest Path • Nutzt man Brents Theorem aus, kann man mit n log n Prozessoren n Variablen an- sprechen und deren Minimum finden, weil jeder Prozessor im 1. Schritt log n Elemente auf einmal ansprechen kann und nicht nur eines. Des Weiteren kann jeder Prozessor im 2. Schritt das Minimum aus log n Elementen auf einmal finden, ohne die Komplexität des Algorithmus zu erhöhen. Es reichen also n n Prozessoren aus um dieses log n 2 Problem in Θ log n Zeit zu lösen. [Chin, Lam, Chen] ( • ) Wenn man mit einbezieht, dass in jeder Iteration die Betrachtung der Wurzeln der Knotengruppen ausreicht und isolierte Bäume nicht mehr weiter betrachtet werden ( ) müssen, so verringert sich die benötigte Anzahl der Prozessoren auf Θ logn2 n bei ( ) Θ log2 n Zeit. [Chin, Lam, Chen] • Nassimi und Sahni haben Hirschbergs Algorithmus auf dem q-dimensional-mesh connected-SIMD-Modell implementiert. Für den Fall des 2-dimensionalen Gitters wird nun auf eine Beispielimplementierung eingegangen. Zunächst müssen drei neue Befehle eingeführt werden. Der Befehl random.read(a,[b]c) liest den Wert der Variablen c im Bereich von Prozess b in die Variable a ein. Der Befehl random.write(a,[b]c) schreibt den Wert der Variablen a in die Variable c im Bereich von Prozess b. Die Komplexität dieser beiden Operationen ist auf dem betrachteten Modell O(q2p), wobei q die Dimension des Prozessornetzes und p die Anzahl der Prozessoren in einer Dimension beschreibt [DaBr]. Da sowohl q als auch p nur vom verwendeten System abhängig und somit extern gegeben sind, kann deren Laufzeit bei der Laufzeitbetrachtung als konstant angesehen werden. Diese Implementierung benutzt zusätzlich noch die Funktion bits(i,j,k). Dabei werden die Stellen von j bis k von der Binärdarstellung der Zahl i betrachtet und deren Wert zurückgegeben. Z. B. gibt Bits(10,3,2) =2 zurück. Die Binärdarstellung von 10 lautet 1010. Nimmt man nun die Bits, zwischen Stelle j und k (man fängt bei 0 an zu zählen), so erhält man hier binär 10, also die Dezimale 2. Ist j<k, so wird 0 zurückgegeben. 4 Kapitel 2: Single Source Shorest Path public VERBUNDENE KOMPONENTEN(): Parameter: d //maximaler Knotengrad n //Anzahl Knoten = Anzahl Prozessoren Globale Variablen: k //betrachtete Kante interation //Nummer der Iteration Lokale Variablen: kandidat //Wurzel des Nachbarknotens nachbar[1...d] //Nachbarknoten der aktuellen Knotengruppe w //Wurzel 1. begin 2. for (alle Pi where 1 ≤ i ≤ n ){ 3. w = i; jeder Knoten wird Wurzel 4. } 5. for (iteration = 0 to log n − 1 ){ 6. for (alle Pi where 1 ≤ i ≤ n ){ es gibt keine bekannten Nachbarn 7. kandidat=∞; =>Entfernung zu Nachbarn wird auf 8. } ∞ gesetzt 9. for (k=1 to d){ 10. for (alle Pi where 1 ≤ i ≤ n ){ Suchen des Nach11. HOLEN.UND.VERGLEICHEN(nachbar[k], w, kandidat); barknotens mit 12. } der kleinsten 13. } Wurzel 14. for (alle Pi where 1 ≤ i ≤ n ){ 15. AKTUALISIERE.WURZEL(kandidat, w); 16. } 17. ZUSAMMENFASSEN(w, n); //Zusammenfügen der Knotengruppen 18. } 19. end private HOLEN.UND.VERGLEICHEN(v, w, kandidat); //entspricht 1. Schritt übergebene Werte: v //Nachbarknoten w, kandidat lokale Variablen: tmp //Wurzel des Nachbarknotens 20. begin 21. random.read(tmp, [v]w); 22. if (tmp==w){ 23. tmp=∞; Prüfen, ob v schon kandidat als Wurzel 24. } hat 25. kandidat=min(kandidat, tmp); 26. end private AKTUALISIERE.WURZEL(kandidat, w); //entspricht 2. Schritt übergebene Werte: kandidat, w 27. begin 28. random.write(kandidat[w]w); 29. if (w==∞){ hat diese Knotengruppe überhaupt 30. w=i; Nachbarn? 31. } 32. if (w>i){ 33. random.read(w, [w]w); gibt es einen Zyklus? 34. } 35. end 5 Kapitel 2: Single Source Shorest Path private ZUSAMMENFASSEN(w, n); //entspricht 3. Schritt übergebene Werte: w, n 36. begin 37. for (b=1 to log(n)){ 38. for (alle Pi){ 39. if (bits(w, log(n)-1, b)==bits(i, log(n), b)){ 40. random.read(w, [w]w); 41. } 42. } 43. } 44. end Abbildung 2: Hirschbergs Algorithmus in Pseudocode • Der Algorithmus hat bei obiger Implementierung auf einem 2D-mesh-SIMDRechner mit n Prozessoren und n=2k Knoten mit Maximalwert von d eine Komplexität von O(dn log n) [Nassimi, Sahni]. Der äußere Loop (Zeilen 5-18) wird log n mal durchlaufen. Im inneren Loop (Zeilen 9-13) wird d mal die Minimumoperation angewendet, welche O(n) Zeit verbraucht. 3 Single Source Shortest Path Beim Single Source Shortest Path Problem besteht die Aufgabe darin, zu einem Knoten den jeweils kürzesten Pfad zu jedem anderen Knoten in einem gewichteten, gerichteten Graphen zu finden. 3.1 Moores Algorithmus 3.1.1 Die Idee Zur Vorbereitung wird die Entfernung vom Startknoten S zu den jeweils anderen Knoten auf ∞ und zu sich selbst auf 0 gesetzt. Es gibt eine Queue, in der die Knoten aufgeführt sind, die als nächstes betrachtet werden sollen. Anfangs befindet sich hier nur der Startknoten . Nun wird das oberste Element der Queue entfernt. Für alle Verbindungen vom Startknoten zu anderen Knoten wird überprüft, ob es kürzere Wege über den zu überprüfenden Knoten und dessen ausgehende Kanten gibt. Ist dies der Fall, so wird die entsprechende Verbindung aktualisiert und deren Zielknoten an das Ende der Queue eingefügt, wenn er nicht bereits in der Queue enthalten ist. 6 Kapitel 3: Minimum Cost Spanning Tree Der Algorithmus läuft nun so lange weiter, bis die Queue leer ist. Hier ein Beispiel: Entf. Q Schritt 2 A 0 A 2 2 F E F E B ∞ C ∞ D D 12 12 2 1 2 1 3 3 D ∞ 2 2 E ∞ A B C A B C 6 6 F ∞ Entf. Q Schritt 4 Schritt 3 A 0 C 2 2 F E F E B 2 D C 8 F D D 12 12 2 1 2 1 D 5 3 3 2 2 E ∞ A B C A B C 6 6 F 14 Entf. Q Schritt 6 Schritt 5 A 0 F 2 2 F E F E B 2 E C 7 C D D 12 12 2 1 2 1 3 3 D 5 2 2 E 9 A B C A B C 6 6 F 14 Entf. Q Schritt 8 Schritt 7 A 0 C 2 2 F E F E B 2 F C 7 D D 12 12 2 1 D 5 2 1 3 3 2 2 E 9 A B C A B C 6 6 F 11 Entf. Q Schritt 10 Schritt 9 A 0 E 2 2 F E F E B 2 C 7 D D 12 12 2 1 2 1 3 3 D 5 2 2 E 8 A B C A B C 6 6 F 11 Entf. Q Hinweis: Schritt 11 A 0 2 F E Entf. = Entfernungstabelle B 2 C 7 D 12 2 1 3 Q = Queue D 5 2 E 8 A B C 6 F 10 Abbildung 3: Der sequentielle Algorithmus von Moore Schritt 1 Entf. A 0 B 2 C ∞ D ∞ E ∞ F ∞ Entf. A 0 B 2 C 8 D 5 E 9 F 14 Entf. A 0 B 2 C 7 D 5 E 9 F 14 Entf. A 0 B 2 C 7 D 5 E 8 F 11 Entf. A 0 B 2 C 7 D 5 E 8 F 10 Q B Q D F E Q E C Q F E Q F 7 Kapitel 3: Minimum Cost Spanning Tree public SHORTEST PATH(): Globale Variablen: entfernung [I] //Entfernung von S zu I S //Startknoten wert[U,V] //Wert der Kante zwischen U und V Parameter: n //Anzahl Knoten im Graph 1. begin 2. for (i=1 to n) { 3. initialisiere(i); 4. } 5. enqueue(S); 6. while (queue ist nicht leer){ 7. suche(); 8. } 9. end private SUCHE(): Lokal Variablen: neue.entfernung U V //Entfernung zu V über U //Startknoten der betrachteten Kante //Endknoten der betrachteten Kante 10. begin 11. dequeue(U); 12. for (jede Kante {U,V} im Graphen){ 13. neue.entfernung = entfernung[U] + wert[U,V]; 14. if (neue.entfernung < entfernung[V]){ 15. entfernung[V] = neue.entfernung; 16. if (V ist nicht in der queue){ 17. enqueue(V); 18. } 19. } 20. } Überprüfen ob es durch neue Kante kürzere Entfernungen gibt end Abbildung 4: Moores sequentieller Algorithmus in Pseudocode 3.1.2 Parallelisierung Es gibt zwei mögliche Ansätze diesen Algorithmus zu parallelisieren. Der Erste sieht die parallele Bearbeitung aller ausgehenden Kanten eines zu überprüfenden Knotens vor (Zeilen 12-20), während der Zweite mehrere Knoten der Queue auf einmal überprüft (Zeilen 6-8). Unabhängig von der Wahl des Ansatzes kann die Initialisierung (Zeilen 24) durch Prescheduling parallelisiert werden. Der zweite Ansatz ist vorzuziehen, da das Problem hierdurch in größere Einheiten aufgespaltet wird und die Performanz des 1. Ansatzes sehr stark von der Anzahl der ausgehenden Knoten des Graphen abhängt. Nachdem die Queue mit dem Startknoten initialisiert wurde, werden eine Reihe von asynchronen Prozessen generiert, welche jeweils einen Knoten aus der Queue löschen, dessen ausgehende Kanten betrachten und Knoten mit kürzeren Pfaden in die Queue einfügen. 8 Kapitel 3: Minimum Cost Spanning Tree public SHORTEST PATH(): Globale Variablen: entfernung [I] //Entfernung von S zu I S //Startknoten wert[U,V] //Wert der Kante zwischen U und V halt //true wenn Algorithmus beendet wartend[i] //true, wenn Prozess i wartet Parameter: n //Anzahl Knoten im Graph p //Anzahl Prozessoren 1. begin 2. for (alle Pi where 1 ≤ i ≤ p ){ 3. for (j = i to n step p) { 4. initialisiere(j); 5. } 6. } 7. enqueue(S); 8. halt = false; 9. for (alle Pi where 1 ≤ i ≤ p ){ 10. repeat {suche(i);} until (halt == true); 11. } 12. end private SUCHE(i): Lokal Variablen: neue.entfernung //Entfernung zu V über U U //Startknoten der betrachteten Kante V //Endknoten der betrachteten Kante Übergebene Variablen: i //Prozessnummer 1. begin 2. sperre (queue); 3. if (queue ist leer){ 4. wartend[i] = true; 5. if (i = 1 && wartend[1] && wartend[2] &&...&& wartend[p]){ 6. halt = true; //Terminierungsbedingung 7. } 8. entsperre (queue); 9. } 10. else { 11. dequeue(U); 12. wartend[i] = false; 13. entsperre (queue); 14. for (jede Kante {U,V} im Graphen){ 15. neue.entfernung = entfernung[U] + wert[U,V]; 16. sperre (entfernung[V]); 17. if (neue.entfernung < entfernung[V]){ 18. entfernung[V] = neue.entfernung; 19. entsperre (entfernung[V]); 20. if (V ist nicht in der queue){ 21. sperre (queue); 22. if (V ist nicht in der queue){ 23. enqueue(V); 24. } 25. } 26. entsperre (queue); 27. } 28. else {entsperre (entfernung[V]);}; 29. } 30. } 31. end Abbildung 5: parallele Version von Moores Algorithmus 9 Kapitel 3: Minimum Cost Spanning Tree 3.1.3 Terminierungsüberlegungen Da man oben einige asynchrone Prozesse initialisiert, kann man nicht davon ausgehen, dass der Algorithmus automatisch terminiert, wenn die Queue leer ist. Um festzustellen, ob die Queue leer ist und gerade keine Knoten bearbeitet werden, führt man zwei neue Variablen ein. Die Erste ist das array waiting[], in welchem für jeden Prozessor eine boolesche Variable gespeichert ist, die den Wert false hat, wenn der Prozessor gerade arbeitet. Des Weiteren wird die boolesche Variable halt eingeführt, welche true ist, wenn alle Prozessoren warten, also wenn alle Werte in waiting[] true sind und die Queue leer ist. Dies überprüft Prozess 1 jedes mal, wenn er mit einem Prozess fertig ist und eine leere Queue vorfindet (Zeilen 3-7 / Abb.5). Ist dies der Fall so setzt er halt auf true, was den Algorithmus terminiert. Anfangs werden alle Werte von waiting[], sowie der Wert von halt auf false initialisiert. 3.1.4 Probleme Enqeue und Dequeue sind keine atomaren Operationen, deshalb kann es zu Fehlern kommen, wenn während dieser Operationen die Queue verändert wird. Für die Zeit dieser Operationen benötigt der betreffende Prozess also exklusiven Zugriff auf die Queue. Die Queue muss zusätzlich noch während des Vergleiches (Zeilen 13-15 / Abb.4) für andere Prozesse gesperrt werden, um zu verhindern, dass zwei Prozesse gleichzeitig den selben Wert aktualisieren und somit der erste geschriebene Wert überschrieben wird. Schließlich muss die Queue noch gesperrt werden, während Prozess 1 überprüft, ob alle Prozesse warten. Durch das häufige Sperren der Queue wird das Beschleunigungspotential durch die Parallelisierung stark eingeschränkt. Dieses Problem lässt sich umgehen, indem jeder Prozess seine eigene private Queue bekommt, was jedoch zu einer ungleichen Auslastung der einzelnen Prozessoren führen kann. Eine Kompromisslösung sieht vor, dass jeder Prozess Elemente in seinen privaten Bereich einfügen darf. Dann werden diese Listen zusammengefügt und jeder der p Prozesse löscht und bearbeitet genau jedes p-te Element dieser großen Liste. Als Datenstruktur für diese Liste bietet sich ein Linked-Array an, da man in diese Datenstruktur sehr einfach unterschiedlich große Elementketten einfügen kann. Im Folgenden wird das Linked-Array genauer beschrieben: 10 Kapitel 3: Minimum Cost Spanning Tree Es ist p(w+p) Elemente groß, wobei w die maximal von einem Prozess in einer Iteration einzufügenden Elemente beschreibt. Jeder Prozessor hat also einen Bereich der Größe w+p, in dem er seine Elemente ablegen kann. Die Namen der von Prozessor i ( 1 ≤ i ≤ p ) erzeugten Knoten ei, die in der nächsten Iteration untersucht werden sollen, stehen im Speicherbereich von (i-1)(w+p)+1 (erster Wert, den Prozessor i einfügt) bis (i-1)(w+p)+ei (letzter Wert, den Prozessor i einfügt). Die Elemente mit den Adressen (i1)(w+p)+ei +1 bis (i-1)(w+p)+ei +p (der Teil des Adressraumes der nicht mit zu berechnenden Werten belegt wird) enthalten die Werte (Zeiger) –i(w+p+1) bis –[i(w+p)+p]. Ein Beispiel: p=4, w=3 => Das Array hat die Länge 28 Prozessor 1 fügt zwei Elemente in die Queue ein, Prozessor 2 drei, Prozessor 3 zwei und Prozessor 4 ein Element. Bereich Prozessor 1 Bereich Prozessor 2 Bereich Prozessor 3 Bereich Prozessor 4 5 10 15 20 25 e e 8 9 10 11 e e e 15 16 17 18 e e 22 23 24 25 e 29 30 31 32 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 Die doppelt umrandete Zeile stellt das Array da. Die grünen Kästchen stehen für die Felder, in denen zu berechnende Werte e stehen, die Blauen sind Pointer und in der Implementierung eigentlich negativ um sie von den Werten zu unterscheiden. Die untere Zahl beschreibt, welcher Prozess welche Felder abgeht. So bearbeitet Prozess 1 beispielsweise die Elemente 1,5,10,14,18 und 23. Der Zeiger in Feld 23 zeigt auf Feld 29, daher merkt der Prozess, dass er am Ende des Arrays angelangt ist. Abbildung 6: Ein Beispiel zur Funktionsweise des Linked-Array. Jeder Prozessor i untersucht nun jede p-te Speicherstelle, angefangen mit Speicherstelle i. Ist der zu durchsuchende Wert negativ, so wird er als Zeiger interpretiert und das nächste Element, auf welches der Zeiger zeigt, wird überprüft. Ist der Wert hingegen positiv, so wird er durch den Algorithmus untersucht. Anschließend springt der Prozess p Elemente weiter und bearbeitet den Wert, den er dort findet. Ist der vorgefundene Wert kleiner als –p(w+p), zeigt der Zeiger also auf einen Wert außerhalb des Arrays, so ist das Array durchlaufen und der Prozess ist mit dieser Iteration fertig. Bei dieser Art der Abarbeitung der Elemente des Linked-Arrays bei mehr als einem Prozessor beträgt der Unterschied in der Belastung der Prozessoren höchstens ein Element, die Arbeit wird also optimal aufgeteilt [Quinn 83]. 11 Kapitel 3: Minimum Cost Spanning Tree Ein Problem des Linked-Array ist jedoch, dass im worst case ein Prozess alle zu bearbeitenden Knoten einfügt, was dazu führt, dass der p-fache Speicherplatz gegenüber einem normalen Array benötigt wird. Diesem Problem kann begegnet werden, indem die neu zu bearbeitenden Knoten zunächst auf die privaten Listen aufgeteilt werden, bevor die große Liste erstellt wird. Praktischerweise werden zwei Linked-Arrays benutzt. Aus dem Einen wird gelesen, während in das andere geschrieben wird. In der folgenden Iteration tauschen die beiden Arrays die Rollen. Eine andere für dieses Problem geeignete Datenstruktur ist die Double-Ended-Queue. 4 Minimum Cost Spanning Tree Das Minimum Cost Spanning Tree Problem ist mit dem Connected-ComponentsProblem verwandt und hat somit auch eine ähnliche Komplexität. So beträgt die Komplexität für das Finden des Minimum-Cost-Spanning-Tree in einem ungerichteten Gra- ( ) phen mit n Knoten auf einem Pyramiden-SIMD-Computer O log n . Die wichtigsten drei sequentiellen Algorithmen sind die nach Sollin, Kruskal und PrimDijkstra. Die ersten Beiden werden im Folgenden näher betrachtet. 4.1 Sollins Algorithmus 4.1.1 Die Idee Am Anfang wird jeder einzelne Knoten als Baum gesehen. In jeder Iteration sucht sich jeder Knoten die Kante mit dem niedrigsten Wert, welche den aktuellen Baum mit einem anderen Baum verbindet. Diese Kante wird dann zum Minimum-Cost-SpanningTree hinzugefügt, wenn sich in diesem dadurch kein Zyklus ergibt. Tritt ein Problem auf, weil zwei gleichwertige Kanten in einer Iteration eingefügt werden sollen, aber wegen der Zyklenbedingung nur eine erlaubt ist, so wird zur Laufzeit willkürlich entschieden, welche Kante genommen wird (Kante (C,D) oder Kante (E,F) in Iteration 3 im Beispiel). 12 Kapitel 4: Verbundene Komponenten Vorher: 2 A 5 Nach Iteration 1: 4 B 3 4 I 7 C 5 1 G A 2 5 E 2 2 D 5 G H 4 B 3 4 7 Nach Iteration 2: C 5 1 F 2 5 2 D A G H 3 5 E 2 4 B 4 7 C 5 D 1 F 2 5 E 2 H Abbildung 7: Sollins Algorithmus 4.1.2 Parallelisierung public Minimum Cost Spanning Tree(): Parameter: n //Anzahl Knoten Globale Variablen:nächster[i] //Entfernung von Baum i zum nächsten Knoten kante[U,V] //Kante zwischen den Knoten U und V, die den aktuellen Baum mit dem nahesten Baum verbindet T //Menge der Kanten im Minimum-Cost-SpanningTree V,W //Knoten, zwischen denen die zu betrachtende Kante liegt wert[U,V] //Wert der Kante zwischen U und V i //Teilbäume 1. begin 2. for (i=1 to n){ 3. Knoten i gehört zu Teilbaum i; 4. } 5. T ist leer; 6. while (Anzahl Kanten in T < n-1){ 7. for (alle i){ 8. nächster[i]=∞; 9. } 10. for (alle kante[V,W]){ 11. if (find(V) ≠ find(W)){ 12. if (wert[V,W] < nächster[find(V)){ 13. nächster[find(V)] = wert[V,W]; 14. kante[find(V)] = [V,W]; 15. } 16. } 17. } 18. for (alle Bäume i){ 19. [V,W] = kante[i]; 20. if (find(V) ≠ find(W)){ 21. T = T ∪ [V,W]; 22. union(V,W); 23. } 24. } 25. } 26. end Abbildung 8: sequentielle Version von Sollins Algorithmus 13 Kapitel 4: Verbundene Komponenten Hier wird die Parallelisierung auf dem UMA Multiprozessor Modell näher betrachtet. Die Funktion Find(k) sucht hierbei die Knotengruppe zu einem Knoten k, während die Funktion Union(v,w) zwei Knotengruppen miteinander verbindet. Da es bei Parallelisierungen immer günstig ist, das Gesamtproblem möglichst weit außen aufzuteilen, würde man hier am liebsten den äußeren while-loop parallelisieren, das geht allerdings aufgrund der starken Abhängigkeiten zwischen den einzelnen Iterationen nicht. Also werden die drei for-Schleifen folgendermaßen parallelisiert: 1. Schleife (Zeilen 7-9): Prescheduling. Jedem Prozessor werden 1/p Bäume zugeteilt. 2. Schleife (Zeilen 10-17): Prescheduling. Die Bäume werden unter den Prozessoren fair aufgeteilt (z.B. nach Anzahl der zu überprüfenden Kanten). 3. Schleife (Zeilen 18-24): Hier kann das Prescheduling nicht einfach eingesetzt werden, weil es zu einem Problem kommen kann, wenn ein Teilbaum A sich mit einem Teilbaum B verbinden möchte und dieser Teilbaum B versucht sich gleichzeitig auch mit Teilbaum A zu verbinden. Wenn nun beide Prozesse Zeile 20 vor der Unionoperation in Zeile 22 abarbeiten, werden beide Kanten hinzugefügt und es ergibt sich ein Zyklus. Dies lässt sich lösen, indem die kritische Region (Zeilen 20-23) gesperrt wird, sobald sich ein Prozess in ihr befindet. 4.1.3 Komplexität Weil die Anzahl der Bäume mit jeder Iteration mindestens halbiert wird, kommt der Algorithmus mit log n Iterationen aus. In einer Iteration werden höchstens O(n2) Vergleiche benötigt. Es ergibt sich also zusammen für den sequentiellen Algorithmus eine Komplexität von O(n 2 log n ) . Die Operationen Find und Union lassen sich quasi in konstanter Zeit O(log* n ) realisieren und brauchen deshalb hier nicht gesondert betrachtet werden [Hopcraft, Ullmann 1973]. Um auf die Komplexität des parallelen Algorithmus zu kommen, muss man sich die einzelnen Schleifen ansehen: Zeile Komplexität Begründung 7-9 1017 1824 O ( n p O ( n2 p +p O ( n p p+ p +p ) n Bäume werden auf p Prozessoren aufgeteilt. Es müssen höchstens n2 Verbindungen, aufgeteilt auf p Prozesso- ) ren überprüft werden. ) Das Zusammenfügen von maximal n Bäumen wird auf die p Prot ilt Ei P i l l di B b i 14 Kapitel 4: Verbundene Komponenten = O(n + p ) 24 zessorenverteilt. Ein Prozess muss maximal p mal die Bearbeitungszeit darauf warten, dass er in den kritischen Bereich eintreten darf. Der Summand p hinter den Komplexitäten beschreibt den Aufwand für die Synchronisation der p Prozessoren. ( ( Es ergibt sich also insgesamt eine Komplexität von O log n n2 p + np + n + p )) für den parallelen Sollin Algorithmus auf der UMA-Multiprozessormaschine. 4.2 Kruskals Algorithmus 4.2.1 Die Idee Am Anfang wird beim Kruskal-Algorithmus ein weiteres Mal jeder Knoten als Baum betrachtet. Daraufhin werden die Kanten des Graphen in den Minimum Cost Spanning Tree nach ihrem Wert in aufsteigender Reihenfolge eingefügt. Würde durch das Einfügen ein Zyklus entstehen, so wird die betreffende Kante nicht eingefügt, sondern verworfen. Iteration 1: A 4 2 C 3 B 7 Iteration 2: 2 4 D 8 A E 1 F 5 6 2 G B Iteration 4: A 4 2 B 7 4 D C 3 7 Iteration 3: 2 4 D 8 A E 1 F 5 6 2 G B Iteration 5: 2 C 3 4 8 F A E 1 5 6 2 G B 4 7 4 D 3 7 2 C 4 D 8 E 6 1 F 5 G Iteration 6: 2 C 3 4 8 F A E 1 5 6 2 G B 4 C 3 7 4 D 2 E 6 8 1 F 5 G Abbildung 9: Kruskals Algorithmus 4.2.2 Der Heap als Datenstruktur Bei diesem Algorithmus lässt man die meiste Arbeit die Datenstruktur machen. Da immer nur die jeweils kleinstbewertete Kante benötigt wird, bietet sich hier der Heap an. 15 Kapitel 4: Verbundene Komponenten Lemma: Man kann mit einer UMA-Multiprozessormaschine mit log n Prozessoren ein Element aus einer n-elementigen Menge in konstanter Zeit aus dem Heap auslesen. [Yoo 1983]. Der Heap ist hier ein vollständiger Binärbaum der Tiefe p= log n , wobei fehlenden Blättern der Wert ∞ zugewiesen wird. Er ist in einem Array gespeichert, wobei sich die Kinder eines Knotens i bei den Adressen 2i und 2i+1 befinden. Jedem der p benötigten Prozessoren wird eine Ebene zugeteilt. Der Algorithmus läuft nun so ab, dass für jede Ebene festgehalten wird, ob sie vollständig ist oder nicht. Ist das nicht der Fall, so wird der leere Knoten in der Variable empty-node(i) notiert. Der Prozessor, der für Ebene i Ebene 5 Voll Keins 2 2 3 7 5 4 3 4 Flag 1 1 6 7 21 8 9 10 25 Empty Node Heap 18 11 12 12 30 14 27 15 30 Leer 2 Leer 6 Voll Keins zuständig ist, füllt den Knoten nun mit dem Kind des leeren Knotens auf, welches den kleinsten Wert hat. Wird ein Blatt leer, so bekommt es den Wert ∞. Prozessor 1 entnimmt nun kontinuierlich Werte aus Knoten 1 bis dieser den Wert ∞ hat, also alle Werte ausgelesen wurden. Abbildung 10: Ein Beispielheap während des Algorithmus 4.2.3 Heapaufbau Der Loop, mit dem der Heap aufgebaut wird, kann durch Prescheduling parallelisiert werden. Der Heap wird nun von unten nach oben erstellt. Bemerkt ein Knoten, dass seine beiden Kinder Heaps sind, so bildet er mit ihnen einen neuen Heap. 16 Kapitel 4: Verbundene Komponenten 4.2.4 Der parallele Algorithmus Der parallele Algorithmus baut zunächst den Heap auf. Danach entnimmt ein Prozess fortlaufend Kanten und überprüft, ob diese in den Minimum Cost Spanning Tree eingefügt werden, oder nicht. Die restlichen Prozesse stellen währenddessen den Heap wieder her. Die Entscheidung, ob durch die Hinzunahme einer bestimmten Kante ein Zyklus entstünde oder nicht geschieht mit Hilfe der Operationen Find und Union in quasi konstanter Zeit. Der Entnahmeprozess kann in konstanter Zeit Werte auslesen, da der Heap für den Wiederaufbau konstante Zeit benötigt und dadurch immer Werte zum Auslesen vorhanden sind. Da jeder einzelne Teilbereich dieses Algorithmus in konstanter Zeit zu bewältigen ist, ergibt sich auch für den Gesamtalgorithmus eine Komplexität von O(n). 5 Schlusswort Es wurden nun einige parallele Algorithmen und Probleme, welche durch die Parallelisierung auftauchen, vorgestellt. Ein Hauptproblem ist oft die Zugriffssteuerung auf Ressourcen, welche von mehreren Prozessen genutzt werden. Es ist nötig die so entstehenden kritischen Bereiche im Quellcode so abzusichern, dass sich nur ein Prozess in ihm befindet. Dies beeinträchtigt den Geschwindigkeitsvorteil, den die Parallelisierung ja bringen soll, oft stark. Ein weitere Schwierigkeit stellt die Koordination der einzelnen Prozesse da, weil z. T. Wechselwirkungen zwischen den einzelnen Iterationen bestehen. Einige Lösungsansätze können durch geschickte Wahl der zugrundeliegenden Datenstruktur wesentlich effizienter gestaltet werden. Andere Ansätze nutzen eine bestimmte Rechnerarchitektur optimal aus und erreichen so gute Ergebnisse. Da sich durch Kombinationen von bekannten Algorithmen, verschiedenen Datenstrukturen und Rechnerarchitekturen viele mögliche neue Lösungsansätze ergeben erschließt sich in der Gestaltung von parallelen Algorithmen, welche auf die jeweilige Hardwareund Datenstruktur des Problems zugeschnitten sind ein breites Feld für Innovationen. 17 Kapitel 6: Literaturverzeichnis 6 Literaturverzeichnis Quinn, M.J.: Parallel Computing Theory and Practise. McGraw-Hill 1994 Quinn, M.J.: Algorithmenbau und Parallelcomputer McGraw-Hill, 1988 [Dekel et al.] Delkel E., D. Nassimi, S. Sahni, 1981 Parallel matrix and graph algorithms SIAM Journal on computing vol. 10, no. 4, Nov., pp. 657-675 [Chin, Lam, Chen] Chin, F. Y., J. Lam and I. N. Chen, 1981 Optimal parallel algorithms for the connected components problem. In the proceedings of the 1981 international conference on parallel processing IEEE New York, Aug. pp.170-175 1982 Efficient parallel algorithms for some graph problems. Communications of the ACM vol. 25 no. 9 Sept. pp 659-665 [DaBr] Data Broadcasting in SIMD computers, University of Minnesota, Technical Report # 79-17, 1979 IEEE Trans Comput. [Nassimi, Sahni] Nassimi D., S. Sahni, 1980, Finding connected components and connected ones on a mesh-connected parallel computer. SIAM Journal on computing vol 9, no.4, Nov. pp.744-757 [Quinn 83]: Quinn, M.J., The design and analysis of algorithms and data structures for the efficient solution of graph-theoretic problems on MIMD computers. Ph.D. dissertation, Computer Science Dept., Washington State University, Pullman [Hopcroft, Ullmann] Hopcroft, Ullmann 1973, Set merging Algorithms, SIAM Journal on Computing, vol. 2, pp. 294-303 [Yoo1983] Yoo, 1983, Parallel processing for some network optimization problems. Ph. D. dissertation, Computer Science Dept., Washington State University, Pullman