3

文字列キーを持つデータを辞書に保存しようとしています。データは非常に大きく、たとえば数千万の文字列です。そのため、より高速な実行を実現するために、並行バージョンを開発することにしました。ただし、並行バージョンのパフォーマンスは非常に悪いです。

私は 2 つの戦略を使用しました:
1- 入力を 2 つのチャンクに分割し、2 つの同時スレッドを使用して各チャンクを 2 つの異なる辞書に挿入します。
2- Parallel.ForEach 呼び出しを使用して、データ全体を ConcurrentDictionary に挿入します。

しかし残念なことに、どちらの戦略のパフォーマンスも有望ではありません。最初の戦略は約20 ~ 30%優れていますが、タスク間でデータが共有されていないため、十分ではありません。また、同時収集は約100% 遅くなります

今、私は何が問題なのか疑問に思っています??????? これは、この問題で並列速度が向上する可能性がないことを意味しますか? 誰かが私を助けることができれば、私は感謝します:)

以下にサンプルコードを添付しました。
私のデュアルコア AMD Turion システムでのサンプル結果は (ミリ秒単位):
初期化: 542
シリアル: 294
並列: 234
同時 Dic: 666

    static void Main(string[] args)
    {
        System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
        watch.Start();
        Random r = new Random();
        int count=1000000;
        string[] list = new string[count];
        for (int i = 0; i < count; i++)
        {
            list[i] = r.Next(10000).ToString();
        }

        watch.Stop();
        Console.WriteLine("Initialization: "+watch.ElapsedMilliseconds);
        watch.Reset();
        watch.Start();

        Dictionary<string, byte> dic1 = new Dictionary<string, byte>();
        Dictionary<string, byte> dic2 = new Dictionary<string, byte>();
        foreach (var s in list)
            dic1[s] = 0;

        watch.Stop();
        Console.WriteLine("Serial: " + watch.ElapsedMilliseconds);
        watch.Reset();
        watch.Start();


        dic1.Clear();

        Task t1 = new Task(
            () =>
            {
                for (int i = 0; i < list.Length / 2; i++)
                    dic1[list[i]] = 1;
            }
            );
        Task t2 = new Task(
            () =>
            {
                for (int i = list.Length / 2; i < list.Length; i++)
                    dic2[list[i]] = 1;
            }
            );

        t1.Start();
        t2.Start();
        Task.WaitAll(t1, t2);

        watch.Stop();
        Console.WriteLine("Parallel: " + watch.ElapsedMilliseconds);
        watch.Reset();
        watch.Start();

        ConcurrentDictionary<string, byte> dicp = new ConcurrentDictionary<string, byte>();
        Parallel.ForEach(list, s =>
            {
                dicp.AddOrUpdate(s, 1, (k, v) => v);
            }
        );

        watch.Stop();
        Console.WriteLine("Concurrent Dic: " + watch.ElapsedMilliseconds);
        watch.Reset();
        watch.Start();

        Console.ReadKey();

        return;

    }
4

5 に答える 5

2

ConcurrentDictionary遅い理由は簡単に説明できます。エントリにアクセスするにはロックが必要です。高負荷向けには作られていません。

Task最初の に基づくベンチマークで大幅な高速化が見られなかった理由を説明するのは、はるかに困難です。持っているはずです。同期をほとんど行わずに作業を正しく分割しました。

たぶん、タスクの 1 回の起動コストは約 100 ミリ秒ですか? ループ内でベンチマークを 10 回繰り返してみてください。結果は前回の実行と同じですか?

新しい辞書を作成してみてください。古いものを再利用すると、古いテストから状態が引き継がれます: 事前にサイズ設定された内部配列。

HansPassant はコメントの中で、メモリ帯域幅に縛られている可能性があると述べています。そうではないと思います。ディクショナリはそれほど安くはない計算を内部で行い、最新のシステムはそれほど帯域幅に縛られていません。それらは遅延に依存している可能性がありますが、帯域幅ではありません.

