Sdílet prostřednictvím


Psaní velkých a pohotově reagujících aplikací .NET Framework

Tento článek obsahuje tipy pro zlepšení výkonu velkých aplikací rozhraní .NET Framework nebo aplikací, které zpracovávají velké množství dat, jako jsou soubory nebo databáze. Tyto tipy pocházejí z přepsání kompilátorů jazyka C# a Visual Basic ve spravovaném kódu a tento článek obsahuje několik skutečných příkladů z kompilátoru jazyka C#.

.NET Framework je vysoce produktivní pro vytváření aplikací. Výkonné a bezpečné jazyky a bohatá kolekce knihoven vytvářejí aplikace vysoce plodné. S velkou produktivitou však přichází zodpovědnost. Měli byste použít všechny možnosti rozhraní .NET Framework, ale být připraveni ladit výkon kódu v případě potřeby.

Proč se nový výkon kompilátoru vztahuje na vaši aplikaci

Tým .NET Compiler Platform (Roslyn) přepíše kompilátory jazyka C# a Visual Basic ve spravovaném kódu, aby poskytovala nová rozhraní API pro modelování a analýzu kódu, nástrojů pro vytváření a povolování mnohem bohatších prostředí pracujících s kódem v sadě Visual Studio. Přepsání kompilátorů a sestavování prostředí sady Visual Studio na nových kompilátorech odhalilo užitečné přehledy o výkonu, které platí pro libovolnou velkou aplikaci .NET Framework nebo jakoukoli aplikaci, která zpracovává velké množství dat. Abyste mohli využít přehledy a příklady z kompilátoru jazyka C#, nemusíte o kompilátoru jazyka C# vědět.

Visual Studio používá rozhraní API kompilátoru k sestavení všech funkcí IntelliSense, které uživatelé milují, jako je zabarvení identifikátorů a klíčových slov, seznamy dokončování syntaxe, vlnovkování chyb, tipy pro parametry, problémy s kódem a akce kódu. Visual Studio poskytuje tuto nápovědu, když vývojáři zapisují a mění kód, a Visual Studio musí zůstat responzivní, zatímco kompilátor průběžně modeluje úpravy vývojářů kódu.

Když koncoví uživatelé s vaší aplikací pracují, očekávají, že budou reagovat. Zadávání nebo zpracování příkazů by nikdy nemělo být blokováno. Nápověda by se měla rychle zobrazit nebo se vzdát, pokud uživatel pokračuje v psaní. Aplikace by se měla vyhnout blokování vlákna uživatelského rozhraní dlouhými výpočty, které aplikaci znemožní.

Další informace o kompilátorech Roslyn naleznete v tématu Sada .NET Compiler Platform SDK.

Jen fakta

Při ladění výkonu a vytváření responzivních aplikací .NET Framework zvažte tato fakta.

Fakt 1: Předčasné optimalizace nejsou vždy užitečné

Psaní kódu, který je složitější, než vyžaduje údržbu, ladění a leštění nákladů. Zkušení programátoři mají intuitivní přehled o tom, jak řešit problémy s kódováním a psát efektivnější kód. Někdy ale kód předčasně optimalizují. Například používají tabulku hash, když stačí jednoduché pole, nebo používají složitou mezipaměť, která může nevratit paměť místo pouhého přepočítání hodnot. I když jste programátor zkušenosti, měli byste otestovat výkon a analyzovat kód, když zjistíte problémy.

Fakt 2: Pokud neměříte, hádáte

Profily a měření neleží. Profily ukazují, jestli je procesor plně načtený nebo jestli jste blokovaní na vstupně-výstupních operacích disku. Profily vám říkají, jaký druh a kolik paměti přidělujete a jestli váš procesor tráví hodně času v uvolňování paměti (GC).

Měli byste nastavit cíle výkonu pro klíčová prostředí nebo scénáře zákazníků ve vaší aplikaci a psát testy pro měření výkonu. Prozkoumejte neúspěšné testy použitím vědecké metody: použijte profily, které vás provedou, hypotézu o tom, co může být problém, a otestujte svou hypotézu pomocí experimentu nebo změny kódu. Pomocí pravidelného testování nastavte standardní měření výkonu v průběhu času, abyste mohli izolovat změny, které způsobují regrese v výkonu. Když se přiblížíte výkonové práci přísným způsobem, vyhnete se plýtvání časem s aktualizacemi kódu, které nepotřebujete.

Fakt 3: Dobré nástroje dělají všechny rozdíly

