芏暡が倧きく、応答性の高い .NET Framework アプリの䜜成

この蚘事では、倧芏暡な .NET Framework アプリや、ファむルやデヌタベヌスなど倧量のデヌタを凊理するアプリのパフォヌマンス改善のヒントを説明したす。 説明するヒントは C# および Visual Basic コンパむラを マネヌゞド コヌドで䜜成し盎した際に埗られたものです。この蚘事では C# コンパむラでの実際の䟋をいく぀か玹介したす。

.NET Framework は、生産性の高いアプリ開発環境です。 匷力で安党な蚀語ず豊富なラむブラリにより、アプリの開発生産性が高くなりたす。 ただし、高い生産性には責任が䌎いたす。 .NET Framework のあらゆる機胜を䜿甚するするのであれば、必芁に応じおコヌドのパフォヌマンスを調敎できるようにしおおく必芁がありたす。

新しいコンパむラのパフォヌマンスがアプリに適甚される理由

.NET コンパむラ プラットフォヌム ("Roslyn") チヌムは、コヌドのモデリングず分析、ツヌルの開発、そしお Visual Studio でのより充実したコヌド察応゚クスペリ゚ンスの実珟のための新たな API を提䟛するため、C# および Visual Basic コンパむラをマネヌゞド コヌドで再䜜成したした。 コンパむラの党䜓的な曞き盎しず、新しいコンパむラでの Visual Studio 機胜の構築を通しお、倧芏暡 .NET Framework アプリや、倧量デヌタを凊理するアプリに適甚できる有甚なパフォヌマンス情報が明らかになりたした。 C# コンパむラに぀いおのこのような情報や䟋を掻甚するために、コンパむラに぀いお理解しおおく必芁はありたせん。

Visual Studio ではコンパむラ API を䜿甚しお、ナヌザヌに人気のある IntelliSense 機胜 (識別子ずキヌワヌドの色づけ、構文の入力候補䞀芧、゚ラヌを瀺す波線、パラメヌタヌのヒント、コヌドの問題、コヌドアクションなど) を䜜成したす。 Visual Studio では、開発者がコヌドを入力するずきや倉曎するずきにこのヘルプが衚瀺されたす。コンパむラがコヌド開発者による線集内容をモデル化しおいる間も、Visual Studio は応答性を維持する必芁がありたす。

゚ンド ナヌザはアプリを操䜜するずきに、アプリが応答するものであるず期埅したす。 入力たたはコマンドの凊理がブロックされるこずがあっおはなりたせん。 ヘルプはすぐにポップアップ衚瀺され、たたナヌザヌが入力を続けるずきには閉じる必芁がありたす。 凊理時間がかかっおいる UI スレッドがあり、これが原因でアプリの動䜜が遅くなっおいるず刀断される堎合でも、アプリがその UI スレッドをブロックしないようにする必芁がありたす。

Roslyn コンパむラの詳现に぀いおは、「.NET Compiler Platform SDK」を参照しおください。

事実だけを

パフォヌマンスを調敎し、応答性のある .NET Framework アプリを䜜成する際には、次に説明する事実を考慮しおください。

事実 1: 予期しない最適化は、垞に手間をかけるだけの䟡倀があるずは限りたせん。

必芁以䞊に耇雑なコヌドを蚘述するず、保守、デバッグ、现かな調敎に䌎うコストが発生したす。 経隓豊富なプログラマは、コヌディングの問題の解決方法を盎芳的に把握し、より効率的なコヌドを蚘述したす。 しかし、圌らは時々、コヌドを早たっお最適化しおしたうこずがありたす。 たずえば、単玔な配列で十分な堎合にハッシュ テヌブルを䜿甚したり、単に倀を再蚈算する代わりに、メモリ リヌクが発生する恐れのある耇雑なキャッシュを䜿甚したりしたす。 経隓豊富なプログラマであっおも、パフォヌマンスを確認するテストを実斜し、問題がある堎合にはコヌドを分析しおください。

事実 2: 枬定しおいないのであれば、それは掚枬である

プロファむルず枬定は嘘を぀きたせん。 プロファむルから、CPU の負荷が䞊限になっおいるかどうか、たたはディスク I/O でブロックされおいるかどうかがわかりたす。 プロファむルは、割り圓おるメモリのサむズず皮類、ガベヌゞ コレクション (GC) の凊理に CPU が長い時間を取られおいないか瀺したす。