于 2012-08-12T22:59:03.643 に答える
1

あなたが思い付くことができるいくつかの最適化があります。1. 非常に膨大な量のデータがあるとおっしゃっていたので、辞書の初期サイズを大きな数値に指定してみてください (おおよそ、辞書に保存する量とほぼ同じです) 2. この場合、マルチスレッドを避けるようにしてください-挿入がすべてである場合、ここには何のメリットもありません。

于 2012-08-12T22:47:03.753 に答える
0

あなたはあなたの問題に対してシリアル、パラレル、そしての3つの解決策を提案しましたConcurrentDictionary

まず、2番目の解決策(並列の解決策)は、1番目と3番目とはまったく異なる質問の解決策です。結果は単一の辞書ではありませんが、他の2つのソリューションは単一の辞書になります。パラレルの方がパフォーマンスが良いように見えるのは、まだ終わっていないからです。マージするためのもう1つのステップがdic1ありdic2ます。

とにかく、並列ソリューションを同じ辞書に追加したい場合は、lock競合状態を回避するためにを配置する必要があります。追加のためにロックをかけると、並列ソリューションは3番目のソリューションと同じようになります(ConcurrentDictionaryはロックを使用する方が優れている可能性があるため、3番目のソリューションの方が少し速い場合があります)。

ところで、入力するリスト(または辞書)があり、すべての入力データの準備ができている場合、ボトルネック部分のアルゴリズムは単なるAddコストです。を使用しているために複数のスレッドを使用して追加を行っている場合でもlock、各スレッドは他のスレッドが追加ジョブを完了するのを待つ必要があります。したがって、実際には、多数のスレッド(タスク)が相互に待機しているため、ソリューションはシーケンシャルなもののように実行され、コンテキスト切り替えのオーバーヘッドが発生します。

結局のところ、並列プログラミングはこの状況ではまったく役に立ちません。アルゴリズムを最適化するには、追加部分をより最適化する必要があります。たとえば、辞書の初期サイズを設定できます(addメソッドがリスト拡張部分を実行しないようにするのに役立ちます)。または、より高速なComparerを定義することもできます(文字列が大きすぎる状況では)。個人的には、最適化された比較器を設計すると、状況のパフォーマンスに大きな影響を与えると思います。比較アルゴリズムでハッシュを使用できます。

于 2012-12-07T09:08:00.393 に答える
0

辞書は、あなたのように膨大な量 (数千万) のエントリを保持するようには設計されていません。実際、ASP.NET に対する攻撃があります。これは、asp.net ディクショナリがかなり早い段階でハッシュ衝突を起こし始めるという事実に正確に依存しています。

これは、通常は O(1) ではなく O(n) (n は衝突したキーの数) である衝突回避メカニズムに依存する必要があることを意味します。これは、攻撃によって実証されたように、辞書をかなり遅くする可能性があります。

ハッシュの衝突とロック メカニズムを組み合わせると、速度が大幅に低下します。

また、並列タスクは、写真の処理など、時間がかかり、相互に多くのデータを共有しないルーチンを対象としていることにも注意してください。ディクショナリへのエントリの追加は、衝突があっても非常に高速であり、ロックおよび口蓋化機能は大幅に遅くなります。これは、作成する必要がある単一のディクショナリ (並列処理のボトルネック) があるという事実と相まって、ディクショナリの初期化に並列で時間がかかる理由の説明があります。

これが理にかなっていることを願っています。

于 2012-08-12T22:42:44.683 に答える
0

1) 言われた。ディクショナリ コンストラクタに初期サイズを指定します。これにより、少なくともこの数のエントリ/バケットを割り当てるように構造が強制されます。

2) 可能であれば、より短い文字列を使用できるかどうかを確認します。

3) 内部的に、Dictionary は間違いなく多くの文字列比較を行っています。独自の文字列比較子を Dictionary コンストラクターに渡します。

new Dictionary<string, string>(StringComparer.Ordinal);
于 2012-08-12T23:11:42.557 に答える