アプリのパフォーマンスとメモリのトラフィックを最適化する
メモリトラフィックがアプリケーションのパフォーマンスに大きな影響を与えることはよく知られています。トラフィックが多いほど、アプリが遅くなります。 問題は、アプリケーションがどれだけ頻繁にメモリを割り当てるか(これはパフォーマンス的にはほぼ無料です)ではなく、不要になったメモリをどのように回収するかにあります。 これを行うガベージコレクション(GC)機構の便利さには、残念ながらコストが伴います。
まず、GC 自体が CPU 時間を必要とします。 例えば、そのステージの一つは未使用オブジェクトの検出であり、オブジェクト参照グラフを構築する複雑な操作です。 次に、Gen0 および Gen1 GC を実行するために、ガベージコレクターはマネージドヒープの一部に排他的にアクセスする必要があります。 この結果、「ブロッキング GC」を発生させたもの以外のすべてのマネージドスレッドが一時停止します。UI インターフェーススレッドも一時停止するため、ユーザーはこの瞬間に UI のフリーズを体験する可能性があります。
そのため、常にアプリケーションを最適化してメモリトラフィックを削減し、GC がアプリケーションの応答性に与える影響を最小限に抑える必要があります。
このチュートリアルでは、タイムラインプロファイリングを使用して過剰な GC とその原因を検出する方法を学習します。
サンプルアプリケーション
タイムラインプロファイリングを始める チュートリアルと同じ基本的なサンプルアプリケーションを使用します。 このアプリケーションは、テキストファイル内の行を逆順にするために使用されます。例: ABC => CBA。
アプリのソースコードは github(英語) で入手できます。

ファイルの選択 ボタンを使用すると、ユーザーは処理するテキストファイルを選択します。 プロセスファイル ボタンは、ファイル内の行を反転する別の BackgroundWorker スレッド(FileProcessing )を実行します。 メインウィンドウの左隅にあるラベルに進捗が表示されます。 処理が終了すると、ラベルに すべてのファイルが正常に処理されました が表示されます。
次のような状況を想像してください。アプリケーションをテストしていると、テキストファイルの処理速度が予想より遅いことに気付きます。 さらに、ファイル処理中に軽微なラグが発生する場合もあります。
タイムラインプロファイリングを使用して、これらのパフォーマンスの欠点を分析してみましょう。
ステップ 1. プロファイラの実行とスナップショットの取得
Visual Studio で MassFileProcessing.sln ソリューションを開きます。
を選択してプロファイラを実行します。
プロファイリングタイプ で、 タイムライン を選択します。

実行 をクリックします。 dotTrace はアプリケーションを実行し、プロファイリングプロセスを管理する専用のコントローラーウィンドウを表示します。

さて、アプリでパフォーマンスの問題を再現してみましょう。
ファイルの選択 をクリックし、 Text Files フォルダー内のアプリケーション付属のテキストファイルを 5 つ選択してください。

ファイル処理を開始するには、 プロセスファイル をクリックします。
処理が終了したら、コントローラーウィンドウで スナップショットを取得して待機する をクリックして、タイムラインプロファイリングスナップショットを収集します。 スナップショットは Visual Studio の別の パフォーマンスプロファイラ ツールウィンドウで開きます。
アプリケーションを閉じます。 これにより、コントローラーウィンドウも閉じます。
ステップ 2. 分析の開始
パフォーマンスプロファイラ ツールウィンドウで、 スレッドを表示 をクリックします。 これにより、別の スレッド ツールウィンドウでアプリケーションスレッドの一覧が開きます。

dotTrace が複数のマネージドスレッドを検出したことがわかります。 これらは、 Main スレッド、バックグラウンド GC を実行する ガベージコレクション スレッド、およびファイル処理用の FileProcessing スレッド*です。 さらに、何もしていないスレッドが 2 つあり、 ファイナライザーオブジェクトのファイナライズ処理用スレッド スレッドと補助的な スレッドプール です。
ファイル処理の遅さが問題なので、 FileProcessing スレッドがファイルを処理している時間にズームインしてみましょう。 これを行うには、 スレッド ダイアグラムでマウスホイールを使用してください。

すると、表示されている時間範囲で自動的にフィルターが追加されます(1950 ミリ秒)。 このフィルターが他のフィルターにどのような影響を与えるかに注目してください。すべての値が表示範囲の時間に合わせて再計算されます。 現在スナップショットデータに適用されているフィルターは 「すべてのスレッドの表示可能な時間範囲内のすべての時間間隔を選択する」 です。 ファイル処理にかかるおおよその時間として 2 秒を覚えておきましょう。
プロセス概要ダイアグラム(スレッド ウィンドウの上部)で、 GC バーを参照してください。 多くのブロッキング GC がファイル処理中に実行されるようです。 これは、大きなメモリトラフィックを示し、疑いなくアプリケーションのパフォーマンスに影響を与えます。
このトラフィックの真の背景を見てみましょう。 これには 2 つの方法があります。
GC 中に実行されているスレッドとメソッドを特定します。 これらは、これらのコレクションを切り替えたスレッドとメソッドでなければなりません。
メモリの大部分を割り当てる方法を特定します。 ロジックは単純です: GC を切り替える主な理由は、メモリ割り当てのためにヒープのサイズを変更することです。 メソッドがメモリを多く割り当てると、GC も多くトリガされます。
先を見れば、第 2 の方法ははるかに簡単で、少し信頼性が高いことがわかります。 それにもかかわらず、教育目的のために、両方を試してみましょう。
ステップ 3. メモリトラフィックはどこから発生しているのでしょうか? ガベージコレクションを分析する
スレッド ウィンドウ(または パフォーマンスプロファイラ ウィンドウ)上部のフィルター一覧で、 ブロッキング GC フィルターの ブロッキング GC の値を選択します。 結果のフィルターは 「表示中のすべてのスレッドの表示可能な時間範囲内でブロッキング GC が発生するすべての時間間隔を選択する」 となります。

