web-dev-qa-db-ja.com

.Net(4.0)でConcurrentBag <T>が非常に遅いのはなぜですか?私はそれを間違っていますか?

プロジェクトを始める前に、(System.Collections.Concurrent)からのConcurrentBagのパフォーマンスをロックとリストと比較するための簡単なテストを書きました。 ConcurrentBagが単純なリストを使用したロックよりも10倍以上遅いことに非常に驚いています。私が理解していることから、ConcurrentBagは、リーダーとライターが同じスレッドである場合に最もよく機能します。ただし、パフォーマンスが従来のロックよりも大幅に低下するとは思っていませんでした。

2つのParallel forループでテストを実行して、リスト/バッグへの書き込みとリスト/バッグからの読み取りを行いました。ただし、書き込み自体には大きな違いがあります。

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

私のボックスでは、このコードの実行に0.5〜0.9秒と比較して、実行に3〜4秒かかります。

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

先に述べたように、読み取りと書き込みを同時に行っても、同時バッグテストには役立ちません。私は何か間違っているのですか、それともこのデータ構造は本当に遅いのですか?

[編集]-ここでは必要ないため、タスクを削除しました(完全なコードには別のタスクの読み取りがありました)

[編集]答えてくれてありがとう。 「正しい答え」を選ぶのに苦労しているのは、いくつかの答えが混在しているように思われるためです。

Michael Goldshteynが指摘したように、速度は実際にはデータに依存します。 Darinは、ConcurrentBagを高速化するには競合が増えるはずであり、Parallel.Forが必ずしも同じ数のスレッドを開始するとは限らないと指摘しました。取り上げておくべきポイントの1つは、ロックの内側では、何もしないする必要があるをしないことです。上記の場合、一時変数に値を割り当てている場合を除いて、ロック内で自分が何かをしているのを見ていません。

さらに、sixlettervariablesは、実行中のスレッドの数も結果に影響を与える可能性があることを指摘しましたが、元のテストを逆の順序で実行しようとしたが、ConcurrentBagの速度はさらに低下しました。

15個のタスクを開始していくつかのテストを実行しましたが、結果はとりわけコレクションのサイズに依存していました。ただし、ConcurrentBagは、最大100万回の挿入で、リストのロックとほぼ同じかそれ以上のパフォーマンスを発揮しました。 100万を超えると、ロックの方がはるかに高速に見える場合がありますが、プロジェクトのデータ構造が大きくなることはおそらくないでしょう。これが私が実行したコードです:

        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);
42
Tachy

これを聞いてみましょう:常にコレクションに追加しているアプリケーションがあり、それを決して読み取らないとしたら、どれほど現実的ですか?そのようなコレクションの用途は何ですか? (これは純粋に修辞的な質問ではありません。 could は、たとえば、シャットダウン時に(ロギングのために)コレクションから読み取るか、ユーザーから要求されたときにのみコレクションから読み取る用途があると想像します。ただし、これらのシナリオはかなりまれであると考えています。)

これは、コードがシミュレートしているものです。 List<T>.Addの呼び出しは、リストが内部配列のサイズを変更する必要がある場合を除いて、非常に高速になります。しかし、これは非常に迅速に発生する他のすべての追加によってスムーズになります。そのため、このコンテキストでかなりの量の競合が発生する可能性は低いです。特にたとえば、8コアのパーソナルPCでのテスト(コメントで述べたとおり)どこかに)。 たぶん 24コアマシンのようなものでより多くの競合が見られるかもしれません。多くのコアがリストに追加しようとしている可能性があります literally 同時に。

特に、コレクションから read で競合が発生する可能性がはるかに高くなります。 foreachループ(または内部でforeachループになるLINQクエリ)では、操作全体をロックして、反復処理中にコレクションを変更しないようにする必要があります。

このシナリオを現実的に再現できれば、ConcurrentBag<T>のスケールは、現在のテストのスケールよりもはるかに良くなると思います。


UpdateHere は、上記のシナリオ(複数のライター、多くのリーダー)でこれらのコレクションを比較するために作成したプログラムです。 10000のコレクションサイズと8つのリーダースレッドで25回の試行を実行すると、次の結果が得られました。

 529.0095ミリ秒をかけて10要素を8つのリーダースレッドでList <double>に追加します。 ] 309.4475 msをかけて、8つのリーダースレッドでList <double>に10000要素を追加します。
 81.1967 msをかけて、8つのリーダースレッドでConcurrentBag <double>に10000要素を追加します。 8個のリーダースレッドを持つList <double>に10000要素を追加します。
 8つのリーダースレッドを持つConcurrentBag <double>に10000要素を追加するには、164.8376ミリ秒かかりました。
 [... ____。]リストの平均時間:176.072456ミリ秒。平均バッグ時間:59.603656 ms。

つまり、これらのコレクションで何をしているのかによります。

43
Dan Tao

Microsoftが4.5で修正した.NET Framework 4にバグがあるようですが、ConcurrentBagが頻繁に使用されることを期待していなかったようです。

詳細については、次のAyendeの投稿を参照してください

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

15
Paleta

