Unterschied zwischen Ausführungsrichtlinien und wann sie verwendet werden

Unterschied zwischen Ausführungsrichtlinien und wann sie verwendet werden

Was ist der Unterschied zwischen seq und par /par_unseq ?

std::for_each(std::execution::seq, std::begin(v), std::end(v), function_call);

std::execution::seq steht für sequentielle Ausführung. Dies ist die Standardeinstellung, wenn Sie die Ausführungsrichtlinie überhaupt nicht angeben. Es zwingt die Implementierung, alle Funktionsaufrufe nacheinander auszuführen. Es ist auch garantiert, dass alles vom aufrufenden Thread ausgeführt wird.

Im Gegensatz dazu std::execution::par und std::execution::par_unseq impliziert eine parallele Ausführung. Das heißt, Sie versprechen, dass alle Aufrufe der angegebenen Funktion sicher parallel ausgeführt werden können, ohne Datenabhängigkeiten zu verletzen. Die Implementierung darf eine parallele Implementierung verwenden, ist aber nicht dazu gezwungen.

Was ist der Unterschied zwischen par und par_unseq ?

par_unseq erfordert stärkere Garantien als par , erlaubt aber zusätzliche Optimierungen. Insbesondere par_unseq erfordert die Option, die Ausführung mehrerer Funktionsaufrufe im selben Thread zu verschachteln.

Lassen Sie uns den Unterschied an einem Beispiel veranschaulichen. Angenommen, Sie möchten diese Schleife parallelisieren:

std::vector<int> v = { 1, 2, 3 };
int sum = 0;
std::for_each(std::execution::seq, std::begin(v), std::end(v), [&](int i) {
  sum += i*i;
});

Sie können den obigen Code nicht direkt parallelisieren, da dies eine Datenabhängigkeit für sum einführen würde Variable. Um dies zu vermeiden, können Sie eine Sperre einführen:

int sum = 0;
std::mutex m;
std::for_each(std::execution::par, std::begin(v), std::end(v), [&](int i) {
  std::lock_guard<std::mutex> lock{m};
  sum += i*i;
});

Jetzt können alle Funktionsaufrufe sicher parallel ausgeführt werden, und der Code wird nicht unterbrochen, wenn Sie zu par wechseln . Aber was würde passieren, wenn Sie par_unseq verwenden stattdessen, wo ein Thread möglicherweise mehrere Funktionsaufrufe nicht nacheinander, sondern gleichzeitig ausführen könnte?

Es kann zum Beispiel zu einem Deadlock führen, wenn der Code wie folgt umgeordnet wird:

 m.lock();    // iteration 1 (constructor of std::lock_guard)
 m.lock();    // iteration 2
 sum += ...;  // iteration 1
 sum += ...;  // iteration 2
 m.unlock();  // iteration 1 (destructor of std::lock_guard)
 m.unlock();  // iteration 2

Im Standard ist der Begriff vektorisierungsunsicher . Um aus P0024R2 zu zitieren:

Eine Möglichkeit, den obigen Code vektorisierungssicher zu machen, besteht darin, den Mutex durch einen atomaren zu ersetzen:

std::atomic<int> sum{0};
std::for_each(std::execution::par_unseq, std::begin(v), std::end(v), [&](int i) {
  sum.fetch_add(i*i, std::memory_order_relaxed);
});

Was sind die Vorteile der Verwendung von par_unseq über par ?

Die zusätzlichen Optimierungen, die eine Implementierung in par_unseq verwenden kann Modus umfassen vektorisierte Ausführung und Arbeitsmigrationen über Threads hinweg (letzteres ist relevant, wenn Task-Parallelität mit einem Parent-Stealing-Scheduler verwendet wird).

Wenn Vektorisierung erlaubt ist, können Implementierungen intern SIMD-Parallelität (Single-Instruction, Multiple-Data) verwenden. Beispielsweise unterstützt OpenMP dies über #pragma omp simd Anmerkungen, die Compilern helfen können, besseren Code zu generieren.