ここで、GC が発生するスレッドを特定しましょう .* 例: これは Main スレッドと仮定できます。
スレッド ダイアグラムで Main スレッドを選択します。 結果のフィルターは 、「ブロッキング GC が発生するメインスレッドの表示可能な時間範囲内のすべての時間間隔を選択する」になります。

スレッドの状態 フィルターを開いて参照してください。 これで、すべての GC 中にメインスレッドが何をしていたかが表示されます。 ほとんどの時間(97.3% )は 待機 でした。 これは GC が別のスレッド(明らかに FileProcessing スレッド)で発生し、メインスレッドが GC の完了まで待っていたことを意味します。

では FileProcessing スレッドのどのメソッドがこれらの GC の原因なのかを調べてみましょう。
メインスレッドでフィルターを削除します。 これを行うには、スレッドリストの対応するチェックボックスをオフにします。
代わりにリストで FileProcessing スレッドを選択してください。 結果のフィルターは 「FileProcessing スレッドの表示可能な時間範囲内でブロッキング GC が発生するすべての時間間隔を選択する」 となります。

スレッドの状態 フィルターを参照してください。 それによると、GC 時間の 99.1% は FileProcessing スレッドが 実行中 でした。 このことから、このスレッドがこれらの GC の原因であることが確認できます。

スレッドの状態 の 実行中 を選択してください。
パフォーマンスプロファイラ ツールウィンドウで ホットスポット をクリックすると、自己時間が最も多いユーザーメソッドのリストが表示されます。

これで適用されたすべてのフィルターを考慮し、 ホットスポット は GC をトリガーした主要なメソッド*を表示します。 ご覧のとおり、
StringReverser.Reverseはアプリケーション内で最も多くのメモリトラフィックを生成している可能性があるメソッドです。
ステップ 3. メモリトラフィックはどこから発生しているのでしょうか? メモリの割り当てを分析する
次に、メモリトラフィックを分析する簡単な方法を試してみましょう。 先に記述されていたように、最大のメモリ量を割り当てる方法を特定することが考えられます。
パフォーマンスプロファイラ または スレッド ツールウィンドウで
ボタンをクリックして、すべてのフィルターを削除します。イベント フィルターで、 メモリの割り当て* を選択します。

スレッド ダイアグラムを参照してください。 FileProcessing スレッドは、5882 MB の膨大なメモリを割り当てます。 私たちのアプリで高メモリトラフィックに責任があることは間違いありません。
ホットスポット を参照してください。 これで、メソッドが割り当てたメモリ量でソートされます。
StringReverser.Reverseメソッドは 5496 MB で大きく離れています。
ステップ 4. コードの改善
メソッドのコードを見て、何が間違っているのか調べてみましょう。
ホットスポット 内の
StringReverser.Reverseメソッドを右クリックし、コンテキストメニューで コードに移動する を選択してください。
コードを参照してください。 この方法は、テキストファイルから行を逆にするために使用されるようです。 入力時に文字列を受け取り、その逆の文字列を返します。
public string Reverse(string line) { char[] charArray = line.ToCharArray(); string stringResult = null; for (int i = charArray.Length; i > 0; i--) { stringResult += charArray[i - 1]; } return stringResult; }どうやら問題は、文字列を逆順にする方法にあるようです。 文字列は不変型であり、一度作成した後は内容を変更できないという点にあります。 つまり、
stringResultに+=操作で文字を追加するたびに、新しい文字列がメモリ上に確保されます。 特別なStringBuilderクラスを利用したり、文字列をcharの配列として処理してReverseメソッドを使うと、はるかに効果的です。 オプションのうち後者を試してみましょう。下記のように
StringReverser.Reverseの方法を修正してください。public string Reverse(string line) { char[] charArray = line.ToCharArray(); Array.Reverse(charArray); return new string(charArray); }
ステップ 5. 修正の確認
ステップ 1 に従って、ソリューションを再構築し、もう一度プロファイリングを実行します。
スナップショットを開いたら、 イベント を メモリの割り当て に切り替えます。
スレッド ダイアグラムを参照してください。 改善がうまくいきました! メモリトラフィックは FileProcessing スレッドでほぼ 6 GB から 166 MB に減少しました。

FileProcessing がファイルを処理した時間範囲にズームインすると、ファイル処理が大幅に高速化されたことが分かります。 いまでは、修正前が 2 秒だったのに対し、すべてのファイル処理が約 300 ms で完了します。