2.3 Präfixsummen parallel berechnen

Im letzten Teilkapitel haben wir gelernt, wie man das scheinbar inhärent sequentielle Problem der Binäraddition effizient parallelisiert. Als Rechnermodell haben wir dabei die uns bereits bekannten Booelschen Schaltkreise gewählt. Wir werden nun ein ähnliches Problem untersuchen: wir haben ein Array [a1,,an] von n natürlichen Zahlen berechnet und wollen die n Präfixsummen

si:=a1++ai

Dieses Problem nennt man Präfixsummen parallel berechnen oder kurz parallele Präfixsummen, auf Englisch parallel prefix sum. Der Lösungsansatz ist ganz ähnlich wie für den Binäraddierer. Allerdings wird bei den parallelen Präfixen das zugrunde liegende Problem besser deutlich, und noch dazu dient es als Baustein für viele weitere parallele Algorithmen.

Als Berechnungsmodell nehmen wir wieder Schaltkreise, nun aber nicht Boolesche, sondern arithmetische. Jedes Gate kann nur eine Operation durchführen: Addition zweier natürlicher Zahlen. Auf den Kanten des Schaltkreises fließen auch keine Booleschen Werte sondern natürliche Zahlen (natürlich). Beim Entwurf unseres Schaltkreises gehen wir rekursiv vor:

Sie S(n) die Größe des obigen Schaltkreise (also Anzahl der Gates) bei einem Array der Größe n. Wenn n gerade ist, gilt

S(n)=S(n/2)+n1

Das zusätzliche n kommt daher, dass jedes Gate einem Output entspricht, wobei s1 kein Gate benötigt. Der Einfachheit halber schätzen wir S(n)S(n/2)+n ab und somit

S(n)=n+n/2+n/4++1=2n1

wenn n eine Zweierpotenz ist. Anzahl der Zeitschritte ist 2logn.

Schaltkreis-Sichtweise und Prozessoren-Sichtweise

So wie vor zwei Kapiteln begründet, betrachte ich jedes Gate als einen Prozessor, der zu einem bestimmten Zeitpunkt die Bühne betrifft, genau einen Schritt ausführt (oder besser gesagt: O(1) Schritte) und sich dann aus dem Geschehen zieht. Begründet habe ich das unter Anderem mit der Behauptung, wir könnten die Anzahl der Prozessoren problemlos verkleinern (was natürlich einen Zuwachs in der Zeit nach sich zieht).

Übungsaufgabe 2.3.1 Arbeiten Sie die Details aus! Nehmen Sie an, Sie hätten p=n/log(n) Prozessoren zur Verfügung. Können Sie die Rechenschritte so auf die Prozessoren verteilen, dass Sie immer noch in O(logn) Zeitschritten fertig werden?

Hinweis. Jetzt kommt die Schaltkreis-Intuition an ihre Grenzen und wir müssen festlegen, wie unsere Prozessoren arbeiten. Nehmen Sie an, die Prozessoren würden auf einen gemeinsamen Arbeitsspeicher zugreifen (in dessen ersten n Zellen das Eingabe-Array liegt). In einem Makro-Schritt könnte ein Prozessor also so etwas tun wie

  1. Lade Speicherzellen i und j
  2. Berechne die Summe der Inhalte
  3. Speichere das Ergebnis in Zelle k

Abstrahierung

Die rekursive Konstruktion oben ist viel einfacher als unser Gerechne mit den Binärintervallen und Präfixintervallen im letzten Teilkapitel. Ich will daher argumentieren, dass man ein grundlegendes Prinzip herausarbeiten kann, mit dem man parallele Präfixsummen und binäre Addition gleichzeitig erschlagen kann. Hierfür müssen wir abstrahieren.

Sei X eine Menge und (a1,,an)Xn ein Array. Des weiteren sei eine Operation :X×XX, wobei wir allerdings statt (a,b) die Infixschreibweise ab bevorzugen. Wir nehmen an, dass assoziativ ist, dass also

a,b,cX: a(bc)=(ab)c

gilt. In diesem Falle dürfen wir ohne Verwechslungsgefahr abc schreiben. Wir wollen nun alle Präfixprodukte

si:=a1ai

berechnen. Dies effizient parallel zu berechnen nennt man paralleles Berechnen von Präfixen, kurz parallele Präfixe, Englisch parallel prefix. Wir nehmen an, dass ein Prozessor das Produkt ab in einem Schritt berechnen kann und stellen uns das wieder als -Gate vor. Die effiziente Konstruktion ist identisch mit der obigen, nur dass wird + durch ersetzen. Die Operation + ist natürlich assoziativ. Überzeugen Sie sich, dass es für den Erfolg der Parallelpräfixkonstruktion nicht entscheidet ist, ob auch kommutativ ist. Auch ob es eine Gruppenoperation ist, ob es also neutrale oder inverse Elemente gibt, ist nicht wichtig.

Können wir die Binäraddition mit Carry-Lookahead in dieses Framework pressen? Hierfür beobachten wir, dass gpI drei Werte annehmen kann: (1,1),(0,1),(0,0), die den drei "Aktionen" Übertrag erzeugen, Nicht erzeugen, aber weiterreichen und Übertrag verschlucken entsprechen. Die Menge X unserer Werte ist also X={(1,1),(0,1),(0,0)} und ist durch das gp-Gate realisiert. Wir können für auch die "Multiplikationstabelle" schreiben. Für I=[a,b], J=[a,i] und K=[i+1,b] kann nämlich gpI durch folgende Tabelle gegeben:

