web-dev-qa-db-fra.com

Quelle est l'efficacité du verrouillage d'un mutex déverrouillé? Quel est le coût d'un mutex?

Dans un langage de bas niveau (C, C++ ou autre): j'ai le choix entre avoir un tas de mutex (comme ce que pthread me donne ou ce que la bibliothèque système native fournit) ou un seul pour un objet.

Dans quelle mesure est-il efficace de verrouiller un mutex? C'est à dire. combien d'instructions d'assembleur sont probables et combien de temps prennent-elles (dans le cas où le mutex est déverrouillé)?

Combien coûte un mutex? Est-ce un problème d'avoir vraiment beaucoup de mutex? Ou puis-je simplement ajouter autant de variables mutex dans mon code que j'ai int variables et cela n'a pas vraiment d'importance?

(Je ne sais pas trop quelles différences existe entre différents matériels. Si c'est le cas, j'aimerais également les connaître. Mais surtout, je m'intéresse au matériel commun.)

Le fait est qu'en utilisant de nombreux mutex couvrant chacun une partie de l'objet au lieu d'un seul mutex pour l'objet entier, j'ai pu sécuriser de nombreux blocs. Et je me demande jusqu'où je devrais aller à ce sujet. C'est à dire. dois-je essayer de sécuriser le plus possible un bloc possible, aussi compliqué soit-il, et combien de mutex supplémentaires cela signifie-t-il?


article de blog WebKits (2016) sur le verrouillage est très lié à cette question et explique les différences entre un verrou tournant, un verrou adaptatif, un futex, etc.

131
Albert

J'ai le choix entre avoir un tas de mutex ou un seul pour un objet.

Si vous avez plusieurs threads et que l'accès à l'objet se produit souvent, plusieurs verrous augmenteraient le parallélisme. Au prix de la maintenabilité, plus de verrouillage signifie plus de débogage du verrouillage.

Dans quelle mesure est-il efficace de verrouiller un mutex? C'est à dire. combien d'instructions d'assembleur sont-elles probables et combien de temps prennent-elles (dans le cas où le mutex est déverrouillé)?

Les instructions précises de l'assembleur représentent le minimum de temps système de n mutex - cohérence de la mémoire/du cache sont le temps système principal. Et moins souvent, un verrou particulier est pris - mieux.

Le mutex est composé de deux parties principales (simplification excessive): (1) un drapeau indiquant si le mutex est verrouillé ou non et (2) une file d'attente.

Le changement de drapeau ne prend que quelques instructions et se fait normalement sans appel système. Si mutex est verrouillé, syscall ajoutera le thread appelant à la file d'attente et lancera l'attente. Le déverrouillage, si la file d'attente est vide, est économique, mais nécessite par ailleurs un appel système pour réactiver l'un des processus en attente. (Sur certains systèmes, les appels système économiques/rapides sont utilisés pour implémenter les mutex, ils deviennent des appels système lents (normaux) uniquement en cas de conflit.)

Verrouiller mutex débloqué est vraiment pas cher. Déverrouiller un mutex sans contention est également bon marché.

Combien coûte un mutex? Est-ce un problème d'avoir beaucoup de mutex? Ou puis-je ajouter autant de variables mutex dans mon code que de variables int et cela n'a pas vraiment d'importance?

Vous pouvez ajouter autant de variables mutex dans votre code que vous le souhaitez. Vous n'êtes limité que par la quantité de mémoire allouée par votre application.

Sommaire. Les verrouillages d’espace utilisateur (et les mutex en particulier) sont peu coûteux et ne sont soumis à aucune limite du système. Mais trop d'entre eux sont un cauchemar pour le débogage. Tableau simple:

  1. Moins de verrous signifie plus de contentions (appels système lents, blocage du processeur) et moins de parallélisme
  2. Moins de verrous signifie moins de problèmes pour déboguer des problèmes multi-threading.
  3. Plus de verrous signifie moins de conflits et plus de parallélisme
  4. Plus de verrous signifie plus de chances de vous heurter à des impasses irréversibles.

Un schéma de verrouillage équilibré pour l'application devrait être trouvé et maintenu, en équilibrant généralement le n ° 2 et le n ° 3.


(*) Le problème avec les mutex moins souvent verrouillés est que si vous avez trop de verrouillage dans votre application, une grande partie du trafic inter-processeur/cœur entraîne le vidage de la mémoire mutex du cache de données des autres processeurs cohérence du cache. Les vidages de cache ressemblent à des interruptions légères et sont gérés de manière transparente par les CPU - mais ils introduisent ce qu'on appelle stalls (recherche de "stall").

Et ce sont les stalles qui font que le code de verrouillage fonctionne lentement, souvent sans aucune indication apparente de la lenteur de l'application. (Certains Arch fournissent les statistiques de trafic inter-processeur/principal, d'autres non.)