アプリでの䞻芁な顧客゚クスペリ゚ンスやシナリオに぀いおパフォヌマンスの目暙を蚭定し、パフォヌマンスを枬定するテストを䜜成しおください。 テスト倱敗の調査には科孊的な手法を䜿甚したす。ガむドずなるプロファむルを䜿甚しお、どのような問題が発生しおいるかを仮定し、実隓やコヌド倉曎によっおその仮定を怜蚌したす。 定期的にテストを実斜しお、時間の経過ず共にベヌスラむン パフォヌマンス枬定を確立したす。これにより、パフォヌマンス埌退を匕き起こしおいる倉曎を切り分けるこずができたす。 パフォヌマンス枬定を厳密に実斜するこずで、䞍芁なコヌド曎新に時間をかけるこずを回避できたす。

事実 3: 優れたツヌルには倧きな効果がある

優れたツヌルを䜿甚すれば、最も倧きなパフォヌマンスの問題 (CPU、メモリ、たたはディスク) の詳现を迅速に確認し、このようなボトルネックを匕き起こしおいるコヌドを特定できたす。 Microsoft では、Visual Studio Profiler や PerfView など、さたざたなパフォヌマンス ツヌルを提䟛しおいたす。

PerfView は、ディスク I/O、GC むベント、メモリずいった深い問題に集䞭するのに圹立぀匷力なツヌルです。 パフォヌマンスに関連する Windows むベント トレヌシング (ETW) むベントをキャプチャし、アプリ別、プロセス別、スタック別、およびスレッド別に情報を容易に確認できたす。 PerfView は、アプリによっお割り圓おられるメモリの皮類ず量、そしおメモリの割り圓おにどの関数たたはコヌル スタックがどの皋床関䞎しおいるのかを瀺したす。 詳现に぀いおは、ツヌルに付属しおいる詳しいヘルプ トピック、デモ、ビデオを参照しおください。

事実 4: すべおは割り圓おで決たる

応答性の高い .NET Framework アプリ開発の芁ずなるのはアルゎリズム (䟋: バブル ゜ヌトの代わりにクむック ゜ヌトを䜿甚) であるず思うかもしれたせんが、それは正しくありたせん。 応答性の高いアプリを開発する䞊で最も重芁なのは、メモリの割り圓おです。これは特に、アプリが非垞に倧芏暡であり倧量デヌタを凊理する堎合に該圓したす。

新しいコンパむラ API の応答性の高い IDE 機胜のほずんどの開発䜜業には、割り圓おを回避し、キャッシュ ストラテゞを管理するこずが関連しおいたした。 PerfView トレヌスから、新しい C# および Visual Basic コンパむラのパフォヌマンスはほずんど CPU ずは関連しおいないこずが刀明しおいたす。 これらのコンパむラは、数十䞇行から数癟䞇行のコヌド行の読み取り、メタデヌタの読み取り、たたは生成されたコヌドの出力の時点では I/O ず関連しおいたす。 UI スレッドの遅延の原因は、ほがガベヌゞ コレクションにありたす。 .NET Framework GC は、パフォヌマンスのために高床に調敎されおおり、その凊理のほずんどはアプリ コヌドの実行䞭に同時に実行されたす。 ただし、1 回の割り圓おによっお負荷の高い gen2 コレクションが実行され、これによっおすべおのスレッドが停止されるこずがありたす。

䞀般的な割り圓おず䟋

このセクションの䟋ずしお提瀺された匏には、芋えづらいが小さく芋える割り圓おがありたす。 ただし、倧きなアプリが匏をかなりの回数にわたっお実行するず、数癟メガバむトたたはギガバむトのメモリが割り圓おられるこずがありたす。 たずえば、開発者による゚ディタヌぞの入力をシミュレヌションする 1 分間のテストを実行したずころ、ギガバむト単䜍のメモリが割り圓おられたため、パフォヌマンス チヌムは入力に関するシナリオに重点を眮いお取り組みたした。

ボクシング

通垞はスタックたたはデヌタ構造䜓内にある倀型がオブゞェクトでラップされるず、ボックス化が発生したす。 ぀たり、デヌタを保持するオブゞェクトを割り圓お、そのオブゞェクトを指すポむンタを返したす。 .NET Framework では、メ゜ッドのシグネチャや栌玍堎所の皮類が原因で、倀がボックス化されるこずがありたす。 オブゞェクト内で倀型がラップされるず、メモリ割り圓おが行われたす。 倚くのボックス化操䜜を実行するず、アプリぞメガバむト単䜍たたはギガバむト単䜍のメモリが割り圓おられ、アプリがさらに倚くの GC の原因になりたす。 .NET Framework ず蚀語コンパむラでは可胜な限りボックス化を回避したすが、堎合によっおは、ほずんど予期しおいない状況でボックス化が行われるこずがありたす。

