3

開発サイトにある Event Viewer アプリケーションのパフォーマンスの問題を調査していると、アルゴリズムに興味深い問題があることに気付きました。次に、単純化されたテスト プロジェクトを作成して、2 つの異なるアルゴリズムをテストしました。このプログラムは基本的に、クラスを使用して Windows イベント ログを取得し、それらのログをクエリ可能なエンティティEventLogに変換します。EventLogItem

この操作は、2 つの異なるループを使用して実行され、タイミングがとられます。最初の (逆方向の) ループは、リストの最後の項目のインデックスから開始し、項目を変換してからインデックスを減らします。メソッドは次のように定義されます。

private static void TranslateLogsUsingBackwardLoop()
{
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();

    var originalLogs = EventLog.GetEventLogs();
    var translatedLogs = new List<EventLogItem>();

    Parallel.ForEach<EventLog>(originalLogs, currentLog =>
    {
        for (int index = currentLog.Entries.Count - 1; index >= 0; index--)
        {
            var currentEntry = currentLog.Entries[index];

            EventLogItem translatedEntry = new EventLogItem
            {
                MachineName = currentEntry.MachineName,
                LogName = currentLog.LogDisplayName,
                CreatedTime = currentEntry.TimeGenerated,
                Source = currentEntry.Source,
                Message = currentEntry.Message,
                Number = currentEntry.Index,
                Category = currentEntry.Category,
                Type = currentEntry.EntryType,
                InstanceID = currentEntry.InstanceId,
                User = currentEntry.UserName,
            };

            lock (translatedLogs)
            {
                translatedLogs.Add(translatedEntry);
            }
        }
    });

    stopwatch.Stop();

    Console.WriteLine("{0} logs were translated in {1} using backward loop.", translatedLogs.Count, stopwatch.Elapsed);
}

2 番目の (順方向) ループは、インデックス 0 から開始し、インデックスをインクリメントします。このメソッドは次のように定義されています。

private static void TranslateLogsUsingForwardLoop()
{
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();

    var originalLogs = EventLog.GetEventLogs();
    var translatedLogs = new List<EventLogItem>();

    Parallel.ForEach<EventLog>(originalLogs, currentLog =>
    {
        for (int index = 0; index < currentLog.Entries.Count; index++)
        {
            var currentEntry = currentLog.Entries[index];

            EventLogItem translatedEntry = new EventLogItem
            {
                MachineName = currentEntry.MachineName,
                LogName = currentLog.LogDisplayName,
                CreatedTime = currentEntry.TimeGenerated,
                Source = currentEntry.Source,
                Message = currentEntry.Message,
                Number = currentEntry.Index,
                Category = currentEntry.Category,
                Type = currentEntry.EntryType,
                InstanceID = currentEntry.InstanceId,
                User = currentEntry.UserName,
            };

            lock (translatedLogs)
            {
                translatedLogs.Add(translatedEntry);
            }
        }
    });

    stopwatch.Stop();

    Console.WriteLine("{0} logs were translated in {1} using forward loop.", translatedLogs.Count, stopwatch.Elapsed);
}

そして主な方法:

static void Main(string[] args)
{
    TranslateLogsUsingForwardLoop();
    Console.WriteLine();
    Thread.Sleep(2000);
    TranslateLogsUsingBackwardLoop();
    Console.ReadLine();
}

これは私が得たものです(このテストを数回実行しましたが、結果はほぼ同じです):

ここに画像の説明を入力

私がこれをテストしたサーバーは、毎秒イベント ログにログを記録したことに注意してください。そのため、変換されたログの数は同じではありません。では、なぜ後方ループの方が速いのでしょうか? 最初は、逆方向ループ アルゴリズムでcurrentLog.Entries.Count は が 1 回だけ評価されるためだと考えていましたが、順方向ループではループの反復ごとに計算して比較する必要がありますindexが、これも正しくないようです。何か案は?

4

3 に答える 3

0

maxndex に対して 0 をテストしても、ほとんど効果がない可能性があります。ただし、test1 を実行してから test2 を実行すると、プロセッサのキャッシュや O/S ページのキャッシュが原因で影響を受けることがよくあります。test1/test2 を逆にして、前進が魔法のように後退よりも速くなるかどうかを確認できます。最新のアーキテクチャーでは、正確なプロファイリングは困難です。

OK、最初に実行すると、Backwards はまだ Faster です。私の最初の推測ではありませんが、Parallel と lock を使用しているため、ロック方法と前方ループと後方ループの違いとの間に相互作用がある可能性があります。

おそらく、逆方向ループはたまたまプロセッサーの分岐予測でうまく機能するだけかもしれません (ここでも、並列処理、プロセッサーのキャッシュなどと相互作用する可能性があります)。

マルチスレッド コードのタイトなループの多くは、ロック オーバーヘッドが原因で、メモリ管理との奇妙なやり取りをします。-- ロックの競合が原因でマルチスレッド ソリューションが遅くなることも珍しくありません

順方向と逆方向の両方で並列なしで実行してみて、時間がより均等になるかどうかを確認できます。コードのプロファイリングは示唆に富むかもしれませんが、明確な答えが得られない場合もあります。この状況では、決定的な答えは非常に難しい場合があります (ほとんどの場合、好奇心旺盛で学習モードにあると想定していました)。

于 2013-08-23T21:26:18.207 に答える
0

最初のループが遅いのは、それが最初だからであり、順方向だからではありません。

キャッシング

最新の CPU はデータをキャッシュします (レベル 1 およびレベル 2 キャッシュ内)。これは、データが最初にアクセスされるときは遅く、その後のアクセスでは速くなります。

   var currentEntry = currentLog.Entries[index];

最初のループは、低速の RAM から L2 キャッシュにロードするため、時間がかかります。

2 番目のループは、L2 キャッシュからロードするため、記述方法に関係なく高速になると予想されます。

List<T>

リストは拡大し続ける配列です。最初は小さく (容量 4)、必要に応じて容量を 2 倍にします。各再割り当ては非常に遅いです。

  var translatedLogs = new List<EventLogItem>();
  ...

  translatedLogs.Add(translatedEntry);

最初のループはかなり頻繁に再割り当てされます: 4、8、16、32、64

2 番目のループでは再割り当ての頻度が低くなります: 64, 128

したがって、2 番目のループは (どのように記述されているかに関係なく) 高速であると期待できます。

CPU の最適化

プロセッサが非常に複雑であるため、奇妙なことが起こります。昔のようにコード速度を予測することはできなくなりました:-)

ソートされた配列の処理がソートされていない配列よりも速いのはなぜですか?

于 2016-06-01T15:50:01.240 に答える