Skriva stora, dynamiska .NET Framework-appar
Den här artikeln innehåller tips för att förbättra prestandan för stora .NET Framework-appar eller appar som bearbetar en stor mängd data, till exempel filer eller databaser. De här tipsen kommer från att skriva om C#- och Visual Basic-kompilatorerna i hanterad kod, och den här artikeln innehåller flera verkliga exempel från C#-kompilatorn.
.NET Framework är mycket produktivt för att skapa appar. Kraftfulla och säkra språk och en omfattande samling bibliotek gör appbyggandet mycket givande. Men med stor produktivitet kommer ansvar. Du bör använda all kraft i .NET Framework, men var beredd på att justera kodens prestanda när det behövs.
Varför den nya kompilatorprestandan gäller för din app
.NET Compiler Platform-teamet ("Roslyn") skrev om C#- och Visual Basic-kompilatorerna i hanterad kod för att tillhandahålla nya API:er för att modellera och analysera kod, skapa verktyg och möjliggöra mycket rikare, kodmedvetna upplevelser i Visual Studio. Att skriva om kompilatorerna och skapa Visual Studio-upplevelser på de nya kompilatorerna visade användbara prestandainsikter som gäller för alla stora .NET Framework-appar eller appar som bearbetar mycket data. Du behöver inte känna till kompilatorer för att dra nytta av insikterna och exemplen från C#-kompilatorn.
Visual Studio använder kompilator-API:erna för att skapa alla IntelliSense-funktioner som användarna älskar, till exempel färgläggning av identifierare och nyckelord, listor över slutförande av syntax, squiggles för fel, parametertips, kodproblem och kodåtgärder. Visual Studio ger den här hjälpen medan utvecklare skriver och ändrar sin kod, och Visual Studio måste vara dynamiskt medan kompilatorn kontinuerligt modellerar kodutvecklarna redigerar.
När slutanvändarna interagerar med din app förväntar de sig att den svarar. Skriv- eller kommandohantering bör aldrig blockeras. Hjälp bör dyka upp snabbt eller ge upp om användaren fortsätter att skriva. Din app bör undvika att blockera användargränssnittstråden med långa beräkningar som gör att appen känns trög.
Mer information om Roslyn-kompilatorer finns i .NET Compiler Platform SDK.
Bara fakta
Tänk på dessa fakta när du justerar prestanda och skapar dynamiska .NET Framework-appar.
Fakta 1: För tidiga optimeringar är inte alltid värda besväret
Att skriva kod som är mer komplex än den behöver medföra underhåll, felsökning och poleringskostnader. Erfarna programmerare har ett intuitivt grepp om hur man löser kodningsproblem och skriver effektivare kod. Men ibland optimerar de koden i förtid. De använder till exempel en hash-tabell när en enkel matris räcker eller använder komplicerad cachelagring som kan läcka minne i stället för att helt enkelt omberäkna värden. Även om du är en upplevelseprogram programmerare bör du testa för prestanda och analysera din kod när du hittar problem.
Fakta 2: Om du inte mäter gissar du
Profiler och mått ligger inte. Profiler visar om processorn är helt inläst eller om du är blockerad på disk-I/O. Profiler visar vilken typ och hur mycket minne du allokerar och om processorn lägger mycket tid på skräpinsamling (GC).
Du bör ange prestandamål för viktiga kundupplevelser eller scenarier i din app och skriva tester för att mäta prestanda. Undersöka misslyckade tester genom att använda den vetenskapliga metoden: använd profiler för att vägleda dig, hypotesera vad problemet kan vara och testa hypotesen med ett experiment eller kodändring. Upprätta prestandamätningar över tid med regelbunden testning, så att du kan isolera ändringar som orsakar prestandaregressioner. Genom att närma dig prestandaarbetet på ett rigoröst sätt undviker du att slösa tid med koduppdateringar som du inte behöver.
Fakta 3: Bra verktyg gör hela skillnaden
Med bra verktyg kan du snabbt gå in på de största prestandaproblemen (PROCESSOR, minne eller disk) och hjälpa dig att hitta koden som orsakar dessa flaskhalsar. Microsoft levererar en mängd olika prestandaverktyg som Visual Studio Profiler och PerfView.
PerfView är ett kraftfullt verktyg som hjälper dig att fokusera på djupa problem som disk-I/O, GC-händelser och minne. Du kan samla in prestandarelaterade händelsespårning för Windows-händelser (ETW) och enkelt visa per app, per process, per stack och per trådinformation. PerfView visar hur mycket och vilken typ av minne din app allokerar och vilka funktioner eller anropsstackar som bidrar med hur mycket minne allokeringar. Mer information finns i de omfattande hjälpavsnitten, demonstrationer och videor som ingår i verktyget.
Fakta 4: Det handlar om allokeringar
Du kanske tror att det handlar om algoritmer att skapa en dynamisk .NET Framework-app, till exempel att använda snabb sortering i stället för bubbelsortering, men så är inte fallet. Den största faktorn för att skapa en dynamisk app är att allokera minne, särskilt när din app är mycket stor eller bearbetar stora mängder data.
Nästan allt arbete med att skapa dynamiska IDE-upplevelser med de nya kompilator-API:erna innebar att undvika allokeringar och hantera cachelagringsstrategier. PerfView-spårningar visar att prestanda för de nya C#- och Visual Basic-kompilatorerna sällan är CPU-bundna. Kompilatorerna kan vara I/O-bundna när de läser hundratusentals eller miljontals rader med kod, läser metadata eller genererar genererad kod. Fördröjningar i användargränssnittstråden beror nästan alla på skräpinsamling. .NET Framework GC är mycket justerat för prestanda och utför mycket av sitt arbete samtidigt medan appkoden körs. En enskild allokering kan dock utlösa en dyr gen2-samling och stoppa alla trådar.
Vanliga allokeringar och exempel
Exempeluttrycken i det här avsnittet har dolda allokeringar som verkar små. Men om en stor app kör uttrycken tillräckligt många gånger kan de orsaka hundratals megabyte, till och med gigabyte, allokeringar. Till exempel en minuts tester som simulerade en utvecklares inskrivning i redigeraren allokerade gigabyte minne och fick prestandateamet att fokusera på att skriva scenarier.
Boxning
Boxning sker när värdetyper som normalt finns på stacken eller i datastrukturer omsluts i ett objekt. Du allokerar alltså ett objekt för att lagra data och returnerar sedan en pekare till objektet. .NET Framework rutor ibland värden på grund av signaturen för en metod eller typen av en lagringsplats. Omslutning av en värdetyp i ett objekt orsakar minnesallokering. Många boxningsåtgärder kan bidra med megabyte eller gigabyte allokeringar till din app, vilket innebär att din app orsakar fler GCs. .NET Framework och språkkompilatorerna undviker boxning när det är möjligt, men ibland händer det när du minst förväntar dig det.
Om du vill se boxning i PerfView öppnar du en spårning och tittar på GC Heap Alloc Stacks under appens processnamn (kom ihåg att PerfView rapporterar om alla processer). Om du ser typer som System.Int32 och System.Char under allokeringar är du boxningsvärdetyper. Om du väljer någon av de här typerna visas de staplar och funktioner där de är boxade.
Exempel 1: strängmetoder och värdetypsargument
Den här exempelkoden illustrerar potentiellt onödig och överdriven boxning:
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);
}
}
Den här koden tillhandahåller loggningsfunktioner, så en app kan anropa Log
funktionen ofta, kanske miljontals gånger. Problemet är att anropet till string.Format
löser överbelastningen Format(String, Object, Object) .
Den här överlagringen kräver att .NET Framework boxar int
värdena till objekt för att skicka dem till det här metodanropet. En partiell korrigering är att anropa id.ToString()
och size.ToString()
skicka alla strängar (som är objekt) till anropet string.Format
. Anrop ToString()
allokerar en sträng, men allokeringen sker ändå i string.Format
.
Du kan tänka dig att det här grundläggande anropet till string.Format
bara är strängsammanfogning, så du kan skriva den här koden i stället:
var s = id.ToString() + ':' + size.ToString();
Den kodraden introducerar dock en boxningsallokering eftersom den kompileras till Concat(Object, Object, Object). .NET Framework måste markera teckenliteralen för att anropa Concat
Åtgärda till exempel 1
Den fullständiga korrigeringen är enkel. Ersätt bara teckenliteralen med en strängliteral, vilket inte medför någon boxning eftersom strängar redan är objekt:
var s = id.ToString() + ":" + size.ToString();
Exempel 2: räkna upp boxning
Det här exemplet var ansvarigt för en enorm mängd allokering i de nya C#- och Visual Basic-kompilatorerna på grund av frekvent användning av uppräkningstyper, särskilt i ordlistesökningsåtgärder.
public enum Color
{
Red, Green, Blue
}
public class BoxingExample
{
private string name;
private Color color;
public override int GetHashCode()
{
return name.GetHashCode() ^ color.GetHashCode();
}
}
Det här problemet är mycket subtilt. PerfView skulle rapportera detta som GetHashCode() boxning eftersom metoden rutor den underliggande representationen av uppräkningstypen av implementeringsskäl. Om du tittar noga i PerfView kan du se två boxningsallokeringar för varje anrop till GetHashCode(). Kompilatorn infogar en och .NET Framework infogar den andra.
Åtgärda till exempel 2
Du kan enkelt undvika båda allokeringarna genom att casta till den underliggande representationen innan du anropar GetHashCode():
((int)color).GetHashCode()
En annan vanlig källa till boxning på uppräkningstyper är Enum.HasFlag(Enum) metoden. Argumentet som skickas till HasFlag(Enum) måste boxas. I de flesta fall är det enklare och allokeringsfritt att ersätta anrop till Enum.HasFlag(Enum) med ett bitvis test.
Tänk på det första prestandafaktaet (dvs. optimera inte i förtid) och börja inte skriva om all kod på det här sättet. Var medveten om dessa boxningskostnader, men ändra din kod först efter profilering av din app och hitta hot spots.
Strängar
Strängmanipuleringar är några av de största syndarna för allokeringar, och de visas ofta i PerfView i de fem främsta allokeringarna. Program använder strängar för serialisering, JSON och REST-API:er. Du kan använda strängar som programmatiska konstanter för att samverka med system när du inte kan använda uppräkningstyper. När profileringen visar att strängarna påverkar prestandan i hög grad letar du efter anrop till String metoder som Format, Concat, Split, Join, Substringoch så vidare. Att använda StringBuilder för att undvika kostnaden för att skapa en sträng från många delar hjälper, men även allokering av StringBuilder objektet kan bli en flaskhals som du behöver hantera.
Exempel 3: strängåtgärder
C#-kompilatorn hade den här koden som skriver texten i en formaterad XML-dokumentkommentar:
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 { /* ... */ }
Du kan se att den här koden utför mycket strängmanipulering. Koden använder biblioteksmetoder för att dela upp rader i separata strängar, för att trimma tomt utrymme, för att kontrollera om argumentet text
är en XML-dokumentationskommentare och för att extrahera delsträngar från rader.
På den första raden inuti WriteFormattedDocComment
allokerar anropet text.Split
en ny matris med tre element som argument varje gång det anropas. Kompilatorn måste generera kod för att allokera den här matrisen varje gång. Det beror på att kompilatorn inte vet om Split lagrar matrisen någonstans där matrisen kan ändras av annan kod, vilket skulle påverka senare anrop till WriteFormattedDocComment
. Anropet till Split allokerar även en sträng för varje rad i text
och allokerar annat minne för att utföra åtgärden.
WriteFormattedDocComment
har tre anrop till TrimStart metoden. Två finns i inre loopar som duplicerar arbete och allokeringar. Om du vill göra saken värre allokerar anrop av TrimStart metoden utan argument en tom matris (för parametern params
) utöver strängresultatet.
Slutligen finns det ett anrop till Substring metoden, som vanligtvis allokerar en ny sträng.
Åtgärda till exempel 3
Till skillnad från tidigare exempel kan små redigeringar inte åtgärda dessa allokeringar. Du måste ta ett steg tillbaka, titta på problemet och närma dig det på ett annat sätt. Du ser till exempel att argumentet till WriteFormattedDocComment()
är en sträng som har all information som metoden behöver, så att koden kan göra mer indexering i stället för att allokera många partiella strängar.
Kompilatorns prestandateam hanterade alla dessa allokeringar med kod som den här:
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...
Den första versionen av WriteFormattedDocComment()
allokerade en matris, flera delsträngar och en trimmad delsträng tillsammans med en tom params
matris. Den sökte också efter "///". Den reviderade koden använder endast indexering och allokerar ingenting. Den hittar det första tecknet som inte är tomt utrymme och kontrollerar sedan tecknet efter tecken för att se om strängen börjar med "///". Den nya koden använder IndexOfFirstNonWhiteSpaceChar
i stället för TrimStart att returnera det första indexet (efter ett angivet startindex) där ett icke-blankstegstecken inträffar. Korrigeringen är inte klar, men du kan se hur du använder liknande korrigeringar för en komplett lösning. Genom att tillämpa den här metoden i koden kan du ta bort alla allokeringar i WriteFormattedDocComment()
.
Exempel 4: StringBuilder
I det här exemplet används ett StringBuilder objekt. Följande funktion genererar ett fullständigt typnamn för generiska typer:
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 ligger på den rad som skapar en ny StringBuilder instans. Koden orsakar en allokering för sb.ToString()
och interna allokeringar inom implementeringen StringBuilder , men du kan inte styra dessa allokeringar om du vill ha strängresultatet.
Korrigering till exempel 4
Om du vill åtgärda StringBuilder
objektallokeringen cachelagrade du objektet. Även cachelagring av en enskild instans som kan kastas bort kan förbättra prestanda avsevärt. Det här är funktionens nya implementering och utelämnar all kod förutom de nya första och sista raderna:
// 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);
}
De viktigaste delarna är de nya AcquireBuilder()
och GetStringAndReleaseBuilder()
funktionerna:
[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;
}
Eftersom de nya kompilatorerna använder trådning använder dessa implementeringar ett trådstatiskt fält (ThreadStaticAttribute attribut) för att cachelagrar StringBuilder, och du kan förmodligen avstå från deklarationen ThreadStatic
. Det trådstatiska fältet innehåller ett unikt värde för varje tråd som kör den här koden.
AcquireBuilder()
returnerar den cachelagrade StringBuilder instansen om det finns en, efter att ha rensat den och angett fältet eller cacheminnet till null. Annars AcquireBuilder()
skapar du en ny instans och returnerar den, vilket gör att fältet eller cacheminnet är inställt på null.
När du är klar med StringBuilder anropar GetStringAndReleaseBuilder()
du för att hämta strängresultatet, sparar instansen StringBuilder i fältet eller cachen och returnerar sedan resultatet. Det är möjligt för körning att ange den här koden igen och skapa flera StringBuilder objekt (även om det sällan händer). Koden sparar endast den senast utgivna instansen StringBuilder för senare användning. Den här enkla cachelagringsstrategin minskade avsevärt allokeringarna i de nya kompilatorerna. Delar av .NET Framework och MSBuild ("MSBuild") använder en liknande teknik för att förbättra prestanda.
Den här enkla cachelagringsstrategin följer god cachedesign eftersom den har ett storlekstak. Det finns dock mer kod nu än i originalet, vilket innebär mer underhållskostnader. Du bör bara använda cachelagringsstrategin om du har hittat ett prestandaproblem, och PerfView har visat att StringBuilder allokeringar är en betydande deltagare.
LINQ och lambdas
Språkintegrerad fråga (LINQ), tillsammans med lambda-uttryck, är ett exempel på en produktivitetsfunktion. Dess användning kan dock ha en betydande inverkan på prestanda över tid, och du kanske upptäcker att du behöver skriva om koden.
Exempel 5: Lambdas, List<T> och IEnumerable<T>
I det här exemplet används LINQ och funktionell formatkod för att hitta en symbol i kompilatorns modell, med en namnsträng:
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);
}
}
Den nya kompilatorn och IDE-funktionerna som bygger på det anropar FindMatchingSymbol()
mycket ofta, och det finns flera dolda allokeringar i den här funktionens enda kodrad. Om du vill undersöka dessa allokeringar delar du först upp funktionens enda kodrad i två rader:
Func<Symbol, bool> predicate = s => s.Name == name;
return symbols.FirstOrDefault(predicate);
På den första raden stängs lambda-uttrycket s => s.Name == name
över den lokala variabeln name
. Det innebär att förutom att allokera ett objekt för ombudet som predicate
innehåller allokerar koden en statisk klass för att lagra miljön som samlar in värdet för name
. Kompilatorn genererar kod som följande:
// 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);
De två new
allokeringarna (en för miljöklassen och en för ombudet) är explicita nu.
Titta nu på anropet till FirstOrDefault
. Den här tilläggsmetoden på System.Collections.Generic.IEnumerable<T> typen medför också en allokering. Eftersom FirstOrDefault
tar ett IEnumerable<T> objekt som sitt första argument kan du expandera anropet till följande kod (förenklat lite för diskussion):
// 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);
Variabeln symbols
har typen List<T>. Samlingstypen List<T> implementerar IEnumerable<T> och definierar skickligt en uppräknare (IEnumerator<T> gränssnitt) som List<T> implementeras med en struct
. Att använda en struktur i stället för en klass innebär att du vanligtvis undviker alla heapallokeringar, vilket i sin tur kan påverka skräpinsamlingens prestanda. Uppräknare används vanligtvis med språkets loop, som använder uppräkningsstrukturen när den returneras i anropsstacken foreach
. Att öka anropsstackens pekare för att göra plats för ett objekt påverkar inte GC som en heapallokering gör.
När det gäller det expanderade FirstOrDefault
anropet måste koden anropa GetEnumerator()
på en IEnumerable<T>. Om symbols
du tilldelar variabeln enumerable
av typen IEnumerable<Symbol>
förloras informationen om att det faktiska objektet är en List<T>. Det innebär att när koden hämtar uppräknaren med enumerable.GetEnumerator()
måste .NET Framework ange den returnerade strukturen för att tilldela den till variabeln enumerator
.
Korrigering till exempel 5
Korrigeringen är att skriva om FindMatchingSymbol
på följande sätt och ersätta den enda kodraden med sex kodrader som fortfarande är koncisa, lätta att läsa och förstå och enkla att underhålla:
public Symbol FindMatchingSymbol(string name)
{
foreach (Symbol s in symbols)
{
if (s.Name == name)
return s;
}
return null;
}
Den här koden använder inte LINQ-tilläggsmetoder, lambdas eller uppräknare, och den medför inga allokeringar. Det finns inga allokeringar eftersom kompilatorn kan se att symbols
samlingen är en List<T> och kan binda den resulterande uppräknaren (en struktur) till en lokal variabel med rätt typ för att undvika boxning. Den ursprungliga versionen av den här funktionen var ett bra exempel på den uttrycksfulla kraften i C# och produktiviteten i .NET Framework. Den här nya och effektivare versionen bevarar dessa egenskaper utan att lägga till någon komplex kod att underhålla.
Cachelagring av asynkron metod
Nästa exempel visar ett vanligt problem när du försöker använda cachelagrade resultat i en asynkron metod.
Exempel 6: cachelagring i asynkrona metoder
Visual Studio IDE-funktionerna som bygger på de nya C#- och Visual Basic-kompilatorerna hämtar ofta syntaxträd, och kompilatorerna använder asynkronisering när de gör det för att hålla Visual Studio responsivt. Här är den första versionen av koden som du kan skriva för att hämta ett syntaxträd:
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;
}
}
Du kan se att anropet GetSyntaxTreeAsync()
instansierar en Parser
, parsar koden och sedan returnerar ett Task objekt, Task<SyntaxTree>
. Den dyra delen är att allokera instansen Parser
och parsa koden. Funktionen returnerar en Task så att anropare kan invänta parsningsarbetet och frigöra användargränssnittstråden så att den svarar på användarindata.
Flera Visual Studio-funktioner kan försöka få samma syntaxträd, så du kan skriva följande kod för att cachelagrar parsningsresultatet för att spara tid och allokeringar. Den här koden ådrar sig dock en allokering:
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;
}
}
Du ser att den nya koden med cachelagring har ett SyntaxTree
fält med namnet cachedResult
. När det här fältet är null GetSyntaxTreeAsync()
utför du arbetet och sparar resultatet i cacheminnet. GetSyntaxTreeAsync()
returnerar objektet SyntaxTree
. Problemet är att när du har en async
funktion av typen Task<SyntaxTree>
, och du returnerar ett värde av typen SyntaxTree
, genererar kompilatorn kod för att allokera en aktivitet för att lagra resultatet (med hjälp Task<SyntaxTree>.FromResult()
av ). Uppgiften markeras som slutförd och resultatet är omedelbart tillgängligt. I koden för de nya kompilatorerna Task inträffade objekt som redan har slutförts så ofta att åtgärda dessa allokeringar förbättrade svarstiden märkbart.
Åtgärda till exempel 6
Om du vill ta bort den slutförda Task allokeringen kan du cachelagras aktivitetsobjektet med det slutförda resultatet:
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;
}
}
Den här koden ändrar typen av cachedResult
till Task<SyntaxTree>
och använder en async
hjälpfunktion som innehåller den ursprungliga koden från GetSyntaxTreeAsync()
. GetSyntaxTreeAsync()
använder nu operatorn null coalescing för att returnera cachedResult
om den inte är null. Om cachedResult
är null GetSyntaxTreeAsync()
anropar GetSyntaxTreeUncachedAsync()
och cachelagrar resultatet. Observera att GetSyntaxTreeAsync()
inte väntar på anropet eftersom GetSyntaxTreeUncachedAsync()
koden normalt skulle göra det. Att inte använda await innebär att när GetSyntaxTreeUncachedAsync()
returnerar objektet GetSyntaxTreeAsync()
Task returnerar Taskomedelbart . Nu är det cachelagrade resultatet ett Task, så det finns inga allokeringar för att returnera det cachelagrade resultatet.
Ytterligare överväganden
Här följer några fler punkter om potentiella problem i stora appar eller appar som bearbetar mycket data.
Ordböcker
Ordlistor används allestädes närvarande i många program, och även om ordlistor är mycket praktiska och till sin natur effektiva. De används dock ofta på ett olämpligt sätt. I Visual Studio och de nya kompilatorerna visar analysen att många av ordlistorna innehöll ett enda element eller var tomma. En tom Dictionary<TKey,TValue> har tio fält och upptar 48 byte på heapen på en x86-dator. Ordlistor är bra när du behöver en mappning eller associativ datastruktur med konstant sökning. Men när du bara har ett fåtal element slösar du mycket utrymme med hjälp av en ordlista. I stället kan du till exempel iterativt titta igenom en List<KeyValuePair\<K,V>>
, lika snabbt. Om du bara använder en ordlista för att läsa in den med data och sedan läsa från den (ett mycket vanligt mönster) kan det vara nästan lika snabbt att använda en sorterad matris med en N(log(N))-sökning, beroende på antalet element som du använder.
Klasser jämfört med strukturer
På sätt och vis ger klasser och strukturer en klassisk utrymmes-/tidsavvägning för att justera dina appar. Klasser medför 12 byte omkostnader på en x86-dator även om de inte har några fält, men de är billiga att skicka runt eftersom det bara krävs en pekare för att referera till en klassinstans. Strukturer medför inga heap-allokeringar om de inte boxas, men när du skickar stora strukturer som funktionsargument eller returnerar värden tar det cpu-tid att atomiskt kopiera alla datamedlemmar i strukturerna. Se upp för upprepade anrop till egenskaper som returnerar strukturer och cachelagrar egenskapens värde i en lokal variabel för att undvika överdriven datakopiering.
Caches
Ett vanligt prestandatrick är att cachelagra resultat. En cache utan storleksgräns eller borttagningsprincip kan dock vara en minnesläcka. Om du behåller mycket minne i cacheminnen när du bearbetar stora mängder data kan du göra så att skräpinsamlingen åsidosätter fördelarna med cachelagrade sökningar.
I den här artikeln har vi gått igenom hur du bör känna till prestandaflaskhalsssymptom som kan påverka appens svarstider, särskilt för stora system eller system som bearbetar stora mängder data. Vanliga syndare är boxning, strängmanipuleringar, LINQ och lambda, cachelagring i asynkrona metoder, cachelagring utan storleksgräns eller borttagningsprincip, olämplig användning av ordlistor och överföring av strukturer. Tänk på de fyra fakta för att justera dina appar:
Optimera inte i förtid – var produktiv och justera din app när du upptäcker problem.
Profiler ljuger inte – du gissar om du inte mäter.
Bra verktyg gör hela skillnaden – ladda ned PerfView och prova.
Det handlar om allokeringar – det är där kompilatorplattformsteamet tillbringade större delen av sin tid med att förbättra prestandan för de nya kompilatorerna.