Übungsaufgabe 2.3.2 Zeigen Sie, dass assoziativ ist. Konkret also, dass die beiden Schaltkreise

die gleiche Boolesche Funktion berechnen.

Sie führen nun die obige Konstruktion für parallele Präfixsummen durch, ersetzen nun aber + durch gp-Gates. Dann können Sie sich alle Überlegungen zu Binärintervallen und Präfixintervallen sparen.

Übungsaufgabe 2.3.3 Sie haben ein Array aus n Elementen. Wenn in eine Reihe hintereinander mehrmals das gleiche Element steht, sprechen wir von einem Block. Das Array [a,a,a,c,c,d,a,e,e,e] besteht also aus insgesamt fünf Blöcken der Längen 3, 2, 1, 1 und 3.

Sie wollen für jeden Index i berechnen, wie lange der zugehörige Block ist und der wievielte. Für das obige Array zum Beispiel wollen Sie die Ausgabe-Arrays

            Lengths    = [3,3,3,2,2,1,1,3,3,3] 
            BlockIndex = [1,1,1,2,2,3,4,5,5,5]

berechnen. Zeigen Sie, wie das mit O(n) Arbeit und O(logn) Zeit geht.

Übungsaufgabe 2.3.4 (Bestes Subinterval). Sie haben ein Array aus ganzen Zahlen gegeben, z.B.

            A = [-8, 5, -4, 7, -9, 3, 0, 7, -5, 8, -1, 2, -5, 2, 9, -3]

Ein Subarray ist ein Array der Form A[i...j]. Der Wert eines Subarrays ist die Summe seiner Elemente. Sie wollen das Subarray mit dem höchsten Wert finden:

A=[8,5,4,78,9,3,0,7,5,8,1,214,5,2,920,3]

in diesem Falle also wohl [3, 0, 7, -5, 8, -1, 2, -5, 2, 9] mit dem Wert 20.

Entwerfen Sie einen sequentiellen Algorithmus, der den Wert des optimalen Subarrays in O(n) Schritten berechnet.

Lösung Irgendjemand hat uns gesagt, dass das mit Dynamic Programming geht. Wir müssen also Teilprobleme definieren. Sei bestUntilHere(t) der Wert des optimalen Teilarrays von A[ij] mit jt, also die Lösung für das Interval A[0t]. Wir wollen nun zeigen, dass wir bestUntilHere(t) einfach berechnen können, wenn wir bereits bestUntilHere(t1) kennen. Sei nun I[0,t] das beste Teilinterval von A[0t]. Wir unterscheiden zwei Fälle: (1) falls tI ist, dann ist I bereits ein Teilinterval von [0,t1] und somit gilt bestUntilHere(t)=bestUntilHere(t1); (2) falls tI ist, also I=[i,t], dann ist, hmmm, vielleicht bestUntilHere(t)=bestUntilHere(t1)+A[t]? Da können wir uns nur sicher sein, wenn das beste Teilinterval von A[0,t1] auch wirklich mit t1 aufhört. Was nicht immer der Fall ist.

Die Lösung ist, zwei Werte zu speichern: in BestUntilHere(t) das beste Teilinterval von A[0,t] (und dazu dessen Wert {\rm bestUntilHere}; wir beginnen mit einem Großbuchstaben, um das Interval zu bezeichnen und mit einem Kleinbuchstaben für dessen Wert) und BestEndingHere(t), das beste Teilinterval von A[0,t], das t auch enthält (und dessen Wert bestEndingHere). Sei nun I=BestEndingHere(t1). Dann ist BestEndingHere(t) entweder I{t} oder einfach nur {t}. Sei J=BestUntilHere(t1). Dann ist BestUntilHere(t) entweder J (wenn es t nicht enthält) oder I{t}. Daher gelten die rekursiven Formeln

bestEndingHere(t)=max(bestEndingHere(t1)+A[t],A[t])bestUntilHere(t1)=max(bestUntilHere(t),bestEndingHere(t)) .

Jetzt müssen Sie sich nur noch überlegen, wie Sie diese beiden Werte für t=0 initialisieren müssen. Dann können Sie mittels Dynamic Programming den Wert BestUntilHere(n) berechnen.

Übungsaufgabe 2.3.5 Zeigen Sie, wie man das Problem aus der letzten Übungsaufgabe effizient parallel berechnen kann! Erfinden Sie nicht das Rad neu sondern formulieren Sie das als Parallelpräfixproblem mit einer passenden Verknüpfung . Überlegen Sie sich: welche Werte müssen Sie für ein Interval [i,j] (analog zu Carry Propagate und Carry Generate) speichern?

Übungsaufgabe 2.3.6 (Reguläre Sprachen und parallele Berechnung). Erinnern Sie sich an reguläre Sprachen, die wir in Theoretische Informatik II, Kapitel 4 kennengelernt haben. Dort haben wir auch gesehen, dass jede reguläre Sprache durch einen endlichen Automaten entschieden werden kann. Hier ein Beispiel für einen endlichen Automaten:

Geben Sie einne effizienten parallelen Algorithmus für das Wortproblem in regulären Sprachen! Also einen Algorithmus, der, gegeben ein Eingabewort wΣ, entscheidet, ob wL ist, wobei L eben die reguläre Sprache ist, um die es geht.

Hinweis: überlegen Sie sich wieder, welche Werte Sie in Analogie zu Carry Propagate und Carry Generate für ein Interval I=[i,j] berechnen müssen und finden Sie die passende Operation .