web-dev-qa-db-ja.com

シングルスレッドでのConcurrentDictionary <>のパフォーマンスの誤解?

関連する簡単な情報:

AFAIK、並行スタック、キュー、およびバッグクラスは、リンクリストを使用して内部的に実装されます。
そして、各スレッドが独自のリンクリストを担当するため、競合がはるかに少ないことを私は知っています。とにかく、私の質問は_ConcurrentDictionary<,>_
についてです

しかし、私はこのコードをテストしていました:(シングルスレッド)

_Stopwatch sw = new Stopwatch();
sw.Start();

    var d = new ConcurrentDictionary < int,  int > ();
    for(int i = 0; i < 1000000; i++) d[i] = 123;
    for(int i = 1000000; i < 2000000; i++) d[i] = 123;
    for(int i = 2000000; i < 3000000; i++) d[i] = 123;
    Console.WriteLine("baseline = " + sw.Elapsed);

sw.Restart();

    var d2 = new Dictionary < int, int > ();
    for(int i = 0; i < 1000000; i++)         lock (d2) d2[i] = 123;
    for(int i = 1000000; i < 2000000; i++)   lock (d2) d2[i] = 123;
    for(int i = 2000000; i < 3000000; i++)   lock (d2) d2[i] = 123;
    Console.WriteLine("baseline = " + sw.Elapsed);

sw.Stop();
_

結果:(何度もテスト、同じ値(+/-))。

_baseline = 00:00:01.2604656
baseline = 00:00:00.3229741
_

質問 :

Whatは_ConcurrentDictionary<,>_ muchsingleスレッド環境で遅くしますか?

私の最初の本能は、lock(){}が常に遅くなるということです。しかし、明らかにそうではありません。

20
Royi Namir

ConcurrentDictionaryが同じ操作でDictionaryよりもオーバーヘッドが多いという最も可能性の高い理由。あなたが情報源を掘り下げれば、これは明らかに真実です

  • インデクサーにロックを使用します
  • 揮発性の書き込みを使用します
  • .Netでアトミックであることが保証されていない値のアトミック書き込みを行う必要があります
  • コア追加ルーチンに追加のブランチがあります(ロックを取得するか、アトミック書き込みを行うかどうか)

これらのコストはすべて、使用されているスレッドの数に関係なく発生します。これらの費用は個別に小さいかもしれませんが、無料ではなく、時間の経過とともに合計されます

24
JaredPar

さて、ConcurrentDictionaryは、複数のスレッドで使用できる可能性を考慮に入れています。複数のスレッドからのアクセスを心配せずに想定できるものよりも多くの内部ハウスキーピングが必要であると私には完全に合理的に思えます。それが逆にうまくいったなら、私は非常に驚いたでしょう-より安全なバージョンが常にだったらより速くも、なぜ安全性の低いバージョンを使用するのでしょうか?

26
Jon Skeet

ConcurrentDictionaryと辞書

一般に、複数のスレッドからキーまたは値を同時に追加および更新するシナリオでは、System.Collections.Concurrent.ConcurrentDictionaryを使用します。頻繁な更新と比較的少ない読み取りを伴うシナリオでは、ConcurrentDictionaryは一般的に適度な利点を提供します。多くの読み取りと多くの更新を伴うシナリオでは、ConcurrentDictionaryは通常、任意の数のコアを備えたコンピューターで大幅に高速になります。

頻繁な更新を伴うシナリオでは、ConcurrentDictionaryで同時実行の度合いを増やしてから、コアの数が多いコンピューターでパフォーマンスが向上するかどうかを測定できます。並行性レベルを変更する場合は、グローバル操作をできるだけ避けてください。

キーまたは値のみを読み取る場合、ディクショナリがスレッドによって変更されていない場合は同期が必要ないため、ディクショナリは高速です。

リンク: https://msdn.Microsoft.com/en-us/library/dd997373%28v=vs.110%29.aspx

2
gagangupt16

ConcurrentDictionary<>は、作成時にロックオブジェクトの内部セットを作成します(これは、他の要因の中でも特にconcurrencyLevelによって決定されます)-このロックオブジェクトのセットは、一連の細かい内部バケット構造へのアクセスを制御するために使用されます-グレインロック。

シングルスレッドのシナリオでは、ロックは必要ないため、これらのロックを取得および解放するための余分なオーバーヘッドが、おそらくあなたが見ている違いの原因です。

2
JerKimball

シングルスレッド環境で_ConcurrentDictionary<,>_がはるかに遅くなる理由は何ですか?

マルチスレッド環境でそれをはるかに高速化するために必要な機械のオーバーヘッド。

私の最初の本能は、lock(){}が常に遅くなるということです。しかし、明らかにそうではありません。

lockは、争われていない場合は非常に安価です。 1秒間に100万回lockすることができ、単一のスレッドから実行している場合、CPUは気付くことさえありません。マルチスレッドプログラムのパフォーマンスを低下させるのは、ロックの競合です。複数のスレッドが同じlockをめぐって激しく競合している場合、ほとんどすべてのスレッドは、ロックを保持している幸運なスレッドがそれを解放するのを待たなければなりません。これは、きめ細かいロック実装を備えたConcurrentDictionaryが優れているところです。そして、並行性が高いほど(プロセッサー/コアが多いほど)、輝きが増します。