Dobré nástroje umožňují rychle přejít k podrobnostem o největších problémech s výkonem (procesor, paměť nebo disk) a pomáhají najít kód, který tyto kritické body způsobuje. Microsoft dodává celou řadu nástrojů pro výkon, jako je Visual Studio Profiler a PerfView.

PerfView je výkonný nástroj, který vám pomůže zaměřit se na hluboké problémy, jako jsou vstupně-výstupní operace disku, události GC a paměť. Můžete zachytit události trasování událostí související s výkonem pro Windows (ETW) a snadno zobrazit jednotlivé aplikace, procesy, zásobník a informace o vláknech. PerfView ukazuje, kolik a jaký druh paměti aplikace přiděluje a které funkce nebo volání zásobníků přispívají k přidělení paměti. Podrobnosti najdete v bohatých tématech nápovědy, ukázkách a videích, které jsou součástí nástroje.

Fakt 4: Všechno o přiděleních

Možná si myslíte, že vytvoření responzivní aplikace .NET Framework se týká algoritmů, jako je použití rychlého řazení místo řazení bublin, ale to není ten případ. Největší faktor při vytváření responzivní aplikace je přidělování paměti, zejména pokud je aplikace velmi velká nebo zpracovává velké objemy dat.

Téměř veškerou práci na vytváření responzivních prostředí IDE pomocí nových rozhraní API kompilátoru, která brání přidělování a správě strategií ukládání do mezipaměti. Trasování perfView ukazují, že výkon nových kompilátorů jazyka C# a Visual Basic je zřídka vázán na procesor. Kompilátory můžou být vázané na vstupně-výstupní operace při čtení stovek tisíc nebo milionů řádků kódu, čtení metadat nebo generování generovaného kódu. Zpoždění vlákna uživatelského rozhraní jsou téměř všechna kvůli uvolňování paměti. GC rozhraní .NET Framework je vysoce vyladěný pro výkon a provádí většinu své práce souběžně při provádění kódu aplikace. Jedno přidělení ale může aktivovat nákladnou kolekci Gen2 a zastavit všechna vlákna.

Běžné přidělení a příklady

Ukázkové výrazy v této části mají skryté přidělení, které jsou malé. Pokud ale velká aplikace spustí výrazy dostatečně dlouho, můžou způsobit stovky megabajtů, dokonce i gigabajty přidělení. Například jednominutové testy, které simulovaly psaní vývojáře v editoru přidělených gigabajty paměti a vedla tým výkonu k zaměření na scénáře psaní.

Zabalení

Boxing nastane, když jsou v objektu zabalené typy hodnot, které obvykle žijí v zásobníku nebo v datových strukturách. To znamená, že objekt přidělíte k uložení dat a pak vrátíte ukazatel na objekt. Rozhraní .NET Framework někdy zadává hodnoty z důvodu podpisu metody nebo typu umístění úložiště. Zabalení typu hodnoty v objektu způsobí přidělení paměti. Mnoho operací boxingu může přispívat megabajty nebo gigabajty přidělení do vaší aplikace, což znamená, že vaše aplikace způsobí více GCS. Rozhraní .NET Framework a kompilátory jazyka se vyhýbají boxování, pokud je to možné, ale někdy se stane, když ho nejméně očekáváte.

Pokud chcete zobrazit boxování v nástroji PerfView, otevřete trasování a podívejte se na zásobníky GC Heap Alloc Pod názvem procesu vaší aplikace (pamatujte si, že perfView sestavy pro všechny procesy). Pokud se zobrazí typy jako System.Int32 a System.Char v části Přidělení, jedná se o typy hodnot boxingu. Když zvolíte jeden z těchto typů, zobrazí se zásobníky a funkce, ve kterých jsou vložené do rámečku.

Příklad 1: Řetězcové metody a argumenty typu hodnoty

Tento ukázkový kód ilustruje potenciálně zbytečné a nadměrné balení:

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);
    }
}

Tento kód poskytuje funkce protokolování, takže aplikace může funkci často volat Log , možná milionykrát. Problém spočívá v tom, že volání, které se string.Format má vyřešit přetížení Format(String, Object, Object) .

Toto přetížení vyžaduje rozhraní .NET Framework, aby int hodnoty do objektů předal do volání této metody. Částečnou opravou je volání id.ToString() a size.ToString() předání všech řetězců (což jsou objekty) volání string.Format . Volání ToString() přiděluje řetězec, ale toto přidělení se přesto stane uvnitř string.Format.

