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 ?
- Korrektheit (Vermeidung von Data Races)
- Vermeidung von parallelem Overhead (Startkosten und Synchronisation)
- 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 (aberpar
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.