2015-04-17 21 views
15

Ich experimentiere an der Optimierung einiger mathematischer Operationen mit C# .net in einem Paket namens Grasshopper (Teil von Rhino3D). Die Operation ist ziemlich einfach, aber die Liste, auf der sie ausgeführt werden muss, ist groß und kann viel größer werden.C# .net Multithreading

Ich benutze Parallel.ForEach und Listen in meinem C# -Skript und die Anzahl der endgültigen Ergebnisse, die ich bekomme, ist niedriger als erwartet. Dies liegt wahrscheinlich daran, dass list.add nicht Thread-sicher ist (oder nicht threadsicher in der Software, auf der ich es aufbaue).

private void RunScript(double z, int x, List<double> y, ref object A) 
    { 
    List<double> temp = new List<double>(); 
    double r; 
    System.Threading.Tasks.Parallel.ForEach(y, numb => 
     { 
     r = Math.Pow((numb * x), z); 
     temp.Add(r); 
     }); 
    A = temp; 

Bitte helfen Sie mir diesen einfachen mathematischen Betrieb läuft über eine einfache und effiziente Art und Weise herauszufinden, mehrere hundert Werte CPU Multithreading (oder wenn Sie Vorschläge über GPU CUDA).

Ich hoffe, dass die obskure und spezifische Software Sie nicht stört, denn soweit ich weiß, funktioniert es identisch mit normalen C# .Net/Python/VB.Net.

+0

Ja, höchstwahrscheinlich der List.Add nicht THREAD ist und intern auf Fragen in der Liste führen kann. Ein weiteres Problem ist, dass Sie die lokale Variable r zwischen den ausführenden Threads ohne Synchronisierung teilen. Die lokale Variablendefinition sollte sich innerhalb des ausführenden Blocks befinden oder sogar besser in den List.Add-Methodenaufruf eingebunden werden. –

+0

Anstelle von 'List' könnten Sie' ConcurrentBag' versuchen, das Thread-sicher ist: [MSDN] (https://msdn.microsoft.com/en-us/library/dd381779%28v=vs.110%29. aspx) – Thaoden

+0

Nur ein Vorschlag: Versuchen Sie, einen spezifischeren Fragetitel zu schreiben. Der Inhalt der Frage ist gut und vollkommen gültig, aber der Titel ist auch wichtig. – chris

Antwort

14

Sie vermuten, List<T> ist nicht Thread-sicher. Sie müssen den Zugriff auf jede Instanz davon synchronisieren.

Eine Möglichkeit ist, einfach in jeder Aufgabe zu synchronisieren:

private void RunScript(double z, int x, List<double> y, ref object A) 
{ 
    List<double> temp = new List<double>(); 
    object l = new object(); 
    System.Threading.Tasks.Parallel.ForEach(y, numb => 
    { 
     double r = Math.Pow((numb * x), z); 
     lock (l) temp.Add(r); 
    }); 
    A = temp; 
} 

Hinweis: Code auch in ihm einen weiteren Fehler hatte. Sie haben die gleiche r Variable unter allen Aufgaben geteilt, was dazu führen konnte, dass derselbe Wert zweimal oder öfter zum Ergebnis hinzugefügt wurde, während andere Werte weggelassen wurden. Ich habe den Fehler behoben, indem ich einfach die Variablendeklaration in den Rumpf der anonymen Methode verschoben habe, die für den Aufruf ForEach() verwendet wurde.


Eine weitere Möglichkeit, dass Sie im Voraus zu erkennen ist, wie viele Ergebnisse, die Sie haben, und so einfach ein Array groß genug initialisieren können alle Ergebnisse enthalten:

private void RunScript(double z, int x, List<double> y, ref object A) 
{ 
    double[] results = new double[y.Count]; 
    System.Threading.Tasks.Parallel.For(0, y.Count, i => 
    { 
     // read-only access of `y` is thread-safe: 
     results[i] = Math.Pow((y[i] * x), z); 
    }); 
    A = new List<double>(results); 
} 

Keine zwei Threads je versuchen Sie, auf das gleiche Element im Array results zugreifen, und das Array selbst wird nie ändern (dh neu zugewiesen werden), so dass dies absolut threadsicher ist.

Das obige geht davon aus, dass Sie wirklich ein List<double> als das Ausgabeobjekt benötigen. Wenn ein Array zufriedenstellend ist, können Sie results natürlich A zuweisen, anstatt es an den List<T>-Konstruktor zu übergeben, um am Ende ein ganz neues Objekt zu erstellen.

+0

Das "Lock" um den gesamten Körper der Schleife besiegt den Zweck von "Parallel.ForEach" vollständig. –

+0

@LucasTrzesniewski: Ja, du hast Recht ... Ich wünschte, jemand hätte das früher bemerkt. Es endete auf diese Weise, als ich "r" losließ, und ich bewegte die Berechnung in das Schloss (es war vorher nicht da), ohne es durch zu denken. Danke, dass du darauf hingewiesen hast. Ich werde bemerken, dass selbst wenn es "richtig" gemacht wird, die Synchronisation immer noch schädlich ist, daher das zweite Beispiel. Jede "frei-threaded" Implementierung wird eine synchronisierte einfach schlagen, und diese "einfache" Herangehensweise ist die schlechteste. –

2

Hier ist eine weitere Option:

private void RunScript(double z, int x, List<double> y, ref object A) { 
     var temp = new System.Collections.Concurrent.BlockingCollection<double>(); 
     System.Threading.Tasks.Parallel.ForEach(y, numb => { 
      double r = Math.Pow((numb * x), z); 
      temp.Add(r); 
     }); 
     A = temp; // if needed you can A = temp.ToList(); 
     } 

Peter denken gute Arbeit geleistet haben, die Probleme mit dem Code von umreißt und ich die zweite Funktion er die beste Wahl ist wahrscheinlich vermuten lässt. Dennoch ist es schön, Alternativen zu sehen und zu erfahren, dass das .NET-Framework gleichzeitig sichere Sammlungen enthält.

7

Eine einfachere Lösung wäre wahrscheinlich .AsParallel() und arbeiten auf der resultierende ParallelEnumerable stattdessen zu verwenden:

private void RunScript(double z, int x, List<double> y, ref object A) 
{ 
    A = y 
     .AsParallel().AsOrdered() 
     .Select(elem => Math.Pow((elem * x), z)) 
     .ToList(); 
} 
+1

Es ist interessant, wenn Sie die '.AsOrdered()' herausnehmen, dass die Berechnung 3 bis 4 mal langsamer ist. – Enigmativity

+0

@Enigmatismus Sie sind auch nicht garantiert, dass die Elemente in "A" den gleichen Indizes wie die Elemente in "y" entsprechen, standardmäßig muss AsParallel() 'nicht die Reihenfolge der Eingaben gleich halten. –

+2

@ScottChamberlain - Ja, mir ist das bekannt, aber ich fand es interessant, dass, wenn wir ** bestellen wollen, die Berechnung 3 bis 4 mal ** schneller ist **. Das schien mir widersinnig zu sein. – Enigmativity

0

Vielen Dank für Ihre Eingabe! Wenn Sie im Profiler Ausgang interessiert sind, ist wie folgt:

Peter Duniho erste Option: 330 ms

Peter Duniho 2. Option: 207ms

Dweeberly Option: 335ms

Mattias Buelens Option: 376ms

das ist sehr seltsam angeblich .net Skripte müssen schneller laufen in Heuschrecke (weil es. NET ist) aber keine Ihrer Lösungen schlägt die Python-Parallel-Berechnung von 129ms!

Jedenfalls danke Ihnen allen für die detaillierten Antworten! Du bist großartig!

+0

Es ist schwer Äpfel mit Äpfeln zu vergleichen. Einer der wichtigsten Faktoren ist jedoch, dass zumindest für das von Ihnen beschriebene Problem die Parallelisierung nicht viel kostet. Ich kenne Ihre genauen Eingaben nicht, aber ich habe ein Szenario erstellt, das in etwa 200 ms abgeschlossen wurde. Ich fand heraus, dass etwa 25% der Kosten des Algorithmus eine Speicherbereinigung waren (der Wechsel zu einer Version mit 0 Zwischenzuweisungen verbesserte die Zeit um 25%). Wenn Sie nur Single-Threading ausführen, erhöht sich die Zeit um 50% für meine 2-Core-CPU (Hyperthread). Pythons Mathematik ist möglicherweise nicht so genau wie .NET usw. –

+0

Ohne vollständige Beispiele für C# und die Python-Implementierung ist es unmöglich, genau zu wissen, woher die Zeitunterschiede kommen. Und ohne diese Details sind die tatsächlichen Zeitvergleiche nicht sehr nützlich. Es ist ein klassisches Problem bei der Code-Optimierung. –

+0

Um ehrlich zu sein, bin ich nicht so sehr mit Präzision beschäftigt ... solange es innerhalb von 0,001 Toleranz liegt. Es ist ein skizzenhaftes Skript, so ist die Mathematik drin. Wenn ich es in einen Satz schreiben muss.Was ist letztlich der schnellste Weg, um eine große Anzahl von Werten zu multiplizieren und zu erhöhen. (sagen wir mehr als 20k Anzahl der Werte). Die Python-Option zeigt 2-3 Verbesserungen der C# -Option bisher und da Python in dieser Umgebung inhärent langsamer sein sollte, denke ich immer noch, dass es eine bessere Möglichkeit gibt, dies mit C# zu tun. –

0

Ich schaute auch auf die Änderung der Eingabe ein wenig. Aufteilen der Daten in separate Zweige, Berechnen jeder Verzweigung in einem separaten Thread und erneutes Kombinieren dieser Daten am Ende. Allerdings schneidet es bei 531ms schlechter ab. Ich verstehe das Skript ist schlecht, aber ich denke, es zeigt meine Idee gut und wenn richtig geschrieben kann Erfolg erreichen. Nein?

private void RunScript(double z, int x, List<double> y, DataTree<double> u, ref object A) 
    { 
    System.Threading.Tasks.Task<double[]> th1 = System.Threading.Tasks.Task<double[]>.Factory.StartNew(() => mP(u.Branch(0).ToArray(), x, z)); 
    System.Threading.Tasks.Task<double[]> th2 = System.Threading.Tasks.Task<double[]>.Factory.StartNew(() => mP(u.Branch(1).ToArray(), x, z)); 
    System.Threading.Tasks.Task<double[]> th3 = System.Threading.Tasks.Task<double[]>.Factory.StartNew(() => mP(u.Branch(2).ToArray(), x, z)); 
    System.Threading.Tasks.Task<double[]> th4 = System.Threading.Tasks.Task<double[]>.Factory.StartNew(() => mP(u.Branch(3).ToArray(), x, z)); 
    System.Threading.Tasks.Task<double[]> th5 = System.Threading.Tasks.Task<double[]>.Factory.StartNew(() => mP(u.Branch(4).ToArray(), x, z)); 
    System.Threading.Tasks.Task<double[]> th6 = System.Threading.Tasks.Task<double[]>.Factory.StartNew(() => mP(u.Branch(5).ToArray(), x, z)); 
    System.Threading.Tasks.Task<double[]> th7 = System.Threading.Tasks.Task<double[]>.Factory.StartNew(() => mP(u.Branch(6).ToArray(), x, z)); 
    System.Threading.Tasks.Task<double[]> th8 = System.Threading.Tasks.Task<double[]>.Factory.StartNew(() => mP(u.Branch(7).ToArray(), x, z)); 

    List<double> list = new List<double>(); 

    list.AddRange(th1.Result); 
    list.AddRange(th2.Result); 
    list.AddRange(th3.Result); 
    list.AddRange(th4.Result); 
    list.AddRange(th5.Result); 
    list.AddRange(th6.Result); 
    list.AddRange(th7.Result); 
    list.AddRange(th8.Result); 


    A = list; 


    } 

Sorry, ich kann nicht Sachen in dem „mit“

+0

Ein Teil des Problems hier ist, dass die Berechnung selbst so einfach ist, dass jeder Versuch, die Nebenläufigkeit mikro-zu verwalten, besonders wenn Sie Zwischenobjekte erstellen müssen und besonders wenn Sie tatsächlich neue Kopien der Daten erstellen müssen Tendenz, die Rechenkosten zu dominieren. Und da dieses Micro-Management in etwa proportional zum Grad der Parallelität ist, wird es die Vorteile der Parallelität stark ausgleichen. Denken Sie daran: Meine Worst-Case-, Singlethread- und 0-Concurrency-Lösung war nur 300 ms, verglichen mit einem Best-Case (bisher) von 120 ms. –