42

プロジェクトを開始する前に、(System.Collections.Concurrent) からの ConcurrentBag のパフォーマンスをロックとリストと比較する簡単なテストを作成しました。ConcurrentBag が単純な List でロックするよりも 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);
4

11 に答える 11

43

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

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

競合は、特にコレクションから読み取る場所に忍び寄る可能性がはるかに高くなります。コレクションを繰り返し処理している間にコレクションを変更しないように、操作全体をロックする必要があるループ (または内部でループに相当する LINQ クエリ)内foreachforeach

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


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

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

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

于 2011-01-24T20:01:01.437 に答える
15

Microsoft が 4.5 で修正した .NET Framework 4 にはバグがあるようです。ConcurrentBag があまり使用されるとは予想していなかったようです。

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

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

于 2012-06-07T14:51:49.327 に答える
10

一般的な答えとして:

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

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

あなたの特定の例は非常に高度な論争を持っているので、私はその振る舞いに驚いていると言わなければなりません。一方で、ロックをかけたままの作業量は非常に少ないので、結局、ロック自体の争いは少ないのではないでしょうか。また、ConcurrentBagの同時実行処理の実装に欠陥がある可能性があります。これにより、特定の例(頻繁な挿入と読み取りなし)が悪いユースケースになります。

于 2011-01-24T18:29:41.310 に答える
9

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 に記載されているように、「同じスレッドがバッグに格納されたデータの生成と消費の両方を行うシナリオに最適化されています。」これは、生のリストと比べてパフォーマンスが低下する最も可能性の高い説明です。

于 2011-01-24T19:38:20.163 に答える
5

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

source - .NET 4.0 での ConcurrentBag の高コスト

于 2012-09-03T22:52:09.027 に答える
3

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

于 2011-01-24T18:54:59.350 に答える
1

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

于 2011-01-24T21:06:29.277 に答える
1

基本的に、同時書き込みはほとんどなく、競合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);
    }
}
于 2011-01-24T18:37:15.350 に答える
0

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

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

方法: 小さなループ本体を高速化する

于 2011-02-24T00:30:53.653 に答える
0

2 つの間のスケーリングを見るのは興味深いでしょう。

2 つの質問

1) 読み取りのバッグとリストの速度。リストにロックを設定することを忘れないでください。

2) 別のスレッドが書き込み中の場合、バッグとリストの読み込み速度

于 2011-01-24T18:40:29.893 に答える
0

ConcurrentBag は他の同時収集よりも遅いようです。

これは実装の問題だと思います.ANTS Profilerは、配列のコピーを含むいくつかの場所で行き詰っていることを示しています.

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

于 2011-04-01T21:02:01.920 に答える