42 votes

Pourquoi ConcurrentBag <T> si lent dans. Net (4.0)? Est-ce que je me trompe?

Avant que je commence un projet, j'ai écrit un test simple pour comparer les performances de ConcurrentBag d' (Système d'.Les Collections.En même temps) par rapport à verrouillage & listes. Je suis extrêmement surpris que ConcurrentBag est plus de 10 fois plus lent que le verrouillage avec une simple Liste. Ce que je comprends, le ConcurrentBag fonctionne mieux lorsque le lecteur et l'écrivain est le même thread. Cependant, je n'avais pas pensé que ça serait tellement pire que les serrures traditionnelles.

J'ai lancer un test avec deux Parallèles pour les boucles d'écriture et de lecture à partir d'une liste/sac. Cependant, l'écriture en elle-même montre une énorme différence:

private static void ConcurrentBagTest()
   {
        int collSize = 10000000;
        Stopwatch stopWatch = new Stopwatch();
        ConcurrentBag<int> bag1 = new ConcurrentBag<int>();

        stopWatch.Start();


        Parallel.For(0, collSize, delegate(int i)
        {
            bag1.Add(i);
        });


        stopWatch.Stop();
        Console.WriteLine("Elapsed Time = {0}", 
                          stopWatch.Elapsed.TotalSeconds);
 }

Sur ma boîte, cela prend entre 3-4 secondes pour exécuter, par rapport à 0.5 - 0.9 secondes de ce code:

       private static void LockCollTest()
       {
        int collSize = 10000000;
        object list1_lock=new object();
        List<int> lst1 = new List<int>(collSize);

        Stopwatch stopWatch = new Stopwatch();
        stopWatch.Start();


        Parallel.For(0, collSize, delegate(int i)
            {
                lock(list1_lock)
                {
                    lst1.Add(i);
                }
            });

        stopWatch.Stop();
        Console.WriteLine("Elapsed = {0}", 
                          stopWatch.Elapsed.TotalSeconds);
       }

Comme je l'ai mentionné, en faisant des lectures et écritures simultanées n'aide pas la concurrente de sac de test. Suis-je en train de faire quelque chose de mal ou est-ce la structure de données vraiment lent?

[EDIT] - j'ai enlevé les Tâches parce que je n'ai pas besoin d'eux ici (code Complet avait une autre tâche de lecture)

[MODIFIER] Merci beaucoup pour les réponses. Je vais avoir du mal à choisir "la bonne réponse", car elle semble être un mélange d'un peu de réponses.

Comme Michael Goldshteyn souligné, la vitesse dépend vraiment de données. Darin a souligné qu'il devrait y avoir plus de contention pour ConcurrentBag pour être plus rapide, et en Parallèle.Pour ne pas nécessairement commencer le même nombre de threads. Un point à retenir est de ne pas faire quelque chose que vous ne pas avoir à l'intérieur d'une serrure. Dans le cas ci-dessus, je ne me vois pas faire quelque chose à l'intérieur de la serrure à l'exception peut-être de l'affectation de la valeur d'une variable temp.

En outre, sixlettervariables a souligné que le nombre de threads qui se trouvent être en cours d'exécution peut également affecter les résultats, bien que j'ai essayé de courir à l'origine du test dans l'ordre inverse et ConcurrentBag était encore plus lent.

J'ai couru quelques tests à partir du 15 Tâches et les résultats dépendait de la taille de la collection, entre autres choses. Cependant, ConcurrentBag fait presque aussi bien ou mieux que le verrouillage d'une liste, jusqu'à 1 million d'insertions. Au-dessus de 1 million de dollars, verrouillage semblait être beaucoup plus rapide parfois, mais je vais probablement jamais à avoir un plus grand discbased pour mon projet. Voici le code que j'ai couru:

        int collSize = 1000000;
        object list1_lock=new object();
        List<int> lst1 = new List<int>();
        ConcurrentBag<int> concBag = new ConcurrentBag<int>();
        int numTasks = 15;

        int i = 0;

        Stopwatch sWatch = new Stopwatch();
        sWatch.Start();
         //First, try locks
        Task.WaitAll(Enumerable.Range(1, numTasks)
           .Select(x => Task.Factory.StartNew(() =>
            {
                for (i = 0; i < collSize / numTasks; i++)
                {
                    lock (list1_lock)
                    {
                        lst1.Add(x);
                    }
                }
            })).ToArray());

        sWatch.Stop();
        Console.WriteLine("lock test. Elapsed = {0}", 
            sWatch.Elapsed.TotalSeconds);

        // now try concurrentBag
        sWatch.Restart();
        Task.WaitAll(Enumerable.Range(1, numTasks).
                Select(x => Task.Factory.StartNew(() =>
            {
                for (i = 0; i < collSize / numTasks; i++)
                {
                    concBag.Add(x);
                }
            })).ToArray());

        sWatch.Stop();
        Console.WriteLine("Conc Bag test. Elapsed = {0}",
               sWatch.Elapsed.TotalSeconds);

43voto

Dan Tao Points 60518

Permettez-moi de vous poser cette question: comment est-il réaliste de ce que vous auriez une application qui est constamment à ajouter à une collection et de ne jamais le lire? Quelle est l'utilité d'une telle collection? (Ce n'est pas purement une question de rhétorique. Je pourrait imaginer des utilisations où, par exemple, vous ne lisez à partir de la collection à l'arrêt (pour l'enregistrement) ou lorsqu'il est demandé par l'utilisateur. Je crois que ces scénarios sont assez rares, cependant).

C'est ce que votre code est la simulation. Appelant List<T>.Add va être rapide comme l'éclair, sauf dans les rares cas où la liste a de redimensionner son tableau interne; mais ce n'est lissé par tous les autres ajoute que, arriver assez rapidement. Si vous n'êtes pas susceptible de voir une quantité importante de conflit dans ce contexte, en particulier des tests sur un PC personnel avec, par exemple, de 8 cœurs (comme vous l'avez indiqué que vous avez dans un commentaire quelque part). Peut-être que vous pourriez voir plus de contention sur quelque chose comme un 24-core de la machine, où de nombreux noyaux peut être essayer d'ajouter à la liste littéralement en même temps.

La Contention est beaucoup plus probable de s'installer où vous le lire à partir de votre collection, esp. en foreach boucles (ou requêtes LINQ pour un montant foreach boucles sous le capot) qui exigent le verrouillage de l'ensemble de l'opération, de sorte que vous ne pouvez pas modifier votre collection lors de l'itération sur elle.

Si vous pouvez reproduire de façon réaliste ce scénario, je crois que vous verrez ConcurrentBag<T> échelle beaucoup mieux que votre test en cours est affiché.


Mise à jour: Ici est un programme que j'ai écrit pour comparer ces collections dans le scénario que j'ai décrit ci-dessus (plusieurs écrivains, beaucoup de lecteurs). L'exécution de 25 essais avec une collection de taille de 10 000 et 8 threads de lecture, j'ai obtenu les résultats suivants:

Pris 529.0095 ms pour ajouter 10000 éléments à une Liste de<double> avec 8 threads de lecture.
Pris 39.5237 ms pour ajouter 10000 éléments pour une ConcurrentBag<double> avec 8 threads de lecture.
Pris 309.4475 ms pour ajouter 10000 éléments à une Liste de<double> avec 8 threads de lecture.
Pris 81.1967 ms pour ajouter 10000 éléments pour une ConcurrentBag<double> avec 8 threads de lecture.
Pris 228.7669 ms pour ajouter 10000 éléments à une Liste de<double> avec 8 threads de lecture.
Pris 164.8376 ms pour ajouter 10000 éléments pour une ConcurrentBag<double> avec 8 threads de lecture.
[ ... ]
Moyenne de temps de liste: 176.072456 mme.
Moyen sac de temps: 59.603656 mme.

Donc clairement, il dépend exactement ce que vous faites avec ces collections.

15voto

Paleta Points 310

Il semble y avoir un bogue dans le .NET Framework 4 que Microsoft a corrigé dans la version 4.5, il semble qu'ils ne s'attendaient pas à ce que ConcurrentBag soit beaucoup utilisé.

Voir le post Ayende suivant pour plus d'informations

http://ayende.com/blog/156097/the-high-cost-of-concurrentbag-in-net-4-0

9voto

Michael Goldshteyn Points 24679

Comme une réponse générale:

  • Les collections simultanées qui utilisent le verrouillage peut être très rapide si il y a peu ou pas de querelles de leurs données (c'est à dire, des verrous). Cela est dû au fait que la collecte de ces classes sont souvent construits à l'aide de très peu coûteux de verrouillage primitives, surtout quand uncontented.
  • Lockless collections peut être plus lente, en raison de trucs utilisés pour éviter les verrous et en raison d'autres goulets d'étranglement tels que les faux partage, de la complexité nécessaire pour mettre en œuvre leurs lockless nature conduisant à des défauts de cache, etc...

Pour résumer, la décision de ce qui est plus rapide est fortement dépendante sur les structures de données utilisées et la quantité de contention pour les serrures parmi les autres questions (par exemple, nombre de lecteurs de vs écrivains dans une commune/exclusif type d'arrangement).

Votre exemple a un très haut degré de contention, donc je dois dire que je suis surpris par le comportement. D'autre part, la quantité de travail effectué pendant que le verrou est maintenu est très petit, alors peut-être il y a peu de contention pour la serrure en elle-même, après tout. Il pourrait également y avoir des lacunes dans la mise en œuvre de ConcurrentBag de simultanéité de la manipulation, ce qui rend votre exemple particulier (avec de fréquentes inserts et pas de lit) d'une mauvaise affaire pour elle.

9voto

user7116 Points 39829

En regardant le programme à l'aide de MS affirmation de visualizer montre qu' ConcurrentBag<T> a un coût beaucoup plus élevé associé avec le parallèle de l'insertion que de simplement le verrouillage sur un List<T>. Une chose que j'ai remarqué, c'est que il semble y avoir un coût associé à la mise en rotation des 6 fils (sur ma machine) pour commencer le premier ConcurrentBag<T> run (exécution froide). 5 ou 6 filets sont ensuite utilisés avec l' List<T> code, qui est plus rapide (chaud exécuter). L'ajout d'un autre ConcurrentBag<T> courir après la liste montre qu'il prend moins de temps que la première (chaud exécuter).

À partir de ce que je vois dans le conflit, beaucoup de temps est consacré à l' ConcurrentBag<T> de la mise en œuvre de l'allocation de mémoire. Retrait de l'explicite, de l'allocation de la taille de l' List<T> code de la ralentit, mais pas assez pour faire une différence.

EDIT: il semble que l' ConcurrentBag<T> conserve en interne une liste par Thread.CurrentThread, serrures de 2 à 4 fois selon si elle est en cours d'exécution sur un nouveau fil de discussion, et effectue au moins un Interlocked.Exchange. Comme indiqué dans MSDN: "optimisé pour les scénarios où le même thread sera à la fois productrices et consommatrices de données stockées dans le sac." C'est l'explication la plus probable pour votre baisse de performance par rapport à une crue de la liste.

3voto

Chris Shain Points 33569

Comme @ Darin-Dimitrov l'a dit, je soupçonne que votre Parallel.For ne génère pas le même nombre de threads dans chacun des deux résultats. Essayez de créer manuellement N threads pour vous assurer que vous voyez réellement des conflits de threads dans les deux cas.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X