Combien coûte l'instruction de verrouillage?

111

J'ai expérimenté le multi threading et le traitement parallèle et j'avais besoin d'un compteur pour faire un comptage de base et une analyse statistique de la vitesse du traitement. Pour éviter les problèmes d'utilisation simultanée de ma classe, j'ai utilisé une instruction de verrouillage sur une variable privée de ma classe:

private object mutex = new object();

public void Count(int amount)
{
 lock(mutex)
 {
  done += amount;
 }
}

Mais je me demandais ... combien coûte le verrouillage d'une variable? Quels sont les effets négatifs sur les performances?

Kees C. Bakker
la source
10
Verrouiller la variable n'est pas si cher; c'est l'attente sur une variable verrouillée que vous voulez éviter.
Gabe
53
c'est beaucoup moins cher que de passer des heures à rechercher une autre condition de course ;-)
BrokenGlass
2
Eh bien ... si une serrure coûte cher, vous voudrez peut-être les éviter en modifiant la programmation afin qu'elle ait besoin de moins de serrures. Je pourrais implémenter une sorte de synchronisation.
Kees C. Bakker
1
J'ai eu une amélioration spectaculaire des performances (en ce moment, après avoir lu le commentaire de @Gabe) simplement en déplaçant beaucoup de code de mes blocs de verrouillage. Bottomline: à partir de maintenant je ne laisserai que l'accès variable (généralement une ligne) à l'intérieur d'un bloc de verrouillage, une sorte de "verrouillage juste à temps". Est-ce que ça fait du sens?
heltonbiker
2
@heltonbiker Bien sûr, cela a du sens. Cela devrait être aussi un principe architectural, vous êtes censé faire des serrures aussi courtes, simples et rapides que possible. Seules les données vraiment nécessaires qui doivent être synchronisées. Sur les boîtiers serveur, vous devez également prendre en compte la nature hybride du verrou. Le conflit, même s'il n'est pas critique pour votre code, est dû à la nature hybride du verrou qui fait tourner les cœurs à chaque accès si le verrou est détenu par quelqu'un d'autre. Vous dévorez effectivement certaines ressources cpu d'autres services sur le serveur pendant un certain temps avant que votre thread ne soit suspendu.
ipavlu

Réponses:

86

Voici un article sur le coût. La réponse courte est 50ns.