Můžete zvážit, že toto základní volání string.Format je pouze zřetězení řetězců, takže místo toho můžete napsat tento kód:

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

Tento řádek kódu však zavádí přidělení boxingu, protože se zkompiluje do Concat(Object, Object, Object). Rozhraní .NET Framework musí zadat literál znaku, který se má vyvolat. Concat

Oprava příkladu 1

Úplná oprava je jednoduchá. Jednoduše nahraďte literál znaku řetězcovým literálem, který nemá žádné pole, protože řetězce jsou již objekty:

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

Příklad 2: vytvoření výčtu

Tento příklad zodpovídá za obrovské množství přidělení v nových kompilátorech C# a Visual Basic kvůli častému použití typů výčtů, zejména v operacích vyhledávání slovníku.

public enum Color
{
    Red, Green, Blue
}

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

Tento problém je velmi jemný. PerfView by to ohlásil jako GetHashCode() boxování, protože metoda boxuje základní reprezentaci typu výčtu z důvodů implementace. Pokud se v nástroji PerfView podíváte pozorně, může se zobrazit dvě přidělení boxů pro každé volání GetHashCode(). Kompilátor vloží jeden a rozhraní .NET Framework vloží druhý.

Oprava příkladu 2

Před voláním GetHashCode()se můžete snadno vyhnout oběma přidělením přetypováním do podkladové reprezentace:

((int)color).GetHashCode()

Dalším běžným zdrojem boxingu pro typy výčtu Enum.HasFlag(Enum) je metoda. Argument předaný HasFlag(Enum) musí být v rámečku. Ve většině případů je nahrazení volání Enum.HasFlag(Enum) bitovým testem jednodušší a přidělení zdarma.

Mějte na paměti první fakt o výkonu (to znamená, že nechejte předčasně optimalizovat) a nezačínejte přepisovat veškerý kód tímto způsobem. Mějte na paměti tyto náklady na boxování, ale změňte kód až po profilaci aplikace a nalezení horkých míst.

Řetězce

Manipulace s řetězci jsou některými z největších pachatelů přidělení a často se zobrazují v PerfView v prvních pěti přiděleních. Programy používají řetězce pro serializaci, JSON a rozhraní REST API. Řetězce můžete použít jako programové konstanty pro spolupráci se systémy, pokud nemůžete použít typy výčtu. Když profilace ukazuje, že řetězce mají vysoké vliv na výkon, vyhledejte volání String metod, jako Formatjsou , Concat, Split, Join, Substringatd. Použití StringBuilder k tomu, aby se zabránilo nákladům na vytvoření jednoho řetězce z mnoha částí, ale i přidělení objektu StringBuilder může být kritickým bodem, který potřebujete spravovat.

Příklad 3: Operace s řetězci

Kompilátor jazyka C# měl tento kód, který zapisuje text formátovaného komentáře dokumentu XML:

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 { /* ... */ }

Vidíte, že tento kód dělá spoustu manipulace s řetězci. Kód používá metody knihovny k rozdělení řádků do samostatných řetězců, k oříznutí prázdných znaků, ke kontrole, zda text argument je komentář dokumentace XML a extrahuje podřetězce z řádků.

Na prvním řádku uvnitř WriteFormattedDocCommenttext.Split volání přidělí nové pole se třemi prvky jako argument pokaždé, když je volána. Kompilátor musí pokaždé vygenerovat kód pro přidělení tohoto pole. Je to proto, že kompilátor neví, jestli Split pole ukládá někam, kde pole může být změněno jiným kódem, což by mělo vliv na WriteFormattedDocCommentpozdější volání . Split Volání také přidělí řetězec pro každý řádek v text řádku a přidělí další paměť k provedení operace.

WriteFormattedDocComment má tři volání metody TrimStart . Dva jsou ve vnitřních smycích, které duplikují práci a přidělení. Aby to bylo ještě horší, volání TrimStart metody bez argumentů přidělí prázdné pole (pro params parametr) kromě výsledku řetězce.

Nakonec existuje volání Substring metody, která obvykle přiděluje nový řetězec.

Oprava příkladu 3

Na rozdíl od předchozích příkladů nemohou malé úpravy tyto přidělení opravit. Musíte se vrátit zpět, podívat se na problém a přistupovat k němu jinak. Všimněte si například, že argumentem WriteFormattedDocComment() je řetězec, který obsahuje všechny informace, které metoda potřebuje, takže kód by mohl místo přidělování mnoha částečných řetězců provádět větší indexování.