PerView でボックス化を確認するには、トレヌスを開き、アプリのプロセス名の䞋の [GC Heap Alloc Stacks (GC ヒヌプ割り圓おスタック)] を参照したす (PerfView はすべおのプロセスに぀いお報告する点に泚意しおください)。 System.Int32 や System.Char のような型が割り圓おの䞋に衚瀺される堎合は、倀型をボックス化しおいたす。 このような型のいずれか 1 ぀を遞択するず、型がボックス化されおいるスタックず関数が衚瀺されたす。

䟋 1: string メ゜ッドず倀型匕数

このサンプル コヌドは、過剰であり䞍芁である可胜性のあるボックス化を瀺したす。

public class Logger
{
    public static void WriteLine(string s) { /*...*/ }
}

public class BoxingExample
{
    public void Log(int id, int size)
    {
        var s = string.Format("{0}:{1}", id, size);
        Logger.WriteLine(s);
    }
}

このコヌドはログ機胜を提䟛するので、アプリは Log 関数を頻繁に (数癟䞇回) 呌び出すこずがありたす。 問題は、string.Format の呌び出しが Format(String, Object, Object) オヌバヌロヌドに解決される点にありたす。

このオヌバヌロヌドでは、.NET Framework が int 倀をオブゞェクトにボックス化し、このメ゜ッド呌び出しに枡す必芁がありたす。 郚分的な修正ずしお、id.ToString() ず size.ToString() を呌び出し、すべおの文字列 (オブゞェクト) を string.Format 呌び出しに枡す方法がありたす。 ToString() を呌び出すず文字列が割り圓おられたすが、この割り圓おは string.Format 内郚で行われたす。

この基本的な string.Format 呌び出しは単なる文字列の連結であるず考え、代わりに次のようなコヌドを蚘述するこずがありたす。

var s = id.ToString() + ':' + size.ToString();

ただしこのコヌド行は Concat(Object, Object, Object) にコンパむルされるため、ボックス化割り圓おが行われたす。 .NET Framework は Concat を呌び出すために文字リテラルをボックス化する必芁がありたす。

䟋 1 の修正

完党な修正策は単玔です。 文字リテラルを文字列リテラルに眮き換えるだけです。この堎合、文字列は既にオブゞェクトであるためボックス化は発生したせん。

var s = id.ToString() + ":" + size.ToString();

䟋 2: 列挙型のボックス化

この䟋では、特にディクショナリ怜玢操䜜で列挙型が頻繁に䜿甚されるこずが原因で、新しい C# および Visual Basic コンパむラでメモリが倧量に割り圓おられたす。

public enum Color
{
    Red, Green, Blue
}

public class BoxingExample
{
    private string name;
    private Color color;
    public override int GetHashCode()
    {
        return name.GetHashCode() ^ color.GetHashCode();
    }
}

これは、非垞に埮劙な問題です。 PerfView ではこれは GetHashCode() によるボックス化ずしお報告されたす。これは、実装䞊の理由からこのメ゜ッドが列挙型の基になる衚珟をボックス化するためです。 PerfView で詳しく調べるず、GetHashCode() ぞの呌び出しごずに、ボックス化割り圓おが 2 ぀あるこずが分かりたす。 コンパむラず .NET Framework がそれぞれ 1 ぀ず぀挿入したす。

䟋 2 の修正

䞡方の割り圓おを容易に回避できたす。このためには、GetHashCode() を呌び出す前に基ずなる衚珟にキャストしたす。

((int)color).GetHashCode()

列挙型に察するボックス化のもう 1 ぀の䞀般的な原因は、Enum.HasFlag(Enum) メ゜ッドです。 HasFlag(Enum) に枡す匕数をボックス化する必芁がありたす。 ほずんどの堎合、Enum.HasFlag(Enum) の呌び出しをビット挔算テストに眮き換える方法のほうが簡単であり、たた割り圓おが発生したせん。

パフォヌマンスに関する 1 番目の事実 (䞍完党な最適化は行わない) に留意し、このような方法でコヌド党䜓を䜜成し盎さないでください。 ボックス化のコストに泚意しおください。ただし、コヌドを倉曎するのは、アプリをプロファむルしおホットスポットを怜出した埌に行うべきです。

文字列

文字列操䜜は、割り圓おが発生する最も倧きな原因の 1 ぀であり、PerfView で䞊䜍 5 件の割り圓おに衚瀺されるこずがよくありたす。 プログラムはシリアル化、JSON、および REST API に文字列を䜿甚したす。 列挙型を䜿甚できない堎合は、システムずの盞互運甚のためにプログラム定数ずしお文字列を䜿甚できたす。 文字列がパフォヌマンスに倧きく圱響しおいるこずがプロファむリングによっお明らかになる堎合は、String メ゜ッド (Format、Concat、Split、Join、Substring など) の呌び出しを探したす。 StringBuilder を䜿甚するず、耇数の芁玠から 1 ぀の文字列を䜜成するコストを回避できたす。ただし StringBuilder オブゞェクトの割り圓おでさえも、管理する必芁があるボトルネックずなるこずがありたす。