MSのコンテンションビジュアライザーを使用してプログラムを見ると、ConcurrentBag<T>は単にList<T>をロックするよりも、並列挿入に関連するコストがはるかに高いことがわかります。最初に気付いたことの1つは、最初のConcurrentBag<T>実行(コールドラン)を開始するために6つのスレッド(私のマシンで使用)をスピンアップすることに関連するコストがあるようです。次に、5つまたは6つのスレッドがList<T>コードで使用され、より高速になります(ウォームラン)。リストにConcurrentBag<T>ランを追加すると、最初の(ウォームラン)よりも時間がかかりません。

私が競合で見ているものから、メモリを割り当てるConcurrentBag<T>実装に多くの時間が費やされています。 List<T>コードからサイズの明示的な割り当てを削除すると、速度が低下しますが、違いを生むには不十分です。

編集:ConcurrentBag<T>Thread.CurrentThreadごとに内部的にリストを保持し、新しいスレッドで実行されているかどうかに応じて2〜4回ロックし、少なくとも1つのInterlocked.Exchange。 MSDNで述べたように、「バッグに格納されたデータを同じスレッドが生成および消費するシナリオに最適化されています。」これは、生のリストに対するパフォーマンス低下の最も可能性の高い説明です。

9
user7116

一般的な答えとして:

  • データの競合(ロックなど)がほとんどないかまったくない場合、ロックを使用する並行コレクションは非常に高速になる可能性があります。これは、そのようなコレクションクラスが、非常に安価なロックプリミティブを使用して構築されることが多いためです(特に、コンテンツがない場合)。
  • ロックレスコレクションは、ロックを回避するために使用されるトリックや、誤った共有、キャッシュミスにつながるロックレスの性質を実装するために必要な複雑さなどの他のボトルネックのために、遅くなる可能性があります。

要約すると、どちらの方法がより高速であるかの決定は、採用されているデータ構造と、他の問題(たとえば、共有/排他型配置でのリーダーとライターの数)間のロックの競合の量に大きく依存します。

あなたの特定の例は非常に高度な競合を持っているので、私はその振る舞いに驚いていると言わざるを得ません。一方、ロックが保持されている間に行われる作業の量は非常に少ないため、おそらくロック自体の競合はほとんどありません。また、ConcurrentBagの同時実行処理の実装に欠陥があり、特定の例(頻繁に挿入され、読み取りが行われない)がその不適切な使用例になる場合もあります。

9

これはすでに.NET 4.5で解決されています。根本的な問題は、ConcurrentBagが使用するThreadLocalが多くのインスタンスを持つことを期待していなかったことでした。これは修正され、かなり高速に実行できるようになりました。

ソース-.NET 4.0でのConcurrentBagの高コスト

5
Rohit

@ Darin-Dimitrovが言ったように、Parallel.Forが実際に2つの結果のそれぞれで同じ数のスレッドを生成していないのではないかと思います。手動でNスレッドを作成して、両方のケースで実際にスレッドの競合が発生していることを確認してください。

3
Chris Shain

基本的に、同時書き込みはほとんどなく、競合はありません(Parallel.Forは、必ずしも多くのスレッドを意味するわけではありません)。書き込みを並列化すると、さまざまな結果が得られます。

class Program
{
    private static object list1_lock = new object();
    private const int collSize = 1000;

    static void Main()
    {
        ConcurrentBagTest();
        LockCollTest();
    }

    private static void ConcurrentBagTest()
    {
        var bag1 = new ConcurrentBag<int>();
        var stopWatch = Stopwatch.StartNew();
        Task.WaitAll(Enumerable.Range(1, collSize).Select(x => Task.Factory.StartNew(() =>
        {
            Thread.Sleep(5);
            bag1.Add(x);
        })).ToArray());
        stopWatch.Stop();
        Console.WriteLine("Elapsed Time = {0}", stopWatch.Elapsed.TotalSeconds);
    }

    private static void LockCollTest()
    {
        var lst1 = new List<int>(collSize);
        var stopWatch = Stopwatch.StartNew();
        Task.WaitAll(Enumerable.Range(1, collSize).Select(x => Task.Factory.StartNew(() =>
        {
            lock (list1_lock)
            {
                Thread.Sleep(5);
                lst1.Add(x);
            }
        })).ToArray());
        stopWatch.Stop();
        Console.WriteLine("Elapsed = {0}", stopWatch.Elapsed.TotalSeconds);
    }
}
1
Darin Dimitrov

私の推測では、ロックはあまり競合を経験していません。次の記事を読むことをお勧めします: Javaの理論と実践:欠陥のあるマイクロベンチマークの分析 。この記事では、ロックマイクロベンチマークについて説明します。記事で述べたように、このような状況では考慮すべきことがたくさんあります。

1
Edin Dazdarevic

ループ本体が小さいため、PartitionerクラスのCreateメソッドを使用してみてください...

デリゲート本体に順次ループを提供できるため、デリゲートが反復ごとに1回ではなく、パーティションごとに1回だけ呼び出されます。

方法:スモールループボディをスピードアップする

0
Craig

それらの2つの間のスケーリングを確認することは興味深いでしょう。

2つの質問

1)バッグとリストの読み取り速度はどれくらいか、リストにロックをかけることを忘れないでください

2)別のスレッドが書き込みを行っている間の読み取りのバッグとリストの速度

0
Bengie

ConcurrentBagは他の並行コレクションよりも遅いようです。

私はそれが実装の問題だと思います-ANTSプロファイラーは、それが配列のコピーを含むいくつかの場所で行き詰まっていることを示しています。

並行辞書を使用すると、数千倍高速になります。

0
Jason Hernandez