Tým výkonu kompilátoru vyřešil všechny tyto přidělení pomocí kódu takto:

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...

První verze přidělené pole WriteFormattedDocComment() , několik podřetězců a oříznutý podřetězce spolu s prázdným params polem. Také se kontrolovala možnost ///. Upravený kód používá pouze indexování a nepřiděluje nic. Najde první znak, který není prázdný, a potom zkontroluje, jestli řetězec začíná znakem ///. Nový kód místo IndexOfFirstNonWhiteSpaceCharTrimStart toho, aby vrátil první index (po zadaném počátečním indexu), kde se vyskytuje jiný než prázdný znak. Oprava není dokončená, ale můžete se podívat, jak použít podobné opravy pro kompletní řešení. Použitím tohoto přístupu v celém kódu můžete odebrat všechna přidělení v WriteFormattedDocComment()souboru .

Příklad 4: StringBuilder

Tento příklad používá StringBuilder objekt. Následující funkce vygeneruje úplný název typu pro obecné typy:

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();
    }
}

Fokus je na řádku, který vytvoří novou StringBuilder instanci. Kód způsobí přidělení a sb.ToString() interní přidělení v rámci StringBuilder implementace, ale pokud chcete mít výsledek řetězce, nemůžete tyto přidělení řídit.

Oprava příkladu 4

Pokud chcete opravit přidělení objektu StringBuilder , ukažte objekt do mezipaměti. Dokonce i ukládání do mezipaměti jedné instance, která by mohla být zahozena, může výrazně zvýšit výkon. Toto je nová implementace funkce, která vynechá veškerý kód s výjimkou nových prvních a posledních řádků:

// 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);
}

Klíčové části jsou nové AcquireBuilder() a GetStringAndReleaseBuilder() funkce:

[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;
}

Vzhledem k tomu, že nové kompilátory používají threading, tyto implementace používají k ukládání do mezipaměti StringBuilderpole se statickým vláknem (ThreadStaticAttributeatribut) a pravděpodobně můžete deklaraci naplnitThreadStatic. Statické pole vlákna obsahuje jedinečnou hodnotu pro každé vlákno, které tento kód spouští.

AcquireBuilder() vrátí instanci uloženou StringBuilder v mezipaměti, pokud existuje, po vymazání a nastavení pole nebo mezipaměti na hodnotu null. AcquireBuilder() V opačném případě vytvoří novou instanci a vrátí ji a ponechá pole nebo mezipaměť nastavenou na hodnotu null.

Až to budete mít StringBuilder , zavoláte GetStringAndReleaseBuilder() , abyste získali výsledek řetězce, uložte StringBuilder instanci do pole nebo mezipaměti a pak vrátíte výsledek. Spuštění je možné znovu zadat tento kód a vytvořit více StringBuilder objektů (i když k tomu dochází jen zřídka). Kód uloží pouze poslední vydanou StringBuilder instanci pro pozdější použití. Tato jednoduchá strategie ukládání do mezipaměti výrazně snížila přidělení v nových kompilátorech. Části rozhraní .NET Framework a MSBuild ("MSBuild") používají podobnou techniku ke zlepšení výkonu.

Tato jednoduchá strategie ukládání do mezipaměti dodržuje dobrý návrh mezipaměti, protože má velikost limitu. Nyní však existuje více kódu než v originálu, což znamená více nákladů na údržbu. Strategii ukládání do mezipaměti byste měli přijmout pouze v případě, že jste narazili na problém s výkonem a nástroj PerfView ukázal, že StringBuilder přidělení jsou významným přispěvatelem.

LINQ a lambda

Jazykově integrovaný dotaz (LINQ) ve spojení s výrazy lambda je příkladem funkce produktivity. Jeho použití ale může mít významný dopad na výkon v průběhu času a možná zjistíte, že budete muset kód přepsat.

Příklad 5: Lambdas, List<T> a IEnumerable<T>

Tento příklad používá kód LINQ a funkčního stylu k vyhledání symbolu v modelu kompilátoru s názvem řetězce:

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);
    }
}

Nový kompilátor a prostředí IDE, která jsou na něm postavena, se velmi často volají FindMatchingSymbol() a v jednom řádku kódu této funkce je několik skrytých přidělení. Pokud chcete tyto přidělení prozkoumat, nejprve rozdělte jeden řádek kódu funkce na dva řádky:

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

