31

私は、ファイルを読み取り、ファイル内の各単語の出現をカウントする非常に単純な「単語カウント」プログラムをコーディングしました。コードの一部は次のとおりです。

class Alaki
{
    private static List<string> input = new List<string>();

    private static void exec(int threadcount)
    {
        ParallelOptions options = new ParallelOptions();
        options.MaxDegreeOfParallelism = threadcount;
        Parallel.ForEach(Partitioner.Create(0, input.Count),options, (range) =>
        {
            var dic = new Dictionary<string, List<int>>();
            for (int i = range.Item1; i < range.Item2; i++)
            {
                //make some delay!
                //for (int x = 0; x < 400000; x++) ;                    

                var tokens = input[i].Split();
                foreach (var token in tokens)
                {
                    if (!dic.ContainsKey(token))
                        dic[token] = new List<int>();
                    dic[token].Add(1);
                }
            }
        });

    }

    public static void Main(String[] args)
    {            
        StreamReader reader=new StreamReader((@"c:\txt-set\agg.txt"));
        while(true)
        {
            var line=reader.ReadLine();
            if(line==null)
                break;
            input.Add(line);
        }

        DateTime t0 = DateTime.Now;
        exec(Environment.ProcessorCount);
        Console.WriteLine("Parallel:  " + (DateTime.Now - t0));
        t0 = DateTime.Now;
        exec(1);
        Console.WriteLine("Serial:  " + (DateTime.Now - t0));
    }
}

シンプルでわかりやすいです。私は辞書を使って各単語の出現を数えます。スタイルは大まかにMapReduceプログラミングモデルに基づいています。ご覧のとおり、各タスクは独自のプライベート辞書を使用しています。したがって、共有変数はありません。単語を自分で数える一連のタスク。コードをクアッドコアi7CPUで実行した場合の出力は次のとおりです。

パラレル:00:00:01.6220927
シリアル:00:00:02.0471171

スピードアップは約1.25で、これは悲劇を意味します。しかし、各行を処理するときに遅延を追加すると、約4のスピードアップ値に達することができます。

遅延のない元の並列実行では、CPUの使用率が30%に達することはほとんどないため、高速化は期待できません。ただし、遅延を追加すると、CPUの使用率は97%に達します。

まず、原因はプログラムのIOバウンドの性質であると思いました(ただし、ディクショナリへの挿入はある程度CPUに負荷がかかると思います)。すべてのスレッドが共有メモリバスからデータを読み取っているため、論理的に思えます。ただし、驚くべき点は、シリアルプログラムの4つのインスタンスを(遅延なしで)同時に実行すると、CPUの使用率が約上昇し、4つのインスタンスすべてが約2.3秒で終了することです。

これは、コードがマルチプロセッシング構成で実行されている場合、約3.5のスピードアップ値に達しますが、マルチスレッド構成で実行されている場合、スピードアップは約1.25であることを意味します。

あなたの考えは?私のコードに何か問題がありますか?共有データはまったくないと思いますし、コードに競合は発生しないと思います。.NETの実行時に欠陥はありますか?

前もって感謝します。

4

3 に答える 3

55

Parallel.For入力をn個に分割しません(nMaxDegreeOfParallelism); 代わりに、多くの小さなバッチを作成し、最大n個が同時に処理されていることを確認します。(これは、1つのバッチの処理に非常に長い時間がかかる場合Parallel.Forでも、他のスレッドで作業を実行できるようにするためです。詳細については、.NETの並列処理-パート5、作業の分割を参照してください。)

この設計により、コードは数十のディクショナリオブジェクト、数百のリストオブジェクト、および数千の文字列オブジェクトを作成して破棄します。これは、ガベージコレクターに大きな圧力をかけています。

コンピューターでPerfMonitorを実行すると、合計実行時間の43%がGCに費やされていると報告されます。使用する一時オブジェクトの数を減らすようにコードを書き直すと、目的の4倍のスピードアップが見られるはずです。PerfMonitorレポートからの抜粋は次のとおりです。

