Ich wollte dasselbe wissen, also habe ich es gemessen. Auf meiner Box (AMD FX (tm) -8150 Acht-Kern-Prozessor bei 3,612361 GHz) benötigt das Sperren und Entsperren eines entsperrten Mutex, der sich in einer eigenen Cache-Zeile befindet und bereits zwischengespeichert ist, 47 Takte (13 ns).
Aufgrund der Synchronisation zwischen zwei Kernen (ich habe CPU # 0 und # 1 verwendet) konnte ich ein Sperren / Entsperren-Paar nur einmal alle 102 ns auf zwei Threads aufrufen, also einmal alle 51 ns, woraus man schließen kann, dass es ungefähr 38 dauert ns, um wiederherzustellen, nachdem ein Thread entsperrt wurde, bevor der nächste Thread ihn wieder sperren kann.
Das Programm, mit dem ich dies untersucht habe, finden Sie hier:
https://github.com/CarloWood/ai-statefultask-testsuite/blob/b69b112e2e91d35b56a39f41809d3e3de2f9e4b8/src/mutex_test.cxx
Beachten Sie, dass es einige fest codierte Werte für meine Box gibt (xrange-, yrange- und rdtsc-Overhead), sodass Sie wahrscheinlich damit experimentieren müssen, bevor es für Sie funktioniert.
Der Graph, den es in diesem Zustand erzeugt, ist:
Dies zeigt das Ergebnis von Benchmark-Läufen mit dem folgenden Code:
uint64_t do_Ndec(int thread, int loop_count)
{
uint64_t start;
uint64_t end;
int __d0;
asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (start) : : "%rdx");
mutex.lock();
mutex.unlock();
asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (end) : : "%rdx");
asm volatile ("\n1:\n\tdecl %%ecx\n\tjnz 1b" : "=c" (__d0) : "c" (loop_count - thread) : "cc");
return end - start;
}
Die beiden rdtsc-Aufrufe messen die Anzahl der Uhren, die zum Sperren und Entsperren von Mutex erforderlich sind (mit einem Overhead von 39 Uhren für die rdtsc-Aufrufe auf meiner Box). Der dritte Asm ist eine Verzögerungsschleife. Die Größe der Verzögerungsschleife ist für Thread 1 um 1 Anzahl kleiner als für Thread 0, sodass Thread 1 etwas schneller ist.
Die obige Funktion wird in einer engen Schleife der Größe 100.000 aufgerufen. Obwohl die Funktion für Thread 1 etwas schneller ist, werden beide Schleifen aufgrund des Aufrufs des Mutex synchronisiert. Dies ist in der Grafik aus der Tatsache ersichtlich, dass die Anzahl der für das Verriegelungs- / Entriegelungspaar gemessenen Takte für Thread 1 etwas größer ist, um die kürzere Verzögerung in der Schleife darunter zu berücksichtigen.
In der obigen Grafik ist der untere rechte Punkt eine Messung mit einer Verzögerung von loop_count von 150. Wenn Sie dann den Punkten unten nach links folgen, wird der loop_count bei jeder Messung um eins reduziert. Wenn es 77 wird, wird die Funktion in beiden Threads alle 102 ns aufgerufen. Wenn anschließend loop_count noch weiter reduziert wird, ist es nicht mehr möglich, die Threads zu synchronisieren, und der Mutex wird die meiste Zeit tatsächlich gesperrt, was zu einer erhöhten Anzahl von Uhren führt, die zum Sperren / Entsperren erforderlich sind. Dadurch erhöht sich auch die durchschnittliche Zeit des Funktionsaufrufs; Die Handlungspunkte gehen nun wieder nach rechts.
Daraus können wir schließen, dass das Sperren und Entsperren eines Mutex alle 50 ns auf meiner Box kein Problem darstellt.
Alles in allem ist meine Schlussfolgerung, dass die Antwort auf die Frage von OP lautet, dass das Hinzufügen von mehr Mutexen besser ist, solange dies zu weniger Konflikten führt.
Versuchen Sie, Mutexe so kurz wie möglich zu halten. Der einzige Grund, sie außerhalb einer Schleife zu platzieren, wäre, wenn diese Schleife alle 100 ns schneller als einmal wiederholt wird (oder besser gesagt, die Anzahl der Threads, die diese Schleife gleichzeitig ausführen möchten, mal 50 ns) oder wenn 13 ns mal Die Schleifengröße ist mehr Verzögerung als die Verzögerung, die Sie durch Konkurrenz erhalten.
EDIT: Ich habe jetzt viel mehr über das Thema erfahren und beginne an der Schlussfolgerung zu zweifeln, die ich hier vorgestellt habe. Zunächst stellen sich heraus, dass CPU 0 und 1 Hyper-Threaded sind. Obwohl AMD behauptet, 8 echte Kerne zu haben, gibt es sicherlich etwas sehr faul, da die Verzögerungen zwischen zwei anderen Kernen viel größer sind (dh 0 und 1 bilden ein Paar, ebenso wie 2 und 3, 4 und 5 und 6 und 7 ). Zweitens ist der std :: mutex so implementiert, dass er Sperren ein wenig dreht, bevor er tatsächlich Systemaufrufe ausführt, wenn er die Sperre für einen Mutex nicht sofort erhält (was zweifellos extrem langsam sein wird). Was ich hier gemessen habe, ist die absolut idealste Situation. In der Praxis kann das Sperren und Entsperren pro Sperre / Entsperrung drastisch länger dauern.
Unterm Strich wird ein Mutex mit Atomics implementiert. Um Atomics zwischen Kernen zu synchronisieren, muss ein interner Bus gesperrt werden, der die entsprechende Cache-Zeile für mehrere hundert Taktzyklen einfriert. Für den Fall, dass keine Sperre erhalten werden kann, muss ein Systemaufruf ausgeführt werden, um den Thread in den Ruhezustand zu versetzen. das ist offensichtlich extrem langsam (Systemaufrufe liegen in der Größenordnung von 10 Mircosekunden). Normalerweise ist das kein wirkliches Problem, da dieser Thread sowieso schlafen muss - aber es könnte ein Problem mit hohen Konflikten sein, bei dem ein Thread die Sperre für die Zeit, in der er sich normalerweise dreht, nicht erhalten kann, und der Systemaufruf auch, aber CAN Nehmen Sie kurz darauf das Schloss. Wenn beispielsweise mehrere Threads einen Mutex in einer engen Schleife sperren und entsperren und jeder die Sperre etwa 1 Mikrosekunde lang beibehält, dann könnten sie enorm verlangsamt werden, weil sie ständig eingeschläfert und wieder aufgewacht werden. Sobald ein Thread in den Ruhezustand versetzt wurde und ein anderer Thread ihn aufwecken muss, muss dieser Thread einen Systemaufruf ausführen und ist um ~ 10 Mikrosekunden verzögert. Diese Verzögerung tritt also beim Entsperren eines Mutex auf, wenn ein anderer Thread im Kernel auf diesen Mutex wartet (nachdem das Drehen zu lange gedauert hat).