Na prvním řádku se výrazs => s.Name == namelambda zavře přes místní proměnnou .name To znamená, že kromě přidělování objektu pro delegáta , který predicate obsahuje, kód přidělí statickou třídu pro uložení prostředí, které zachycuje hodnotu name. Kompilátor vygeneruje kód podobný následujícímu:

// 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);

new Dvě přidělení (jedna pro třídu prostředí a druhá pro delegáta) jsou teď explicitní.

Teď se podívejte na volání FirstOrDefault. Tato metoda rozšíření u System.Collections.Generic.IEnumerable<T> typu také způsobuje přidělení. Vzhledem k tomu, že FirstOrDefault jako IEnumerable<T> první argument vezme objekt, můžete rozšířit volání na následující kód (zjednodušená diskuze):

// 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);

Proměnná symbols má typ List<T>. Typ List<T> kolekce implementuje IEnumerable<T> a chytře definuje enumerátor (IEnumerator<T> rozhraní), který List<T> implementuje pomocí struct. Použití struktury místo třídy znamená, že obvykle vyhněte se přidělení haldy, což může zase ovlivnit výkon uvolňování paměti. Enumerátory se obvykle používají se smyčkou jazyka foreach , která používá strukturu enumerátoru, protože je vrácena v zásobníku volání. Zvýšení ukazatele zásobníku volání, aby místo pro objekt nemá vliv na způsob přidělení haldy.

V případě rozšířeného FirstOrDefault volání musí kód zavolat GetEnumerator() na .IEnumerable<T> Přiřazení symbols proměnné enumerable typu IEnumerable<Symbol> ztratí informace, že skutečný objekt je List<T>. To znamená, že když kód načte enumerátor s enumerable.GetEnumerator(), rozhraní .NET Framework musí vrácené struktury přiřadit proměnné enumerator .

Oprava příkladu 5

Oprava je přepsat FindMatchingSymbol následujícím způsobem a nahradit jeden řádek kódu šesti řádky kódu, které jsou stále stručné, snadno čitelné a srozumitelné a snadno udržovatelné:

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

Tento kód nepoužívá metody rozšíření LINQ, lambda ani enumerátory a nevyděluje se. Neexistují žádné přidělení, protože kompilátor může vidět, že symbols kolekce je a List<T> může svázat výsledný enumerátor (strukturu) s místní proměnnou se správným typem, aby se zabránilo boxování. Původní verze této funkce byla skvělým příkladem expresního výkonu jazyka C# a produktivity rozhraní .NET Framework. Tato nová a efektivnější verze zachovává tyto vlastnosti bez přidání složitého kódu, který se má zachovat.

Ukládání asynchronních metod do mezipaměti

Následující příklad ukazuje běžný problém při pokusu o použití výsledků v mezipaměti v asynchronní metodě.

Příklad 6: Ukládání do mezipaměti v asynchronních metodách

Funkce integrovaného vývojového prostředí sady Visual Studio založené na nových kompilátorech jazyka C# a Visual Basic často načítají stromy syntaxe a kompilátory používají při tom asynchronní odezvu sady Visual Studio. Tady je první verze kódu, kterou můžete napsat, abyste získali strom syntaxe:

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;
    }
}

Můžete vidět, že volání GetSyntaxTreeAsync() vytvoří instanci Parser, parsuje kód a pak vrátí Task objekt, Task<SyntaxTree>. Nákladná část přiděluje Parser instanci a parsuje kód. Funkce vrátí funkci Task tak, aby volající mohli čekat na analýzu a uvolnit vlákno uživatelského rozhraní, aby reagovalo na uživatelský vstup.

Několik funkcí sady Visual Studio se může pokusit získat stejný strom syntaxe, takže můžete napsat následující kód, který uloží výsledek analýzy do mezipaměti, aby se ušetřil čas a přidělení. Tento kód ale způsobuje přidělení:

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;
    }
}

Uvidíte, že nový kód s ukládáním do mezipaměti má SyntaxTree pole s názvem cachedResult. Pokud je toto pole null, GetSyntaxTreeAsync() funguje to a uloží výsledek do mezipaměti. GetSyntaxTreeAsync()SyntaxTree vrátí objekt. Problém je, že pokud máte async funkci typu Task<SyntaxTree>a vrátíte hodnotu typu SyntaxTree, kompilátor generuje kód pro přidělení úkolu pro uložení výsledku (pomocí Task<SyntaxTree>.FromResult()). Úkol je označený jako dokončený a výsledek je okamžitě k dispozici. V kódu pro nové kompilátory došlo k objektům, které byly již dokončeny, Task tak často, že oprava těchto přidělení výrazně zlepšila odezvu.