0
Theodor Zoulias

すべてが単一のスレッドで行われる場合、1つのスレッドでConcurrentDictionaryを使用したり、アクセスを同期したりしても意味がありません。もちろん、辞書はConcrurrentDictionaryを打ち負かします。

使用パターンとスレッド数に大きく依存します。これは、ConcurrentDictionaryが辞書よりも優れており、スレッド番号の増加に伴ってロックすることを示すテストです。

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;

namespace ConsoleApp
{

    class Program
    {

        static void Main(string[] args)
        {
            Run(1, 100000, 10);
            Run(10, 100000, 10);
            Run(100, 100000, 10);
            Run(1000, 100000, 10);
            Console.ReadKey();
        }

        static void Run(int threads, int count, int cycles)
        {
            Console.WriteLine("");
            Console.WriteLine($"Threads: {threads}, items: {count}, cycles:{cycles}");

            var semaphore = new SemaphoreSlim(0, threads);

            var concurrentDictionary = new ConcurrentDictionary<int, string>();

            for (int i = 0; i < threads; i++)
            {
                Thread t = new Thread(() => Run(concurrentDictionary, count, cycles,  semaphore));
                t.Start();
            }

            Thread.Sleep(1000);

            var w = Stopwatch.StartNew();

            semaphore.Release(threads);

            for (int i = 0; i < threads; i++)
                semaphore.Wait();

            Console.WriteLine($"ConcurrentDictionary: {w.Elapsed}");

            var dictionary = new Dictionary<int, string>();
            for (int i = 0; i < threads; i++)
            {
                Thread t = new Thread(() => Run(dictionary, count, cycles, semaphore));
                t.Start();
            }

            Thread.Sleep(1000);

            w.Restart();

            semaphore.Release(threads);


            for (int i = 0; i < threads; i++)
                semaphore.Wait();

            Console.WriteLine($"Dictionary: {w.Elapsed}");

        }

        static void Run(ConcurrentDictionary<int, string> dic, int elements, int cycles, SemaphoreSlim semaphore)
        {
            semaphore.Wait();
            try
            {
                for (int i = 0; i < cycles; i++)
                    for (int j = 0; j < elements; j++)
                    {
                        var x = dic.GetOrAdd(i, x => x.ToString());
                    }
            }
            finally
            {
                semaphore.Release();
            }
        }

        static void Run(Dictionary<int, string> dic, int elements, int cycles, SemaphoreSlim semaphore)
        {
            semaphore.Wait();
            try
            {
                for (int i = 0; i < cycles; i++)
                    for (int j = 0; j < elements; j++)
                        lock (dic)
                        {
                            if (!dic.TryGetValue(i, out string value))
                                dic[i] = i.ToString();
                        }
            }
            finally
            {
                semaphore.Release();
            }
        }
    }
}

スレッド:1、アイテム:100000、サイクル:10 ConcurrentDictionary:00:00:00.0000499ディクショナリ:00:00:00.0000137

スレッド:10、アイテム:100000、サイクル:10 ConcurrentDictionary:00:00:00.0497413ディクショナリ:00:00:00.2638265

スレッド:100、アイテム:100000、サイクル:10 ConcurrentDictionary:00:00:00.2408781ディクショナリ:00:00:02.2257736

スレッド:1000、アイテム:100000、サイクル:10 ConcurrentDictionary:00:00:01.8196668ディクショナリ:00:00:25.5717232

0
xll

更新:最新のConcurrentDictionaryは大幅に改善され、より多くの機能を備えた私の実装と同じパフォーマンスプロファイルを提供するため、代わりにそれを使用することをお勧めします:)

ロックフリーのスレッドセーフなコピーオンライト辞書の実装について、ここに書いたところです。

http://www.singulink.com/CodeIndex/post/fastest-thread-safe-lock-free-dictionary

書き込みとルックアップの高速バーストは通常​​100%標準のDictionary速度でロックなしで実行されるため非常に高速です。時々書いたり、頻繁に読んだりする場合、これは利用可能な最速のオプションです。

キャッシュに使用したため、現在は追加専用ですが、一般的な需要がある場合は、削除メソッドも追加する可能性があります。

ConcurrentDictionaryは、すべての操作で読み取り/書き込みロックを解除するため、低速です。読み取り/書き込みロックは通常のロックよりもさらに低速ですが、ブロックせずに複数のリーダーを使用できます。

私の実装では、辞書が更新されていないときに通常の状況で読み取りロックが不要になるため、読み取りパフォーマンスが最大になります。トレードオフは、更新が適用された後に辞書をコピーして交換する必要があることです(これはバックグラウンドスレッドで行われます)が、頻繁に書き込むことがない場合、または初期化中に1回だけ書き込む場合は、トレードオフは間違いなく価値がありますそれ。

0
Mike Marynowski