Jake Pearson
la source
39
Meilleure réponse courte: 50ns + temps passé à attendre si un autre thread tient le verrou.
Herman
4
Plus il y a de threads qui entrent et sortent du verrou, plus cela coûte cher. Le coût augmente de façon exponentielle avec le nombre de threads
Arsen Zahray
16
Un peu de contexte: la division de deux nombres sur un 3Ghz x86 prend environ 10ns (sans compter le temps nécessaire pour récupérer / décoder l'instruction) ; et le chargement d'une seule variable depuis la mémoire (non mise en cache) dans un registre prend environ 40 ns. Alors 50ns est incroyablement, aveuglante rapide - vous ne devriez pas se soucier du coût d'utilisation lockplus que vous ne vous inquiétez pas au sujet du coût d'utilisation d' une variable.
BlueRaja - Danny Pflughoeft
3
De plus, cet article était ancien lorsque cette question a été posée.
Otis
3
Vraiment super métrique, "presque sans frais", pour ne pas mentionner incorrecte. Vous ne tenez pas compte du fait que c'est court et rapide seulement et UNIQUEMENT s'il n'y a pas de conflit du tout, un fil. DANS CE CAS, VOUS N'AVEZ PAS BESOIN DE VERROUILLER DU TOUT. Deuxième problème, le verrouillage n'est pas le verrouillage, mais le verrouillage hybride, il détecte à l'intérieur du CLR que le verrouillage n'est détenu par personne en fonction d'opérations atomiques et dans ce cas, il évite les appels au noyau du système d'exploitation, c'est-à-dire un anneau différent qui n'est pas mesuré par ceux-ci des tests. Ce qui est mesuré comme 25ns à 50ns est en fait un code d'instructions verrouillées au niveau de l'application si le verrouillage n'est pas pris
ipavlu
50

La réponse technique est que cela est impossible à quantifier, cela dépend fortement de l'état des tampons de réécriture de la mémoire du processeur et de la quantité de données que le pré-récupérateur a collectées doit être rejetée et relue. Les deux sont très non déterministes. J'utilise 150 cycles CPU comme approximation de fond de l'enveloppe qui évite les déceptions majeures.

La réponse pratique est que c'est waaaay moins cher que le temps que vous dépenserez pour déboguer votre code lorsque vous pensez pouvoir ignorer un verrou.

Pour obtenir un nombre précis, vous devrez mesurer. Visual Studio dispose d'un analyseur de concurrence astucieux disponible en tant qu'extension.

Hans Passant
la source
1
En fait non, il peut être quantifié et mesuré. Ce n'est tout simplement pas aussi facile que d'écrire ces verrous tout autour du code, puis de déclarer qu'il ne s'agit que de 50ns, un mythe mesuré sur l'accès à un seul thread au verrou.
ipavlu
8
"Je pense que vous pouvez sauter un verrou" ... Je pense que c'est là que se trouvent beaucoup de gens quand ils lisent cette question ...
Snoop
30

Lectures complémentaires:

Je voudrais présenter quelques articles de ma part, qui s'intéressent aux primitives de synchronisation générales et qui explorent Monitor, le comportement des instructions de verrouillage C #, les propriétés et les coûts en fonction de scénarios distincts et du nombre de threads. Il s'intéresse particulièrement au gaspillage du processeur et aux périodes de débit pour comprendre la quantité de travail pouvant être effectuée dans plusieurs scénarios:

https://www.codeproject.com/Articles/1236238/Unified-Concurrency-I-Introduction https://www.codeproject.com/Articles/1237518/Unified-Concurrency-II-benchmarking-methodologies https: // www. codeproject.com/Articles/1242156/Unified-Concurrency-III-cross-benchmarking

Réponse originale:

Oh cher!

Il semble que la bonne réponse signalée ici comme LA RÉPONSE est intrinsèquement incorrecte! Je voudrais demander à l'auteur de la réponse, respectueusement, de lire l'article lié jusqu'à la fin. article

L'auteur de l'article de 2003 article a été mesure sur la machine Dual Core seulement et dans le premier cas de mesure, il mesure de verrouillage avec un seul fil seulement et le résultat était d' environ 50ns par un accès de verrouillage.

Cela ne dit rien sur un verrou dans l'environnement concurrent. Nous devons donc continuer à lire l'article et dans la seconde moitié, l'auteur mesurait le scénario de verrouillage avec deux et trois threads, ce qui se rapproche des niveaux de concurrence des processeurs actuels.

Ainsi, l'auteur dit qu'avec deux threads sur Dual Core, les verrous coûtent 120ns, et avec 3 threads, cela passe à 180ns. Cela semble donc clairement dépendre du nombre de threads accédant au verrou simultanément.

Donc c'est simple, ce n'est pas 50 ns à moins qu'il ne s'agisse d'un seul thread, où le verrou devient inutile.

Un autre problème à considérer est qu'il est mesuré en temps moyen !

Si le temps des itérations était mesuré, il y aurait même des temps entre 1 ms et 20 ms, simplement parce que la majorité était rapide, mais peu de threads attendront le temps des processeurs et subiront même de longs délais de quelques millisecondes.

C'est une mauvaise nouvelle pour tout type d'application nécessitant un débit élevé et une faible latence.

Et le dernier point à considérer est qu'il pourrait y avoir des opérations plus lentes à l'intérieur de la serrure et c'est très souvent le cas. Plus le bloc de code est exécuté longtemps à l'intérieur de la serrure, plus le conflit est élevé et les retards montent très haut.

Veuillez noter que plus d'une décennie s'est écoulée depuis 2003, c'est-à-dire que quelques générations de processeurs sont spécifiquement conçues pour fonctionner de manière entièrement simultanée et que le verrouillage nuit considérablement à leurs performances.

ipavlu
la source
1
Pour clarifier, l'article ne dit pas que les performances de verrouillage se dégradent avec le nombre de threads dans l'application; les performances se dégradent avec le nombre de threads en concurrence sur le verrou. (Cela est implicite, mais pas clairement indiqué, dans la réponse ci-dessus.)
Gooseberry
Je suppose que vous voulez dire ceci: "Il semble donc clairement dépendre du nombre de threads accédés simultanément et plus, c'est pire." Oui, la formulation pourrait être meilleure. Je voulais dire "accédé simultanément" en tant que threads accédant simultanément au verrou, créant ainsi des conflits.
ipavlu
20

Cela ne répond pas à votre question sur les performances, mais je peux dire que le .NET Framework offre une Interlocked.Addméthode qui vous permettra d'ajouter votre amountà votre donemembre sans verrouiller manuellement sur un autre objet.

Adam Maras
la source
1
Oui, c'est probablement la meilleure réponse. Mais principalement pour des raisons de code plus court et plus propre. La différence de vitesse ne sera probablement pas perceptible.
Henk Holterman
merci pour cette réponse. Je fais plus de choses avec des serrures. Les ints ajoutés sont l'un des nombreux. J'adore la suggestion, je vais l'utiliser à partir de maintenant.
Kees C. Bakker
les verrous sont beaucoup, beaucoup plus faciles à obtenir, même si le code sans verrou est potentiellement plus rapide. Interlocked.Add seul a les mêmes problèmes que + = sans synchronisation.
hangar
10

lock (Monitor.Enter / Exit) est très bon marché, moins cher que des alternatives comme un Waithandle ou Mutex.

Mais si c'était (un peu) lent, préféreriez-vous avoir un programme rapide avec des résultats incorrects?

Henk Holterman
la source
5
Haha ... J'allais pour le programme rapide et les bons résultats.
Kees C. Bakker
@ henk-holterman Il y a plusieurs problèmes avec vos déclarations: Premièrement, comme cette question et réponses l'ont clairement montré, il y a une faible compréhension des impacts du verrouillage sur les performances globales, même les gens déclarant un mythe d'environ 50ns qui n'est applicable qu'avec un environnement à un seul thread. Deuxièmement, votre déclaration est là et restera pendant des années et entre-temps, les processeurs ont grandi en cœurs, mais la vitesse des cœurs ne l'est pas tellement. verrouillage dans l'environnement de nombreux cœurs et le nombre augmente, 2,4,8,10,20,16,32
ipavlu
Mon approche habituelle est de construire la synchronisation de manière lâche avec le moins d'interaction possible. Cela va très vite aux structures de données sans verrouillage. J'ai créé mes wrappers de code autour du verrou tournant pour simplifier le développement et même lorsque TPL a des collections simultanées spéciales, j'ai développé mes propres collections verrouillées par rotation autour de la liste, du tableau, du dictionnaire et de la file d'attente, car j'avais besoin d'un peu plus de contrôle et parfois d'un code fonctionnant sous Spinlock. Je peux vous dire, c'est possible et permet de résoudre plusieurs scénarios que les collections TPL ne peuvent pas faire et avec un grand gain de performances / débit.
ipavlu
7

Le coût d'une serrure en boucle serrée, par rapport à une alternative sans serrure, est énorme. Vous pouvez vous permettre de boucler plusieurs fois tout en étant plus efficace qu'un verrou. C'est pourquoi les files d'attente sans verrouillage sont si efficaces.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LockPerformanceConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            var stopwatch = new Stopwatch();
            const int LoopCount = (int) (100 * 1e6);
            int counter = 0;

            for (int repetition = 0; repetition < 5; repetition++)
            {
                stopwatch.Reset();
                stopwatch.Start();
                for (int i = 0; i < LoopCount; i++)
                    lock (stopwatch)
                        counter = i;
                stopwatch.Stop();
                Console.WriteLine("With lock: {0}", stopwatch.ElapsedMilliseconds);

                stopwatch.Reset();
                stopwatch.Start();
                for (int i = 0; i < LoopCount; i++)
                    counter = i;
                stopwatch.Stop();
                Console.WriteLine("Without lock: {0}", stopwatch.ElapsedMilliseconds);
            }

            Console.ReadKey();
        }
    }
}