Pour éviter le problème, les utilisateurs ont généralement recours à un grand nombre de verrous pour réduire la probabilité de conflits de verrous et pour éviter le blocage. C’est la raison pour laquelle le verrouillage d’espace utilisateur bon marché, non soumis aux limites du système, existe.

100
Dummy00001

Je voulais savoir la même chose, alors je l'ai mesurée. Sur ma machine (processeur à huit cœurs AMD FX (tm) -8150 à 3,612361 GHz), verrouiller et déverrouiller un mutex déverrouillé qui se trouve dans sa propre ligne de cache et est déjà mis en cache prend 47 horloges (13 ns).

En raison de la synchronisation entre deux cœurs (j'ai utilisé les processeurs n ° 0 et n ° 1), je ne pouvais appeler une paire verrouillage/déverrouillage qu'une fois toutes les 102 ns sur deux threads, donc une fois tous les 51 ns, à partir de laquelle on peut conclure qu'il faut environ 38 ns à récupérer après qu'un thread ait déverrouillé avant que le prochain thread ne puisse le verrouiller à nouveau.

Le programme que j'ai utilisé pour étudier cela peut être trouvé ici: https://github.com/CarloWood/ai-statefultask-testsuite/blob/b69b112e2e91d35b56a39f41809d3e3de2f9e4b8/src/mutex_test.cxx

Notez qu'il a quelques valeurs codées en dur spécifiques à ma boîte (overhead xrange, yrange et rdtsc), de sorte que vous devrez probablement l'essayer avant que cela ne fonctionne pour vous.

Le graphique qu'il produit dans cet état est:

enter image description here

Cela montre le résultat des tests de performance sur le code suivant:

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;
}

Les deux appels rdtsc mesurent le nombre d'horloges nécessaires pour verrouiller et déverrouiller le 'mutex' (avec un temps système supplémentaire de 39 horloges pour les appels rdtsc sur ma boîte). Le troisième asm est une boucle à retard. La taille de la boucle à retard est 1 fois plus petite pour le thread 1 que pour le thread 0; le thread 1 est donc légèrement plus rapide.

La fonction ci-dessus est appelée dans une boucle étroite de taille 100 000. Malgré le fait que la fonction soit légèrement plus rapide pour le thread 1, les deux boucles se synchronisent à cause de l'appel du mutex. Ceci est visible dans le graphique car le nombre d'horloges mesurées pour la paire verrouiller/déverrouiller est légèrement plus grand pour le fil 1, afin de prendre en compte le délai plus court dans la boucle en dessous.

Dans le graphique ci-dessus, le point en bas à droite est une mesure avec un délai loop_count de 150, puis en suivant les points en bas, vers la gauche, le nombre de boucles est réduit de un à chaque mesure. Quand il devient 77, la fonction est appelée toutes les 102 ns dans les deux threads. Si par la suite, loop_count est encore réduit, il n'est plus possible de synchroniser les threads et le mutex commence à être réellement verrouillé la plupart du temps, ce qui entraîne un nombre accru d'horloges pour effectuer le verrouillage/déverrouillage. De plus, la durée moyenne de l'appel de fonction augmente à cause de cela; donc les points de l'intrigue montent maintenant et vers la droite à nouveau.

Nous pouvons en conclure que verrouiller et déverrouiller un mutex toutes les 50 ns n’est pas un problème pour ma boîte.

Dans l'ensemble, ma conclusion est que la réponse à la question de OP est qu'il est préférable d'ajouter plus de mutex, à condition que cela engendre moins de conflits.

Essayez de verrouiller les mutex le plus court possible. La seule raison de les placer en dehors d'une boucle est si cette boucle boucle plus rapidement qu'une fois toutes les 100 ns (ou plutôt, le nombre de threads souhaitant exécuter cette boucle simultanément 50 ns) ou 13 ns fois. la taille de la boucle est plus longue que celle que vous obtenez par contention.