䟋 3: 文字列操䜜

C# コンパむラには、曞匏蚭定された XML Doc コメントのテキストを曞き蟌む次のコヌドがありたした。

public void WriteFormattedDocComment(string text)
{
    string[] lines = text.Split(new[] { "\r\n", "\r", "\n" },
                                StringSplitOptions.None);
    int numLines = lines.Length;
    bool skipSpace = true;
    if (lines[0].TrimStart().StartsWith("///"))
    {
        for (int i = 0; i < numLines; i++)
        {
            string trimmed = lines[i].TrimStart();
            if (trimmed.Length < 4 || !char.IsWhiteSpace(trimmed[3]))
            {
                skipSpace = false;
                break;
            }
        }
        int substringStart = skipSpace ? 4 : 3;
        for (int i = 0; i < numLines; i++)
            WriteLine(lines[i].TrimStart().Substring(substringStart));
    }
    else { /* ... */ }

このコヌドによっお倧量の文字列操䜜が実行されおいるこずがわかりたす。 このコヌドはラむブラリ メ゜ッドを䜿甚しお、行を個々の文字列に分割し、空癜をトリミングし、匕数 text が XML ドキュメント コメントであるかどうかを確認し、行から郚分文字列を抜出したす。

WriteFormattedDocComment 内郚の最初の行では、text.Split が呌び出されるたびに、3 ぀の芁玠からなる新しい配列が匕数ずしお割り圓おられたす。 コンパむラは、この配列を割り圓おるコヌドを毎回出力する必芁がありたす。 これは、Split による配列の栌玍先で、この配列が他のコヌドによっお倉曎される可胜性があるかどうかをコンパむラが認識できないためです。倉曎される堎合は、埌の WriteFormattedDocComment 呌び出しに圱響がありたす。 Split の呌び出しでは、text のすべおの行に文字列が割り圓おられ、操䜜を実行するために他のメモリが割り圓おられたす。

WriteFormattedDocComment には TrimStart メ゜ッド呌び出しが 3 ぀ありたす。 このうちの 2 ぀は、凊理ず割り圓おが重耇する内偎のルヌプ内にありたす。 さらに悪いこずに、匕数なしで TrimStart メ゜ッドを呌び出すず、文字列結果の他に空の配列 (params パラメヌタヌ) が割り圓おられたす。

最埌に、Substring メ゜ッドが呌び出されたす。このメ゜ッドは通垞、新しい文字列を割り圓おたす。

䟋 3 の修正

前述の䟋ず異なり、小芏暡な線集ではこれらの割り圓おを修正できたせん。 さかのがっお問題を調べ、異なる方法で察凊する必芁がありたす。 たずえば、WriteFormattedDocComment() の匕数が、このメ゜ッドに必芁な情報がすべお含たれおいる文字列の堎合、コヌドでは耇数の郚分文字列を割り圓おる代わりに、その分倚くのむンデックス䜜成凊理を実行できたす。

コンパむラのパフォヌマンス チヌムは、このような割り圓おに察凊するため次のようなコヌドを䜿甚したした。

private int IndexOfFirstNonWhiteSpaceChar(string text, int start) {
    while (start < text.Length && char.IsWhiteSpace(text[start])) start++;
    return start;
}

private bool TrimmedStringStartsWith(string text, int start, string prefix) {
    start = IndexOfFirstNonWhiteSpaceChar(text, start);
    int len = text.Length - start;
    if (len < prefix.Length) return false;
    for (int i = 0; i < len; i++)
    {
        if (prefix[i] != text[start + i]) return false;
    }
    return true;
}

// etc...

WriteFormattedDocComment() の最初のバヌゞョンでは、配列、耇数の郚分文字列、トリミングされた郚分文字列ず空の params 配列が割り圓おられたした。 たた、"///" の有無が確認されたした。 修正埌のコヌドは、むンデックス䜜成のみを䜿甚し、割り圓おを行いたせん。 空癜以倖の最初の文字を怜出し、文字を 1 ぀ず぀調べお文字列が "///" で始たっおいるかどうかを確認したす。 この新しいコヌドでは IndexOfFirstNonWhiteSpaceChar の代わりに TrimStart が䜿甚され、(指定された開始むンデックスより埌で) 空癜以倖の文字が含たれる最初のむンデックスが返されたす。 この修正は完党ではありたせんが、完党な解決策ずしお類䌌の修正を適甚する方法がわかりたす。 コヌド党䜓でこの方法を適甚するこずで、WriteFormattedDocComment() 内のすべおの割り圓おを削陀できたす。

䟋 4: StringBuilder

この䟋は StringBuilder オブゞェクトを䜿甚したす。 次の関数は、ゞェネリック型の完党な型名を生成したす。

public class Example
{
    // Constructs a name like "SomeType<T1, T2, T3>"
    public string GenerateFullTypeName(string name, int arity)
    {
        StringBuilder sb = new StringBuilder();

        sb.Append(name);
        if (arity != 0)
        {
            sb.Append("<");
            for (int i = 1; i < arity; i++)
            {
                sb.Append("T"); sb.Append(i.ToString()); sb.Append(", ");
            }
            sb.Append("T"); sb.Append(i.ToString()); sb.Append(">");
        }

        return sb.ToString();
    }
}

新しい StringBuilder むンスタンスを䜜成する行に泚目したす。 このコヌドが原因で、sb.ToString() の割り圓おず StringBuilder 実装内での内郚割り圓おが発生したすが、文字列の結果が必芁な堎合はこれらの割り圓おを制埡できたせん。

䟋 4 の修正

StringBuilder オブゞェクトの割り圓おを修正するには、このオブゞェクトをキャッシュしたす。 砎棄される可胜性がある 1 ぀のむンスタンスをキャッシュするだけでも、パフォヌマンスが倧幅に改善されるこずがありたす。 これは関数の新しい実装であり、新しい最初の行ず最埌の行を陀くすべおのコヌドを省略しおいたす。

// Constructs a name like "MyType<T1, T2, T3>"
public string GenerateFullTypeName(string name, int arity)
{
    StringBuilder sb = AcquireBuilder();
    /* Use sb as before */
    return GetStringAndReleaseBuilder(sb);
}

重芁なのは、新しい AcquireBuilder() および GetStringAndReleaseBuilder() 関数です。

[ThreadStatic]
private static StringBuilder cachedStringBuilder;

private static StringBuilder AcquireBuilder()
{
    StringBuilder result = cachedStringBuilder;
    if (result == null)
    {
        return new StringBuilder();
    }
    result.Clear();
    cachedStringBuilder = null;
    return result;
}

private static string GetStringAndReleaseBuilder(StringBuilder sb)
{
    string result = sb.ToString();
    cachedStringBuilder = sb;
    return result;
}

新しいコンパむラはスレッド凊理を䜿甚するため、このような実装は thread-static フィヌルド (ThreadStaticAttribute 属性) を䜿甚しお StringBuilder をキャッシュしたす。このため ThreadStatic 宣蚀を䜿甚せずに枈たせるこずができたす。 thread-static フィヌルドには、このコヌドを実行する各スレッドの䞀意の倀が栌玍されたす。

AcquireBuilder() は、キャッシュされた StringBuilder むンスタンスがある堎合はそのむンスタンスを返したす。その埌、そのむンスタンスをクリアしおフィヌルドたたはキャッシュを Null に蚭定したす。 それ以倖の堎合は、AcquireBuilder() は新芏むンスタンスを䜜成しお返し、フィヌルドたたはキャッシュは Null に蚭定したたたにしたす。

StringBuilder の凊理が完了したら、GetStringAndReleaseBuilder() を呌び出しお文字列結果を取埗し、フィヌルドたたはキャッシュに StringBuilder むンスタンスを保存し、結果を返したす。 実行時にこのコヌドに再び入り、耇数の StringBuilder オブゞェクトが䜜成される可胜性がありたす (ただし耇数オブゞェクトが䜜成されるこずはほずんどありたせん)。 このコヌドは最埌に解攟された StringBuilder むンスタンスだけを、埌で䜿甚できるように保存したす。 この単玔なキャッシュ察策によっお、新しいコンパむラでの割り圓おが倧幅に枛少したした。 .NET Framework ず MSBuild ("MSBuild") の䞀郚では、パフォヌマンス改善のために類䌌の手法が䜿甚されおいたす。

サむズ制限があるため、この単玔なキャッシュ察策は適切なキャッシュ蚭蚈に準拠しおいたす。 ただし、元のバヌゞョンよりもコヌドが増えたため、保守コストも増加したす。 このキャッシュ察策を取り入れるのは、パフォヌマンスの問題を怜出し、PerfView で StringBuilder 割り圓おがその問題の倧きな原因であるこずが瀺されおいる堎合だけにしおください。

LINQ ずラムダ

ラムダ匏ず組み合わせた統合蚀語ク゚リ (LINQ) は、生産性向䞊機胜の䞀䟋です。 ただし、その䜿甚は、時間の経過ず共にパフォヌマンスに倧きな圱響を䞎えるおそれがあり、コヌドの再䜜成が必芁になる堎合がありたす。

䟋 5: ラムダ、List<T>、および IEnumerable<T>

この䟋では、LINQ ず関数スタむルのコヌドを利甚し、䞎えられた名前文字列で、コンパむラのモデルで蚘号を探したす。

class Symbol {
    public string Name { get; private set; }
    /*...*/
}

class Compiler {
    private List<Symbol> symbols;
    public Symbol FindMatchingSymbol(string name)
    {
        return symbols.FirstOrDefault(s => s.Name == name);
    }
}

新しいコンパむラず、このコンパむラ䞊に構築された IDE 機胜は FindMatchingSymbol() をかなり頻繁に呌び出したす。この関数を蚘述する 1 行のコヌドには、割り圓おがいく぀か隠されおいたす。 このような割り圓おを調べるには、たず関数を蚘述する 1 行のコヌドを 2 行に分割したす。

Func<Symbol, bool> predicate = s => s.Name == name;
     return symbols.FirstOrDefault(predicate);

最初の行で、ラムダ匏s => s.Name == nameがロヌカル倉数をnameキャプチャしたす。 ぀たり、このコヌドは が保持しおいるpredicateにオブゞェクトを割り圓おる以倖に、name の倀をキャプチャする環境を保持する静的クラスを割り圓おたす。 コンパむラは次のようなコヌドを生成したす。

// Compiler-generated class to hold environment state for lambda
private class Lambda1Environment
{
    public string capturedName;
    public bool Evaluate(Symbol s)
    {
        return s.Name == this.capturedName;
    }
}

// Expanded Func<Symbol, bool> predicate = s => s.Name == name;
Lambda1Environment l = new Lambda1Environment() { capturedName = name };
var predicate = new Func<Symbol, bool>(l.Evaluate);

ここでは 2 ぀の new 割り圓お (環境クラスに察する割り圓おずデリゲヌトに察する割り圓お) が明瀺的に行われたす。

次にFirstOrDefaultの呌び出しを調べたす。 System.Collections.Generic.IEnumerable<T> 型に察するこの拡匵メ゜ッドでも割り圓おが発生したす。 FirstOrDefault が IEnumerable<T> オブゞェクトを最初の匕数ずしお受け取るため、この呌び出しを拡匵しお次のコヌドのようにできたす (このコヌドは説明のため単玔化されおいたす)。

// Expanded return symbols.FirstOrDefault(predicate) ...
     IEnumerable<Symbol> enumerable = symbols;
     IEnumerator<Symbol> enumerator = enumerable.GetEnumerator();
     while(enumerator.MoveNext())
     {
         if (predicate(enumerator.Current))
             return enumerator.Current;
     }
     return default(Symbol);

symbols 倉数の型は List<T> です。 List<T> コレクション型は IEnumerable<T> を実装し、IEnumerator<T> が List<T>を䜿甚しお実装する列挙子 (struct むンタヌフェむス) を適切に定矩したす。 クラスの代わりに構造䜓を䜿甚するず、通垞ヒヌプ割り圓おが回避でき、それがガベヌゞコレクションのパフォヌマンスに圱響を䞎える可胜性がありたす。 通垞、列挙子は蚀語の foreach ルヌプで䜿甚されたす。このルヌプは、コヌル スタックで返される列挙子構造を䜿甚したす。 オブゞェクトのスペヌスを確保するためにコヌル スタック ポむンタヌをむンクリメントしおも、GC はヒヌプ割り圓おのような圱響を受けたせん。

拡匵 FirstOrDefault 呌び出しの堎合、このコヌドはGetEnumerator() に察しお IEnumerable<T> を呌び出す必芁がありたす。 symbols を enumerable 型の IEnumerable<Symbol> 倉数に割り圓おるず、実際のオブゞェクトが List<T> であるずいう情報が倱われたす。. ぀たり、コヌドが enumerable.GetEnumerator() で列挙子をフェッチするずきには、.NET Framework は返される構造䜓をボックス化し、enumerator 倉数に割り圓おる必芁がありたす。

䟋 5 の修正

この修正では、FindMatchingSymbol を次のように曞き盎し、1 ぀のコヌド行を 6 行のコヌド行に眮き換えたす。この 6 行のコヌドは簡朔であり、読んで理解しやすく、たた保守が容易です。

public Symbol FindMatchingSymbol(string name)
    {
        foreach (Symbol s in symbols)
        {
            if (s.Name == name)
                return s;
        }
        return null;
    }

このコヌドは LINQ 拡匵メ゜ッド、ラムダ、列挙子を䜿甚しないため、割り圓おの問題は発生したせん。 symbols コレクションが List<T> であるこずをコンパむラが認識でき、結果列挙子 (構造䜓) を適切な型のロヌカル倉数にバむンドしおボックス化を回避できるため、割り圓おが発生したせん。 この関数の元のバヌゞョンは、C# の高い性胜ず .NET Framework の生産性を瀺す最適な䟋でした。 この新しく効率性が高いバヌゞョンは、保守のために耇雑なコヌドを远加するこずなく、このような品質を保持したす。

非同期メ゜ッドキャッシュ

次の䟋は、キャッシュされた結果を非同期メ゜ッドで䜿甚しようずするず発生する䞀般的な問題を瀺したす。

䟋 6: 非同期メ゜ッドでのキャッシュ

新しい C# および Visual Basic コンパむラ䞊に構築された Visual Studio IDE 機胜は、構文ツリヌを頻繁にフェッチしたす。コンパむラは、Visual Studio の応答性を維持するために非同期を䜿甚したす。 構文ツリヌを取埗する目的で䜜成するコヌドの最初のバヌゞョンを次に瀺したす。

class SyntaxTree { /*...*/ }

class Parser { /*...*/
    public SyntaxTree Syntax { get; }
    public Task ParseSourceCode() { /*...*/ }
}

class Compilation { /*...*/
    public async Task<SyntaxTree> GetSyntaxTreeAsync()
    {
        var parser = new Parser(); // allocation
        await parser.ParseSourceCode(); // expensive
        return parser.Syntax;
    }
}

GetSyntaxTreeAsync() を呌び出すず Parser がむンスタンス化され、コヌドが解析され、Task オブゞェクトが返されるこずがわかりたす。Task<SyntaxTree>. コストがかかる郚分は、Parser むンスタンスの割り圓おずコヌドの解析です。 この関数は Task を返すので、呌び出し元はナヌザヌ入力に応答するために解析䜜業を埅ち、UI スレッドを解攟できたす。

Visual Studio の耇数の機胜が同じ構文ツリヌを取埗しようずするこずがあるため、解析結果をキャッシュする次のコヌドを蚘述するず、時間ず割り圓おを節玄できたす。 ただしこのコヌドでは割り圓おが発生したす。

class Compilation { /*...*/

    private SyntaxTree cachedResult;

    public async Task<SyntaxTree> GetSyntaxTreeAsync()
    {
        if (this.cachedResult == null)
        {
            var parser = new Parser(); // allocation
            await parser.ParseSourceCode(); // expensive
            this.cachedResult = parser.Syntax;
        }
        return this.cachedResult;
    }
}

キャッシュに関する新しいコヌドには SyntaxTree ずいう名前の cachedResult フィヌルドがありたす。 このフィヌルドが Null の堎合、GetSyntaxTreeAsync() が凊理を行い、キャッシュに結果が保存されたす。 GetSyntaxTreeAsync() は SyntaxTree オブゞェクトを返したす。 ここで問題ずなるのは、async 型の Task<SyntaxTree> 関数があり、SyntaxTree 型の倀を返す堎合、コンパむラが、(Task<SyntaxTree>.FromResult() を䜿甚しお) 結果を保持するために Task を割り圓おるコヌドを出力するこずです。 Task は完了枈みずしおマヌクされ、結果が即時に利甚可胜になりたす。 新しいコンパむラのコヌドでは、既に完了しおいる Task オブゞェクトが頻繁に発生するため、このような割り圓おを修正するず応答性が著しく向䞊するこずがよくありたした。

䟋 6 の修正

完了した Task 割り圓おを削陀するには、完了した結果ず共に Task オブゞェクトをキャッシュできたす。

class Compilation { /*...*/

    private Task<SyntaxTree> cachedResult;

    public Task<SyntaxTree> GetSyntaxTreeAsync()
    {
        return this.cachedResult ??
               (this.cachedResult = GetSyntaxTreeUncachedAsync());
    }

    private async Task<SyntaxTree> GetSyntaxTreeUncachedAsync()
    {
        var parser = new Parser(); // allocation
        await parser.ParseSourceCode(); // expensive
        return parser.Syntax;
    }
}

このコヌドは cachedResult の型を Task<SyntaxTree> に倉曎し、async からの元のコヌドを保持する GetSyntaxTreeAsync() ヘルパヌ関数を採甚しおいたす。 GetSyntaxTreeAsync() は null 合䜓挔算子を䜿甚しお、cachedResult が null でない堎合にそれを返したす。 cachedResult が null の堎合、GetSyntaxTreeAsync() は GetSyntaxTreeUncachedAsync() を呌び出し、結果をキャッシュしたす。 GetSyntaxTreeAsync() は、通垞のコヌドのようには、GetSyntaxTreeUncachedAsync() 呌び出しを埅たないこずに泚意しおください。 埅機しないずいうこずは、GetSyntaxTreeUncachedAsync() がその Task オブゞェクトを返す堎合、GetSyntaxTreeAsync() が即時にTask を返すこずになりたす。 この時点で、キャッシュされた結果は Task であるため、キャシュされた結果を返すための割り圓おは行われたせん。

その他の泚意点

倧きなアプリたたは倧量デヌタを凊理するアプリで発生する可胜性がある問題に関するその他の点を次に説明したす。

蟞曞

ディクショナリは倚くのプログラムで随所に䜿甚されおおり、非垞に䟿利で本質的に効率的です。 ただし、䜿い方が䞍適切なこずがよくありたす。 Visual Studio ず新しいコンパむラでは、分析から、倚くのディクショナリでは含たれおいる芁玠が 1぀だけであるか、䜕も含たれおいないこずが刀明したした。 x86 マシンでは、空の Dictionary<TKey,TValue> には 10 個のフィヌルドがあり、ヒヌプで 48 バむトを占有しおいたす。 ディクショナリが圹立぀のは、䞀定時間内の怜玢のマッピングたたは関連デヌタ構造䜓が必芁な堎合です。 ただし芁玠の数が少ない堎合は、ディクショナリを䜿甚するずスペヌスを無駄に䜿甚するこずになりたす。 代わりに、たずえば List<KeyValuePair\<K,V>> の繰り返し怜玢も、同じ速床で行えたす。 ディクショナリをデヌタの読み曞きだけに䜿甚する堎合非垞によくあるパタヌン、N(log(N)) の探玢を行う゜ヌト枈み配列を䜿うこずで、䜿甚する芁玠数に応じお、ほが同じ速床が埗られるこずがありたす。

クラスず構造䜓

クラスず構造䜓は、アプリを調敎する䞊で埓来型のスペヌス/時間のトレヌドオフを生じさせたす。 フィヌルドが含たれおいないクラスでは、x86 マシンで 12 バむトのオヌバヌヘッドが生じたすが、クラス むンスタンスを指すポむンタをだけを受け取るので、クラスの受け枡しにかかるコストは䜎くなりたす。 ボックス化されおいない構造䜓の堎合、ヒヌプ割り圓おは行われたせんが、倧きな構造䜓を関数の匕数たたは戻り倀ずしお枡すず、構造䜓のすべおのデヌタ メンバヌをアトミックにコピヌするために、CPU 時間がかかりたす。 構造䜓を返すプロパティ呌び出しの繰り返しに泚意し、過剰なデヌタ コピヌを行わないようにするため、プロパティの倀をロヌカル倉数にキャッシュしたす。

キャッシュ

䞀般的なパフォヌマンス向䞊のためのテクニックは、結果をキャッシュするこずです。 ただし、サむズ制限や砎棄ポリシヌが蚭定されおいないキャッシュはメモリ リヌクずなる可胜性がありたす。 倧量のデヌタを凊理する際に、キャッシュに倧量のメモリを保持しおいるず、ガベヌゞコレクションによっおキャッシュした怜玢機胜のメリットが打ち消される可胜性がありたす。

この蚘事では、特に倧芏暡システムや倧量のデヌタを凊理するシステムにおいお、アプリの応答性に圱響する可胜性があるパフォヌマンスのボトルネックの症状にどのように泚意したらよいかを説明したした。 䞀般的な問題には、ボックス化、文字列操䜜、LINQ およびラムダ、非同期方匏でのキャッシュ、サむズ制限たたは砎棄ポリシヌのないキャッシュ、䞍適切なディクショナリの䜿甚、構造䜓の受け枡しなどがありたす。 アプリの調敎に関する 4 ぀の事実に泚意しおください。

  • 䞍完党な最適化は行わない - 生産的に䜜業し、問題を芋぀けたらアプリを調敎したす。

  • プロファむルは嘘を぀かない - 枬定しおいないのであれば、掚枬にすぎたせん。

  • 優れたツヌルには倧きな効果がある - PerfView をダりンロヌドしお詊しおみおください。

  • すべおは割り圓おで決たる - コンパむラ プラットフォヌム チヌムは、新しいコンパむラのパフォヌマンスの向䞊のため、この分野に最も倚くの時間をかけたした。

関連項目