Production:

With lock: 2013
Without lock: 211
With lock: 2002
Without lock: 210
With lock: 1989
Without lock: 210
With lock: 1987
Without lock: 207
With lock: 1988
Without lock: 208
Johan Nilsson
la source
4
Cela peut être un mauvais exemple car votre boucle ne fait vraiment rien, à part une seule affectation de variable et un verrou correspond à au moins 2 appels de fonction. De plus, 20 ns par serrure que vous obtenez n'est pas si grave.
Zar Shardan
5

Il existe différentes manières de définir le «coût». Il y a la surcharge réelle d'obtention et de libération du verrou; comme l'écrit Jake, c'est négligeable à moins que cette opération ne soit effectuée des millions de fois.

L'effet que cela a sur le flux d'exécution est plus pertinent. Ce code ne peut être entré que par un thread à la fois. Si vous avez 5 threads effectuant cette opération régulièrement, 4 d'entre eux finiront par attendre que le verrou soit libéré, puis seront le premier thread programmé pour entrer ce morceau de code après que ce verrou soit libéré. Donc, votre algorithme va souffrir considérablement. Cela dépend de l'algorithme et de la fréquence à laquelle l'opération est appelée. Vous ne pouvez pas vraiment l'éviter sans introduire des conditions de concurrence, mais vous pouvez l'améliorer en minimisant le nombre d'appels au code verrouillé.

KeithS
la source