EDIT: Je suis maintenant beaucoup plus au courant sur le sujet et commence à douter de la conclusion que je vous ai présentée ici. Tout d'abord, les CPU 0 et 1 se révèlent être hyper-threadés; Même si AMD prétend avoir 8 noyaux réels, il y a certainement quelque chose de très louche, car les délais entre deux autres noyaux sont beaucoup plus longs (0 et 1 forment une paire, tout comme 2 et 3, 4 et 5, et 6 et 7 ) Deuxièmement, le std :: mutex est implémenté de telle sorte qu'il verrouille un peu avant de lancer les appels système lorsqu'il ne parvient pas à obtenir immédiatement le verrou sur un mutex (ce qui sera sans doute extrêmement lent). Donc, ce que j’ai mesuré ici est la situation la plus idéale et, dans la pratique, verrouiller et déverrouiller peut prendre beaucoup plus de temps par verrou/déverrouillage.

En bout de ligne, un mutex est implémenté avec des atomiques. Pour synchroniser les atomes entre les cœurs, un bus interne doit être verrouillé, ce qui fige la ligne de cache correspondante pendant plusieurs centaines de cycles d'horloge. Dans le cas où un verrou ne peut pas être obtenu, un appel système doit être effectué pour mettre le thread en veille; c'est évidemment extrêmement lent. Normalement, ce n'est pas vraiment un problème car ce thread doit rester en mode veille de toute façon - mais il pourrait s'agir d'un problème de haute contention, lorsqu'un thread ne peut pas obtenir le verrou pendant le temps qu'il tourne normalement, de même l'appel système, mais PEUT prenez la serrure peu de temps après. Par exemple, si plusieurs threads verrouillent et déverrouillent un mutex dans une boucle serrée et qu’ils gardent le verrou pendant environ une microseconde, ils risquent alors d’être énormément ralentis du fait qu’ils sont constamment endormis et réveillés.

17
Carlo Wood

Cela dépend de ce que vous appelez "mutex", du mode OS, etc.

À minimum, le coût d'une opération de mémoire verrouillée est coûteux. C'est une opération relativement lourde (comparée à d'autres commandes d'assembleur primitif).

Cependant, cela peut être beaucoup plus élevé. Si vous appelez "mutex" un objet du noyau (c'est-à-dire - un objet géré par le système d'exploitation) et que vous l'exécutez en mode utilisateur - chaque opération effectuée entraîne une transaction en mode noyau, qui est très lourde.

Par exemple sur un processeur Intel Core Duo, Windows XP. Opération verrouillée: prend environ 40 cycles de processeur. Appel en mode noyau (appel système, par exemple) - environ 2000 cycles de la CPU.

Si tel est le cas, vous pouvez envisager d’utiliser des sections critiques. C'est un hybride d'un mutex de noyau et d'un accès mémoire verrouillée.

10
valdo

Le coût varie en fonction de la mise en œuvre, mais vous devez garder à l’esprit deux choses:

  • le coût sera probablement minime car il s’agit à la fois d’une opération assez primitive et optimisée autant que possible en raison de son schéma d’utilisation (utilisé avec un lot ).
  • peu importe le coût, puisque vous devez l'utiliser si vous voulez un fonctionnement multithread sécurisé. Si vous en avez besoin, alors vous en avez besoin.

Sur les systèmes à processeur unique, vous pouvez généralement simplement désactiver les interruptions suffisamment longtemps pour modifier les données de manière atomique. Les systèmes multiprocesseurs peuvent utiliser une stratégie test-and-set .

Dans les deux cas, les instructions sont relativement efficaces.

Pour ce qui est de savoir si vous devez fournir un seul mutex pour une structure de données volumineuse, ou si vous avez plusieurs mutex, un pour chaque section de celle-ci, c’est un exercice d’équilibre.

En ayant un seul mutex, vous avez un risque plus élevé de conflit entre plusieurs threads. Vous pouvez réduire ce risque en ayant un mutex par section, mais vous ne voulez pas vous retrouver dans une situation où un thread doit verrouiller 180 mutex pour faire son travail :-)

6
paxdiablo

Je suis complètement nouveau dans pthreads et mutex, mais je peux confirmer par l'expérimentation que le coût du verrouillage/déverrouillage d'un mutex est presque nul lorsqu'il n'y a pas de conflit, mais lorsqu'il y en a, le coût du blocage est extrêmement élevé. J'ai exécuté un code simple avec un pool de threads dans lequel la tâche consistait simplement à calculer une somme dans une variable globale protégée par un verrou mutex:

y = exp(-j*0.0001);
pthread_mutex_lock(&lock);
x += y ;
pthread_mutex_unlock(&lock);

Avec un thread, le programme additionne 10 000 000 de valeurs virtuellement instantanément (moins d’une seconde); avec deux threads (sur un MacBook à 4 cœurs), le même programme prend 39 secondes.

1
Grant Petty