総CPU時間の10%以上がガベージコレクターに費やされました。最もよく調整されたアプリケーションは、0〜10%の範囲です。これは通常、高価なGen2コレクションを必要とするだけの長さのオブジェクトを存続させる割り当てパターンが原因で発生します。

このプログラムのピークGCヒープ割り当て速度は10MB/秒を超えていました。これはかなり高いです。これが単にパフォーマンスのバグであることは珍しいことではありません。

編集:あなたのコメントに従って、私はあなたが報告したタイミングを説明しようとします。私のコンピューターでは、PerfMonitorを使用して、GCに費やした時間の43%から52%を測定しました。簡単にするために、CPU時間の50%が作業であり、50%がGCであると仮定します。したがって、(マルチスレッドを介して)作業を4倍高速化するが、GCの量を同じに保つ場合(これは、処理されるバッチの数が並列構成と直列構成で同じであるために発生します)、最良の方法です。私たちが得ることができる改善は、元の時間の62.5%、つまり1.6倍です。

ただし、GCはデフォルトでマルチスレッド化されていないため(ワークステーションGCでは)、1.25倍のスピードアップしか見られません。ガベージコレクションの基礎に従って、すべての管理対象スレッドは、第0世代または第1世代のコレクション中に一時停止されます。(.NET4および.NET4.5の同時およびバックグラウンドGCは、バックグラウンドスレッドでGen 2を収集できます。)スレッドがほとんどのスレッドを使用するため、プログラムの速度は1.25倍になります(全体でCPU使用率は30%になります)。 GCのために一時停止されている時間(このテストプログラムのメモリ割り当てパターンが非常に貧弱であるため)。

サーバーGCを有効にすると、複数のスレッドでガベージコレクションが実行されます。これを行うと、プログラムは2倍速く実行されます(ほぼ100%のCPU使用率で)。

プログラムの4つのインスタンスを同時に実行すると、それぞれに独自のマネージヒープがあり、4つのプロセスのガベージコレクションを並行して実行できます。これが、100%のCPU使用率が表示される理由です(各プロセスは1つのCPUを100%使用しています)。全体の時間がわずかに長くなる(すべての場合は2.3秒、1つの場合は2.05秒)のは、測定の不正確さ、ディスクの競合、ファイルのロードにかかる時間、スレッドプールの初期化の必要性、コンテキスト切り替えのオーバーヘッドなどが原因である可能性があります。環境要因。

于 2012-12-02T17:11:10.460 に答える
9

結果を説明する試み:

  • VSプロファイラーをすばやく実行すると、CPU使用率が40%に達することはほとんどありません。
  • String.Splitがメインのホットスポットです。
  • したがって、共有された何かがCPUをブロックしている必要があります。
  • その何かはおそらくメモリ割り当てです。あなたのボトルネックは
var dic = new Dictionary<string, List<int>>();
...
   dic[token].Add(1);

これを置き換えました

var dic = new Dictionary<string, int>();
...
... else dic[token] += 1;

その結果、2倍のスピードアップに近づきます。

しかし、私の反対の質問は次のようになります:それは重要ですか?あなたのコードは非常に人工的で不完全です。並列バージョンでは、マージせずに複数の辞書を作成することになります。これは実際の状況にさえ近いものではありません。ご覧のとおり、詳細はほとんど重要ではありません。

サンプルコードは複雑で、について幅広いステートメントを作成しParallel.ForEach()ます。
実際の問題を解決/分析するには単純すぎます。

于 2012-12-02T17:09:28.760 に答える
0

楽しみのために、ここに短いPLINQバージョンがあります:

File.ReadAllText("big.txt").Split().AsParallel().GroupBy(t => t)
                                                .ToDictionary(g => g.Key, g => g.Count());
于 2016-05-11T17:03:12.353 に答える