Oprava příkladu 6

Pokud chcete odebrat dokončené Task přidělení, můžete uložit do mezipaměti objekt Task s dokončeným výsledkem:

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;
    }
}

Tento kód změní typ cachedResult na pomocnou async funkci, která obsahuje původní kód z GetSyntaxTreeAsync()Task<SyntaxTree> . GetSyntaxTreeAsync() nyní používá operátor sjednocení null k vrácení cachedResult , pokud není null. Pokud cachedResult je hodnota null, GetSyntaxTreeAsync() volání GetSyntaxTreeUncachedAsync() a uložení výsledku do mezipaměti. Všimněte si, že GetSyntaxTreeAsync() volání nečeká, protože kód by normálně nečekal GetSyntaxTreeUncachedAsync() . Použití operátoru await znamená, že když GetSyntaxTreeUncachedAsync() vrátí jeho Task objekt, GetSyntaxTreeAsync() okamžitě vrátí Taskhodnotu . Výsledek uložený v mezipaměti je tedy alokace Task, aby se vrátil výsledek uložený v mezipaměti.

Další důležité informace

Tady je několik dalších bodů týkajících se potenciálních problémů ve velkých aplikacích nebo aplikacích, které zpracovávají velké množství dat.

Slovníky

Slovníky se používají všudypřítomně v mnoha programech, i když slovníky jsou velmi pohodlné a ze své podstaty efektivní. Často se ale používají nevhodně. V sadě Visual Studio a nových kompilátorech analýza ukazuje, že mnoho slovníků obsahovalo jeden prvek nebo bylo prázdné. Prázdné Dictionary<TKey,TValue> obsahuje deset polí a zabírá na haldě na počítači x86 48 bajtů. Slovníky jsou skvělé, když potřebujete mapování nebo asociativní datovou strukturu s vyhledáváním v konstantním čase. Pokud ale máte jen několik prvků, můžete pomocí slovníku ztrácet spoustu místa. Místo toho byste se mohli iterativním způsobem podívat na , List<KeyValuePair\<K,V>>stejně rychle. Pokud používáte slovník jenom k načtení dat a následnému čtení z něj (velmi běžný vzor), použití seřazeného pole s vyhledáváním N(log(N)) může být téměř stejně rychlé v závislosti na počtu prvků, které používáte.

Třídy vs. struktury

Třídy a struktury poskytují klasický kompromis mezi prostorem a časem pro ladění aplikací. Třídy mají na počítači x86 režijní náklady 12 bajtů, i když nemají žádná pole, ale jejich předání je levné, protože odkazuje pouze na instanci třídy. Struktury neúčtují přidělení haldy, pokud nejsou v rámečku, ale když předáte velké struktury jako argumenty funkce nebo vrácené hodnoty, trvá čas procesoru atomicky zkopírovat všechny datové členy struktur. Dávejte pozor na opakovaná volání vlastností, které vracejí struktury, a hodnoty vlastnosti do mezipaměti v místní proměnné, aby nedocházelo k nadměrnému kopírování dat.

Caches

Běžným trikem s výkonem je ukládání výsledků do mezipaměti. Mezipaměť bez omezení velikosti nebo zásad vyřazení však může být nevracení paměti. Při zpracování velkých objemů dat můžete při ukládání do velkého množství paměti v mezipaměti způsobit, že uvolňování paměti přepíše výhody vyhledávání v mezipaměti.

V tomto článku jsme probrali, jak byste měli vědět o příznaky kritických bodů výkonu, které můžou ovlivnit rychlost odezvy vaší aplikace, zejména u velkých systémů nebo systémů, které zpracovávají velké množství dat. Mezi běžné příčiny patří boxování, manipulace s řetězci, LINQ a lambda, ukládání do mezipaměti v asynchronních metodách, ukládání do mezipaměti bez omezení velikosti nebo zásady odstranění, nevhodné použití slovníků a předávání struktur. Mějte na paměti čtyři fakta pro ladění aplikací:

  • Nechejte se předčasně optimalizovat – buďte produktivní a vylaďte aplikaci, když narazíte na problémy.

  • Profily nelhávají – hádáte, jestli neměříte.

  • Dobré nástroje dělají všechny rozdíly – stáhněte si PerfView a vyzkoušejte to.

  • Je to vše o přiděleních – to je místo, kde tým platformy kompilátoru strávil většinu času zlepšením výkonu nových kompilátorů.

Viz také