2013-07-30 6 views
10

Hier ist ein Experiment zum Vergleich der Parallelität in C++ und D. Ich implementierte einen Algorithmus (ein paralleles Label-Propagation-Schema für die Community-Erkennung in Netzwerken) in beiden Sprachen mit dem gleichen Design: Ein paralleler Iterator bekommt eine Handle-Funktion (normalerweise a Schließung) und wendet es für jeden Knoten in der Grafik an. HierWarum skaliert dieser parallele Code in D so schlecht?

ist der Iterator in D, umgesetzt mit taskPool von std.parallelism:

/** 
* Iterate in parallel over all nodes of the graph and call handler (lambda closure). 
*/ 
void parallelForNodes(F)(F handle) { 
    foreach (node v; taskPool.parallel(std.range.iota(z))) { 
     // call here 
     handle(v); 
    } 
} 

und dies ist der Griff-Funktion, die übergeben wird:

auto propagateLabels = (node v){ 
     if (active[v] && (G.degree(v) > 0)) { 
      integer[label] labelCounts; 

      G.forNeighborsOf(v, (node w) { 
       label lw = labels[w]; 
       labelCounts[lw] += 1; // add weight of edge {v, w} 
      }); 

      // get dominant label 
      label dominant; 
      integer lcmax = 0; 
      foreach (label l, integer lc; labelCounts) { 
       if (lc > lcmax) { 
        dominant = l; 
        lcmax = lc; 
       } 
      } 

     if (labels[v] != dominant) { // UPDATE 
      labels[v] = dominant; 
      nUpdated += 1; // TODO: atomic update? 
      G.forNeighborsOf(v, (node u) { 
       active[u] = 1; 
      }); 
     } else { 
      active[v] = 0; 
     } 

     } 
    }; 

Die C++ 11 Umsetzung ist nahezu identisch , verwendet jedoch OpenMP zur Parallelisierung. Was zeigen die Skalierungsexperimente?

scaling

Hier untersuche ich schwach Skalierung, die Eingangsdiagrammgröße zu verdoppeln und gleichzeitig die Anzahl der Threads zu verdoppeln und die Laufzeit zu messen. Das Ideal wäre eine gerade Linie, aber natürlich gibt es einen gewissen Mehraufwand für die Parallelität. Ich verwende defaultPoolThreads(nThreads) in meiner Hauptfunktion, um die Anzahl der Threads für das D-Programm festzulegen. Die Kurve für C++ sieht gut aus, aber die Kurve für D sieht überraschend schlecht aus. Mache ich etwas falsches w.r.t. D Parallelität oder spiegelt dies die Skalierbarkeit von parallelen D-Programmen wider?

p.s. Compiler-Flags

für D: rdmd -release -O -inline -noboundscheck

für C++: -std=c++11 -fopenmp -O3 -DNDEBUG

pps. Es muss etwas wirklich falsch sein, weil die D-Implementierung ist langsamer parallel als aufeinander folgend:

enter image description here

PPPs. Für die Neugierigen, hier sind die Mercurial Klon Urls für beide Implementierungen:

+0

Wie sieht die Leistung aus, wenn Sie ohne openmp arbeiten? – greatwolf

+0

Von der Überprüfung her sieht es nicht so aus, als ob der DMD-Compiler derzeit openmp unterstützt. Es scheint mir kein Vergleich zwischen Äpfeln und Äpfeln zu sein, wenn eine Version openmp benutzt und andere nicht. – greatwolf

+0

@greatwolf Wenn ich dich nicht missverstehe, glaube ich, dass du den Punkt verpasst hast. D hat kein OpenMP, aber es hat die Bibliothek 'std.parallelism', die ähnliche parallele Konstrukte bereitstellt. Tatsächlich verwendet das D-Programm beim Ausführen viele Kerne. – clstaudt

Antwort

8

Es ist schwer zu sagen, weil ich nicht vollständig verstehen, wie Ihr Algorithmus funktionieren soll, Es sieht jedoch so aus, als ob Ihr Code nicht Thread-sicher ist, was dazu führt, dass der Algorithmus mehr Iterationen als nötig ausführt.

ich das Ende PLP.run hinzugefügt:

writeln(nIterations); 

Mit 1 Faden nIterations = 34
mit 100 Fäden nIterations = 90

sehen also, wie Sie mit 10 Fäden nIterations = 19
, es nimmt nicht mehr wegen einiger Probleme mit std.parallelism, aber einfach weil es mehr Arbeit macht.

Warum ist Ihr Code nicht Thread-sicher?

Die Funktion, die Sie laufen parallel ist propagateLabels, die geteilt hat, unsynchronisierten Zugang zu labels, nUpdated und active. Wer weiß, was für ein bizarres Verhalten das verursacht, aber es kann nicht gut sein.

Bevor Sie mit der Profilerstellung beginnen, müssen Sie den Algorithmus threadsicher machen.

+1

Gute Beobachtung. Die interessante Frage ist für mich: Warum unterscheidet sich dieses Verhalten in D von der praktisch identischen C++ - Implementierung? Ich bin mir bewusst, dass Threads "Labels", "aktiv" und "nUpdated" teilen. Diese Situation ist die gleiche für die C++/OpenMP-Implementierung, wo es kein Problem ist. – clstaudt

+1

Leider bin ich nicht mit OpenMP vertraut, aber die Art und Weise, wie es Jobs ausfährt, kann sich von der Standardparallelität unterscheiden, also funktioniert die OpenMP-Lösung "einfach" mit der Art und Weise, wie Sie Dinge ausführen. –

5

Wie Peter Alexander hervorhebt, scheint Ihr Algorithmus Thread-unsicher zu sein. Um es threadsicher zu machen, müssen Sie alle Datenabhängigkeiten zwischen Ereignissen eliminieren, die in verschiedenen Threads gleichzeitig oder in einer undefinierten Reihenfolge auftreten können. Eine Möglichkeit, dies zu tun, besteht darin, einen Zustand über Threads hinweg unter Verwendung von WorkerLocalStorage (bereitgestellt in std.parallelism) zu replizieren und die Ergebnisse möglicherweise in einer relativ billigen Schleife am Ende des Algorithmus zusammenzuführen.

In einigen Fällen kann der Prozess diesen Zustand der replizierenden kann durch Schreiben der Algorithmus als eine Reduktion und Verwendung std.parallelism.reduce (möglicherweise in Kombination mit std.algorithm.map oder std.parallelism.map) automatisiert werden, die schwere Arbeit zu tun.