Wann sollte ich std::execution::seq bevorzugen ?

  1. Korrektheit (Vermeidung von Data Races)
  2. Vermeidung von parallelem Overhead (Startkosten und Synchronisation)
  3. Einfachheit (Debugging)

Es ist nicht ungewöhnlich, dass Datenabhängigkeiten eine sequentielle Ausführung erzwingen. Mit anderen Worten, verwenden Sie die sequentielle Ausführung, wenn die parallele Ausführung Data Races hinzufügen würde.

Das Umschreiben und Optimieren des Codes für die parallele Ausführung ist nicht immer trivial. Sofern es sich nicht um einen kritischen Teil Ihrer Anwendung handelt, können Sie mit einer sequentiellen Version beginnen und später optimieren. Sie sollten die parallele Ausführung auch vermeiden, wenn Sie den Code in einer gemeinsam genutzten Umgebung ausführen, in der Sie bei der Ressourcennutzung konservativ sein müssen.

Parallelität gibt es auch nicht umsonst. Wenn die erwartete Gesamtausführungszeit der Schleife sehr gering ist, ist die sequentielle Ausführung höchstwahrscheinlich sogar aus einer reinen Leistungsperspektive die beste. Je größer die Daten und je teurer jeder Berechnungsschritt ist, desto weniger wichtig wird der Synchronisierungsaufwand sein.

Beispielsweise wäre die Verwendung von Parallelität im obigen Beispiel nicht sinnvoll, da der Vektor nur drei Elemente enthält und die Operationen sehr billig sind. Beachten Sie auch, dass die ursprüngliche Version - vor der Einführung von Mutexe oder Atomic - keinen Synchronisations-Overhead enthielt. Ein häufiger Fehler bei der Messung der Beschleunigung eines parallelen Algorithmus besteht darin, eine parallele Version, die auf einer CPU ausgeführt wird, als Basis zu verwenden. Stattdessen sollten Sie immer mit einer optimierten sequentiellen Implementierung ohne Synchronisierungsaufwand vergleichen.

Wann sollte ich std::execution::par_unseq bevorzugen ?

Stellen Sie zunächst sicher, dass die Korrektheit nicht beeinträchtigt wird:

  • Falls es bei der parallelen Ausführung von Schritten durch verschiedene Threads zu Datenrennen kommt, par_unseq ist keine Option.
  • Wenn der Code vektorisierungsunsicher ist , zum Beispiel, weil es eine Sperre par_unseq erwirbt ist keine Option (aber par könnte sein).

Verwenden Sie andernfalls par_unseq wenn es sich um einen leistungskritischen Teil handelt und par_unseq verbessert die Leistung gegenüber seq .

Wann sollte ich std::execution::par bevorzugen ?

Wenn die Schritte sicher parallel ausgeführt werden können, aber Sie par_unseq nicht verwenden können weil es Vektorisierungs-unsicher ist , ist es ein Kandidat für par .

Wie seq_unseq , vergewissern Sie sich, dass es sich um einen leistungskritischen Teil handelt, und par ist eine Leistungsverbesserung gegenüber seq .

Quellen:

  • cppreference.com (Ausführungsrichtlinie)
  • P0024R2:Der Parallelitäts-TS sollte standardisiert werden

seq bedeutet "sequenziell ausführen" und ist genau dasselbe wie die Version ohne Ausführungsrichtlinie.

par bedeutet "parallel ausführen", wodurch die Implementierung auf mehreren Threads parallel ausgeführt werden kann. Sie sind dafür verantwortlich sicherzustellen, dass innerhalb von f keine Datenrennen stattfinden .

par_unseq bedeutet, dass die Implementierung nicht nur in mehreren Threads ausgeführt werden darf, sondern auch einzelne Schleifeniterationen innerhalb eines einzelnen Threads verschachteln darf, d. h. mehrere Elemente laden und f ausführen darf auf alle erst danach. Dies ist erforderlich, um eine vektorisierte Implementierung zu ermöglichen.