Grote, responsieve .NET Framework-apps schrijven
Dit artikel bevat tips voor het verbeteren van de prestaties van grote .NET Framework-apps of apps die een grote hoeveelheid gegevens verwerken, zoals bestanden of databases. Deze tips zijn afkomstig van het herschrijven van de C# en Visual Basic-compilers in beheerde code en dit artikel bevat verschillende echte voorbeelden van de C#-compiler.
Het .NET Framework is zeer productief voor het bouwen van apps. Krachtige en veilige talen en een uitgebreide verzameling bibliotheken maken het bouwen van apps zeer vruchtbaar. Maar met een grote productiviteit komt de verantwoordelijkheid. U moet alle kracht van .NET Framework gebruiken, maar wees voorbereid om de prestaties van uw code af te stemmen wanneer dat nodig is.
Waarom de nieuwe compilerprestaties van toepassing zijn op uw app
Het .NET Compiler Platform (Roslyn)-team heeft de C# en Visual Basic-compilers in beheerde code herschreven om nieuwe API's te bieden voor het modelleren en analyseren van code, het bouwen van hulpprogramma's en het inschakelen van veel uitgebreidere, codebewuste ervaringen in Visual Studio. Het herschrijven van de compilers en het bouwen van Visual Studio-ervaringen op de nieuwe compilers heeft nuttige prestatie-inzichten opgeleverd die van toepassing zijn op elke grote .NET Framework-app of elke app die veel gegevens verwerkt. U hoeft niet te weten over compilers om te profiteren van de inzichten en voorbeelden van de C#-compiler.
Visual Studio maakt gebruik van de compiler-API's om alle IntelliSense-functies te bouwen die gebruikers leuk vinden, zoals kleuren van id's en trefwoorden, syntaxisvoltooiingslijsten, kronkelingen voor fouten, parametertips, codeproblemen en codeacties. Visual Studio biedt deze hulp terwijl ontwikkelaars hun code typen en wijzigen, en Visual Studio moet responsief blijven terwijl de compiler voortdurend de codeontwikkelaars bewerkt.
Wanneer uw eindgebruikers met uw app communiceren, verwachten ze dat deze responsief is. Typen of opdrachtafhandeling mag nooit worden geblokkeerd. Help moet snel verschijnen of opgeven als de gebruiker doorgaat met typen. Uw app moet voorkomen dat de UI-thread wordt geblokkeerd met lange berekeningen waardoor de app traag aanvoelt.
Zie de .NET Compiler Platform SDK voor meer informatie over Roslyn-compilers.
Alleen de feiten
Houd rekening met deze feiten bij het afstemmen van prestaties en het maken van responsieve .NET Framework-apps.
Feit 1: Voortijdige optimalisaties zijn niet altijd het gedoe waard
Het schrijven van code die complexer is dan nodig is voor onderhoud, foutopsporing en het polijsten van kosten. Ervaren programmeurs hebben een intuïtief inzicht in het oplossen van codeproblemen en het schrijven van efficiëntere code. Ze optimaliseren hun code echter soms voortijdig. Ze gebruiken bijvoorbeeld een hash-tabel wanneer een eenvoudige matrix volstaat of complexe caching gebruiken die geheugen kan lekken in plaats van alleen waarden opnieuw te compileren. Zelfs als u een ervaringsprogrammeur bent, moet u testen op prestaties en uw code analyseren wanneer u problemen ondervindt.
Feit 2: Als je niet meet, raad je
Profielen en metingen liegen niet. Profielen laten zien of de CPU volledig is geladen of of u bent geblokkeerd op schijf-I/O. Profielen vertellen u wat voor soort en hoeveel geheugen u toegeeft en of uw CPU veel tijd besteedt aan garbagecollection (GC).
U moet prestatiedoelen instellen voor belangrijke klantervaringen of scenario's in uw app en tests schrijven om de prestaties te meten. Onderzoek mislukte tests door de wetenschappelijke methode toe te passen: gebruik profielen om u te begeleiden, te hypotheseren wat het probleem kan zijn en uw hypothese te testen met een experiment of codewijziging. Stel metingen voor basisprestaties in de loop van de tijd vast met regelmatige tests, zodat u wijzigingen kunt isoleren die regressies in de prestaties veroorzaken. Als u de prestaties op een strikte manier nadert, vermijdt u tijd met code-updates die u niet nodig hebt.
Feit 3: Goede tools maken het verschil
Met goede hulpprogramma's kunt u snel inzoomen op de grootste prestatieproblemen (CPU, geheugen of schijf) en kunt u de code vinden die deze knelpunten veroorzaakt. Microsoft biedt verschillende prestatiehulpprogramma's, zoals Visual Studio Profiler en PerfView.
PerfView is een krachtig hulpprogramma waarmee u zich kunt richten op diepe problemen, zoals schijf-I/O, GC-gebeurtenissen en geheugen. U kunt prestatiegerelateerde gebeurtenistracering voor Windows-gebeurtenissen (ETW) vastleggen en eenvoudig per app, per proces, per stack en per threadgegevens weergeven. PerfView laat zien hoeveel en welk soort geheugen uw app toewijst, en welke functies of aanroepstacks bijdragen aan de geheugentoewijzingen. Zie de uitgebreide Help-onderwerpen, demo's en video's die bij het hulpprogramma zijn opgenomen voor meer informatie.
Feit 4: Het draait allemaal om toewijzingen
U denkt misschien dat het bouwen van een responsieve .NET Framework-app allemaal draait om algoritmen, zoals het gebruik van snelle sortering in plaats van bellen, maar dat is niet het geval. De grootste factor bij het bouwen van een responsieve app is het toewijzen van geheugen, met name wanneer uw app erg groot is of grote hoeveelheden gegevens verwerkt.
Bijna alle werkzaamheden voor het bouwen van responsieve IDE-ervaringen met de nieuwe compiler-API's zijn betrokken bij het vermijden van toewijzingen en het beheren van cachestrategieën. PerfView-traceringen laten zien dat de prestaties van de nieuwe C# en Visual Basic-compilers zelden cpu-gebonden zijn. De compilers kunnen I/O-gebonden zijn bij het lezen van honderdduizenden of miljoenen regels code, het lezen van metagegevens of het verzenden van gegenereerde code. De vertragingen in de UI-thread zijn bijna allemaal vanwege garbagecollection. De .NET Framework GC is sterk afgestemd op prestaties en doet veel van het werk gelijktijdig terwijl app-code wordt uitgevoerd. Een enkele toewijzing kan echter een dure Gen2-verzameling activeren, waardoor alle threads worden gestopt.
Algemene toewijzingen en voorbeelden
De voorbeeldexpressies in deze sectie bevatten verborgen toewijzingen die klein lijken. Als een grote app echter de expressies genoeg keer uitvoert, kunnen deze honderden megabytes, zelfs gigabytes, toewijzingen veroorzaken. Tests van één minuut die bijvoorbeeld het typen van een ontwikkelaar in de editor hebben gesimuleerd, hebben gigabytes aan geheugen toegewezen en het prestatieteam ertoe geleid om zich te concentreren op het typen van scenario's.
Boksen
Boksen vindt plaats wanneer waardetypen die normaal op de stack of in gegevensstructuren leven, in een object worden verpakt. Dat wil gezegd: u wijst een object toe om de gegevens vast te houden en retourneert vervolgens een aanwijzer naar het object. Het .NET Framework vakken soms waarden als gevolg van de handtekening van een methode of het type opslaglocatie. Het verpakken van een waardetype in een object veroorzaakt geheugentoewijzing. Veel boksbewerkingen kunnen megabytes of gigabytes aan toewijzingen aan uw app bijdragen, wat betekent dat uw app meer GCs veroorzaakt. Het .NET Framework en de taalcompilers vermijden indien mogelijk boksen, maar soms gebeurt het wanneer u het minst verwacht.
Als u boksen in PerfView wilt zien, opent u een tracering en bekijkt u GC Heap Alloc Stacks onder de procesnaam van uw app (onthoud, PerfView-rapporten over alle processen). Als u typen ziet zoals System.Int32 en System.Char onder toewijzingen, gebruikt u waardetypen voor boksen. Als u een van deze typen kiest, worden de stapels en functies weergegeven waarin ze zijn geplaatst.
Voorbeeld 1: tekenreeksmethoden en waardetypeargumenten
Deze voorbeeldcode illustreert mogelijk onnodige en overmatige boksen:
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);
}
}
Deze code biedt functionaliteit voor logboekregistratie, dus een app kan de Log
functie vaak aanroepen, misschien miljoenen keren. Het probleem is dat de oproep om de overbelasting op te string.Format
Format(String, Object, Object) lossen.
Voor deze overbelasting moet .NET Framework de int
waarden in objecten in een vak plaatsen om ze door te geven aan deze methode-aanroep. Een gedeeltelijke oplossing is het aanroepen id.ToString()
en size.ToString()
doorgeven van alle tekenreeksen (die objecten zijn) aan de string.Format
aanroep. Aanroepen ToString()
wijzen wel een tekenreeks toe, maar die toewijzing vindt toch plaats binnen string.Format
.
U kunt er rekening mee houden dat deze eenvoudige aanroep string.Format
alleen tekenreekssamenvoeging is, dus u kunt in plaats daarvan deze code schrijven:
var s = id.ToString() + ':' + size.ToString();
Deze coderegel introduceert echter een bokstoewijzing omdat deze wordt gecompileerd naar Concat(Object, Object, Object). Het .NET Framework moet het letterlijke tekenvak gebruiken om aan te roepen Concat
Oplossing bijvoorbeeld 1
De volledige oplossing is eenvoudig. Vervang de letterlijke tekenreeks door een letterlijke tekenreeks, waardoor er geen boksen optreedt omdat tekenreeksen al objecten zijn:
var s = id.ToString() + ":" + size.ToString();
Voorbeeld 2: opsomming boksen
Dit voorbeeld was verantwoordelijk voor een enorme hoeveelheid toewijzing in de nieuwe C# en Visual Basic-compilers vanwege frequent gebruik van opsommingstypen, met name in opzoekbewerkingen voor woordenlijst.
public enum Color
{
Red, Green, Blue
}
public class BoxingExample
{
private string name;
private Color color;
public override int GetHashCode()
{
return name.GetHashCode() ^ color.GetHashCode();
}
}
Dit probleem is heel subtiel. PerfView rapporteert dit als GetHashCode() boksen omdat de methode om implementatieredenen de onderliggende weergave van het opsommingstype bevat. Als u goed kijkt in PerfView, ziet u mogelijk twee bokstoewijzingen voor elke aanroep naar GetHashCode(). De compiler voegt er een in en het .NET Framework voegt de andere in.
Oplossing bijvoorbeeld 2
U kunt beide toewijzingen eenvoudig vermijden door naar de onderliggende weergave te casten voordat u het volgende aanroept GetHashCode():
((int)color).GetHashCode()
Een andere veelvoorkomende bron van boksen op opsommingstypen is de Enum.HasFlag(Enum) methode. Het argument waaraan wordt doorgegeven HasFlag(Enum) , moet worden in een vak geplaatst. In de meeste gevallen is het vervangen van aanroepen naar Enum.HasFlag(Enum) een bitwise-test eenvoudiger en toewijzingsvrij.
Houd rekening met het eerste prestatie-feit (dat wil gezegd, niet voortijdig optimaliseren) en begin niet al uw code op deze manier te herschrijven. Houd rekening met deze bokskosten, maar pas na het profileren van uw app en het vinden van de hotspots.
Tekenreeksen
Tekenreeksmanipulaties zijn enkele van de grootste verantwoordelijken voor toewijzingen en ze worden vaak weergegeven in PerfView in de top vijf toewijzingen. Programma's gebruiken tekenreeksen voor serialisatie, JSON en REST API's. U kunt tekenreeksen gebruiken als programmatische constanten voor samenwerking met systemen wanneer u geen opsommingstypen kunt gebruiken. Wanneer uw profilering laat zien dat tekenreeksen zeer van invloed zijn op de prestaties, zoekt u naar aanroepen naar String methoden zoals Format, Concat, Split, Join, , Substring, enzovoort. Het gebruik StringBuilder om de kosten van het maken van één tekenreeks uit veel stukken te voorkomen, maar zelfs het toewijzen van het StringBuilder object kan een knelpunt worden dat u moet beheren.
Voorbeeld 3: tekenreeksbewerkingen
De C#-compiler had deze code waarmee de tekst van een opgemaakte XML-documentopmerking wordt geschreven:
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 { /* ... */ }
U kunt zien dat deze code veel tekenreeksbewerkingen doet. In de code worden bibliotheekmethoden gebruikt om regels te splitsen in afzonderlijke tekenreeksen, om witruimte te knippen, om te controleren of het argument text
een opmerking in de XML-documentatie is en subtekenreeksen uit regels te extraheren.
Op de eerste regel binnen WriteFormattedDocComment
wijst de text.Split
aanroep een nieuwe matrix met drie elementen toe als het argument telkens wanneer deze wordt aangeroepen. De compiler moet code verzenden om deze matrix telkens toe te wijzen. Dat komt omdat de compiler niet weet of Split de matrix ergens wordt opgeslagen waar de matrix kan worden gewijzigd door andere code, wat van invloed zou zijn op latere aanroepen naar WriteFormattedDocComment
. De aanroep om ook een tekenreeks toe te Split wijzen voor elke regel in text
en wijst ander geheugen toe om de bewerking uit te voeren.
WriteFormattedDocComment
heeft drie aanroepen naar de TrimStart methode. Twee bevinden zich in binnenste lussen die werk en toewijzingen dupliceren. Om het nog erger te maken, wijst het aanroepen van de TrimStart methode zonder argumenten een lege matrix (voor de params
parameter) toe naast het tekenreeksresultaat.
Ten slotte is er een aanroep naar de Substring methode, die meestal een nieuwe tekenreeks toewijst.
Oplossing bijvoorbeeld 3
In tegenstelling tot de eerdere voorbeelden kunnen kleine bewerkingen deze toewijzingen niet herstellen. U moet terugstappen, het probleem bekijken en het anders benaderen. U ziet bijvoorbeeld dat het argument een WriteFormattedDocComment()
tekenreeks is die alle informatie bevat die de methode nodig heeft, zodat de code meer indexering kan uitvoeren in plaats van veel gedeeltelijke tekenreeksen toe te wijzen.
Het prestatieteam van de compiler heeft al deze toewijzingen met code als volgt aangepakt:
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...
De eerste versie van het toewijzen van WriteFormattedDocComment()
een matrix, verschillende subtekenreeksen en een bijgesneden subtekenreeks samen met een lege params
matrix. Het is ook gecontroleerd op '///'. De herziene code maakt alleen gebruik van indexering en wijst niets toe. Er wordt het eerste teken gevonden dat geen witruimte is en controleert vervolgens het teken per teken om te zien of de tekenreeks begint met '///'. De nieuwe code gebruikt IndexOfFirstNonWhiteSpaceChar
in plaats van TrimStart de eerste index (na een opgegeven beginindex) te retourneren waarbij een niet-witruimteteken voorkomt. De oplossing is niet voltooid, maar u kunt zien hoe u vergelijkbare oplossingen toepast voor een volledige oplossing. Door deze methode in de code toe te passen, kunt u alle toewijzingen in WriteFormattedDocComment()
verwijderen.
Voorbeeld 4: StringBuilder
In dit voorbeeld wordt een StringBuilder object gebruikt. Met de volgende functie wordt een volledige typenaam gegenereerd voor algemene typen:
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();
}
}
De focus ligt op de regel waarmee een nieuw StringBuilder exemplaar wordt gemaakt. De code veroorzaakt een toewijzing voor sb.ToString()
en interne toewijzingen binnen de StringBuilder implementatie, maar u kunt deze toewijzingen niet beheren als u het tekenreeksresultaat wilt.
Oplossing bijvoorbeeld 4
Als u de StringBuilder
objecttoewijzing wilt herstellen, slaat u het object in de cache op. Zelfs het opslaan van één exemplaar dat kan worden weggegooid, kan de prestaties aanzienlijk verbeteren. Dit is de nieuwe implementatie van de functie, die alle code weglaat, met uitzondering van de nieuwe eerste en laatste regels:
// 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 belangrijkste onderdelen zijn de nieuwe AcquireBuilder()
en GetStringAndReleaseBuilder()
functies:
[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;
}
Omdat de nieuwe compilers threading gebruiken, gebruiken deze implementaties een thread-statisch veld (ThreadStaticAttributekenmerk) om de declaratie in de StringBuilderThreadStatic
cache op te nemen. Het statische threadveld bevat een unieke waarde voor elke thread die deze code uitvoert.
AcquireBuilder()
retourneert het exemplaar StringBuilder in de cache als er een is, na het wissen en het veld of de cache in te stellen op null. AcquireBuilder()
Anders maakt u een nieuw exemplaar en retourneert u het, waarbij het veld of de cache is ingesteld op null.
Wanneer u klaar StringBuilder bent, roept GetStringAndReleaseBuilder()
u aan om het resultaat van de tekenreeks op te halen, slaat u het StringBuilder exemplaar op in het veld of de cache en retourneert u het resultaat. Het is mogelijk om deze code opnieuw in te voeren en meerdere StringBuilder objecten te maken (hoewel dat zelden gebeurt). De code slaat alleen het laatst uitgebrachte StringBuilder exemplaar op voor later gebruik. Deze eenvoudige cachingstrategie vermindert aanzienlijk de toewijzingen in de nieuwe compilers. Onderdelen van .NET Framework en MSBuild ("MSBuild") gebruiken een vergelijkbare techniek om de prestaties te verbeteren.
Deze eenvoudige cachestrategie voldoet aan een goed cacheontwerp omdat deze een groottelimiet heeft. Er is echter nu meer code dan in het origineel, wat betekent dat er meer onderhoudskosten zijn. U moet de cachestrategie alleen gebruiken als u een prestatieprobleem hebt gevonden en PerfView heeft aangetoond dat StringBuilder toewijzingen een aanzienlijke bijdrager zijn.
LINQ en lambdas
Language-Integrated Query (LINQ), in combinatie met lambda-expressies, is een voorbeeld van een productiviteitsfunctie. Het gebruik ervan kan echter een aanzienlijke invloed hebben op de prestaties in de loop van de tijd en mogelijk zult u uw code moeten herschrijven.
Voorbeeld 5: Lambdas, Lijst<T> en IEnumerable<T>
In dit voorbeeld wordt LINQ en functionele stijlcode gebruikt om een symbool in het model van de compiler te vinden, met een naamtekenreeks:
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);
}
}
De nieuwe compiler en de IDE-ervaringen die erop zijn gebouwd, worden zeer vaak aangeroepen FindMatchingSymbol()
en er zijn verschillende verborgen toewijzingen in de coderegel van deze functie. Als u deze toewijzingen wilt onderzoeken, splitst u eerst de enkele coderegel van de functie op in twee regels:
Func<Symbol, bool> predicate = s => s.Name == name;
return symbols.FirstOrDefault(predicate);
In de eerste regel sluit de lambda-expressie s => s.Name == name
over de lokale variabele.name
Dit betekent dat naast het toewijzen van een object aan de gemachtigde die predicate
bewaringen bevat, de code een statische klasse toewijst om de omgeving vast te leggen die de waarde van name
. De compiler genereert code als de volgende:
// 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 twee new
toewijzingen (één voor de omgevingsklasse en één voor de gemachtigde) zijn nu expliciet.
Kijk nu naar de oproep naar FirstOrDefault
. Met deze extensiemethode voor het System.Collections.Generic.IEnumerable<T> type wordt ook een toewijzing in rekening gebracht. Omdat FirstOrDefault
een IEnumerable<T> object als eerste argument wordt gebruikt, kunt u de aanroep naar de volgende code uitvouwen (een beetje eenvoudiger voor discussie):
// 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);
De symbols
variabele heeft het type List<T>. Het List<T> verzamelingstype IEnumerable<T> implementeert en definieert een enumerator (IEnumerator<T> interface) die List<T> met een struct
. Het gebruik van een structuur in plaats van een klasse betekent dat u meestal heap-toewijzingen vermijdt, die op zijn beurt de prestaties van garbagecollection kunnen beïnvloeden. Enumerators worden meestal gebruikt met de taallus foreach
, die gebruikmaakt van de enumeratorstructuur, omdat deze wordt geretourneerd op de aanroepstack. Het verhogen van de aanroepstackaanwijzer om ruimte te maken voor een object heeft geen invloed op de manier waarop een heaptoewijzing dat doet.
In het geval van de uitgebreide FirstOrDefault
aanroep moet de code een IEnumerable<T>aanroep GetEnumerator()
doen. symbols
Als u de variabele van het enumerable
type IEnumerable<Symbol>
toewijst, verliest u de informatie dat het werkelijke object een List<T>is. Dit betekent dat wanneer de code de enumerator ophaalt, enumerable.GetEnumerator()
de .NET Framework de geretourneerde structuur moet inschakelen om deze toe te wijzen aan de enumerator
variabele.
Oplossing bijvoorbeeld 5
De oplossing is om als volgt te herschrijven FindMatchingSymbol
, waarbij de coderegel wordt vervangen door zes regels code die nog steeds beknopt, gemakkelijk te lezen en te begrijpen zijn en gemakkelijk te onderhouden zijn:
public Symbol FindMatchingSymbol(string name)
{
foreach (Symbol s in symbols)
{
if (s.Name == name)
return s;
}
return null;
}
Deze code maakt geen gebruik van LINQ-extensiemethoden, lambdas of opsommingen en er worden geen toewijzingen in rekening gebracht. Er zijn geen toewijzingen omdat de compiler kan zien dat de symbols
verzameling een List<T> is en de resulterende enumerator (een structuur) kan binden aan een lokale variabele met het juiste type om boksen te voorkomen. De oorspronkelijke versie van deze functie was een goed voorbeeld van de expressieve kracht van C# en de productiviteit van .NET Framework. Deze nieuwe en efficiëntere versie behoudt deze kwaliteiten zonder complexe code toe te voegen om te onderhouden.
Async-methode opslaan in cache
In het volgende voorbeeld ziet u een veelvoorkomend probleem wanneer u probeert in de cache opgeslagen resultaten te gebruiken in een asynchrone methode.
Voorbeeld 6: caching in asynchrone methoden
De Visual Studio IDE-functies die zijn gebouwd op de nieuwe C# en Visual Basic-compilers halen regelmatig syntaxisstructuren op en de compilers gebruiken asynchroon wanneer ze dit doen om Visual Studio responsief te houden. Hier volgt de eerste versie van de code die u kunt schrijven om een syntaxisstructuur op te halen:
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;
}
}
U kunt zien dat het aanroepen GetSyntaxTreeAsync()
een instantie van een Parser
, parseert de code en vervolgens een Task object retourneert, Task<SyntaxTree>
. Het dure deel is het toewijzen van het Parser
exemplaar en het parseren van de code. De functie retourneert een Task zodat bellers kunnen wachten op het parseringswerk en de UI-thread kunnen vrijmaken om responsief te zijn op gebruikersinvoer.
Verschillende Visual Studio-functies proberen mogelijk dezelfde syntaxisstructuur te krijgen, dus u kunt de volgende code schrijven om het parseringsresultaat op te slaan om tijd en toewijzingen te besparen. Deze code brengt echter een toewijzing met zich mee:
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;
}
}
U ziet dat de nieuwe code met caching een veld heeft met de SyntaxTree
naam cachedResult
. Wanneer dit veld null is, GetSyntaxTreeAsync()
wordt het werk uitgevoerd en wordt het resultaat opgeslagen in de cache. GetSyntaxTreeAsync()
retourneert het SyntaxTree
object. Het probleem is dat wanneer u een async
functie van het type Task<SyntaxTree>
hebt en u een waarde van het type SyntaxTree
retourneert, de compiler code verzendt om een taak toe te wijzen voor het resultaat (met behulp van Task<SyntaxTree>.FromResult()
). De taak is gemarkeerd als voltooid en het resultaat is onmiddellijk beschikbaar. In de code voor de nieuwe compilers Task zijn objecten die al zijn voltooid zo vaak opgetreden dat het corrigeren van deze toewijzingen de reactiesnelheid aanzienlijk verbetert.
Oplossing bijvoorbeeld 6
Als u de voltooide Task toewijzing wilt verwijderen, kunt u het taakobject in de cache opslaan met het voltooide resultaat:
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;
}
}
Deze code wijzigt het type van Task<SyntaxTree>
en maakt gebruik van cachedResult
een async
helper-functie die de oorspronkelijke code bevat.GetSyntaxTreeAsync()
GetSyntaxTreeAsync()
maakt nu gebruik van de samensnookoperator null om te retourneren cachedResult
als deze niet null is. Als cachedResult
null is, roept GetSyntaxTreeAsync()
GetSyntaxTreeUncachedAsync()
u het resultaat aan en slaat u het in de cache op. U ziet dat GetSyntaxTreeAsync()
de aanroep GetSyntaxTreeUncachedAsync()
niet in afwachting is van de normale code. Niet gebruiken van await betekent dat wanneer GetSyntaxTreeUncachedAsync()
het object wordt geretourneerd Task , GetSyntaxTreeAsync()
onmiddellijk de Task. Het resultaat in de cache is nu een Task, dus er zijn geen toewijzingen om het resultaat in de cache te retourneren.
Aanvullende overwegingen
Hier volgen nog enkele punten over mogelijke problemen in grote apps of apps die veel gegevens verwerken.
Woordenboeken
Woordenlijsten worden in veel programma's alom gebruikt en hoewel woordenlijsten erg handig en inherent efficiënt zijn. Ze worden echter vaak ongepast gebruikt. In Visual Studio en de nieuwe compilers toont analyse aan dat veel van de woordenlijsten één element bevatten of leeg waren. Een lege Dictionary<TKey,TValue> heeft tien velden en neemt 48 bytes in beslag op de heap op een x86-machine. Woordenlijsten zijn handig wanneer u een toewijzings- of associatieve gegevensstructuur nodig hebt met constant zoeken. Als u echter maar een paar elementen hebt, verspilt u veel ruimte door een woordenlijst te gebruiken. In plaats daarvan kunt u bijvoorbeeld een , net zo snel, iteratief bekijken List<KeyValuePair\<K,V>>
. Als u een woordenlijst alleen gebruikt om deze met gegevens te laden en deze vervolgens te lezen (een veelvoorkomend patroon), kan het gebruik van een gesorteerde matrix met een N(log(N)-zoekactie bijna zo snel zijn, afhankelijk van het aantal elementen dat u gebruikt.
Klassen versus structuren
Klassen en structuren bieden op een manier een klassieke ruimte/tijdafweging voor het afstemmen van uw apps. Klassen hebben 12 bytes overhead op een x86-computer, zelfs als ze geen velden hebben, maar ze zijn goedkoop om door te geven, omdat er alleen een aanwijzer nodig is om naar een klasse-exemplaar te verwijzen. Structuren hebben geen heap-toewijzingen als ze niet zijn geplaatst, maar wanneer u grote structuren doorgeeft als functieargumenten of waarden retourneert, duurt het CPU-tijd om alle gegevensleden van de structuren atomisch te kopiëren. Let op herhaalde aanroepen naar eigenschappen die structuren retourneren en cache de waarde van de eigenschap in een lokale variabele opslaan om overmatig kopiëren van gegevens te voorkomen.
Caches
Een veelvoorkomende prestatietrog is het opslaan van resultaten in de cache. Een cache zonder een groottelimiet of verwijderingsbeleid kan echter een geheugenlek zijn. Bij het verwerken van grote hoeveelheden gegevens, als u veel geheugen in caches ingedrukt houdt, kunt u ervoor zorgen dat garbagecollection de voordelen van uw opzoekacties in de cache overschrijft.
In dit artikel hebben we besproken hoe u rekening moet houden met prestatieknelpunten die de reactiesnelheid van uw app kunnen beïnvloeden, met name voor grote systemen of systemen die een grote hoeveelheid gegevens verwerken. Veelvoorkomende koppen zijn boksen, tekenreeksmanipulaties, LINQ en lambda, opslaan in asynchrone methoden, opslaan in cache zonder een groottelimiet of verwijderingsbeleid, ongepast gebruik van woordenlijsten en het doorgeven van structuren. Houd rekening met de vier feiten voor het afstemmen van uw apps:
Optimaliseer niet voortijdig: wees productief en stem uw app af wanneer u problemen ondervindt.
Profielen liegen niet. U raadt aan of u niet meet.
Goede hulpprogramma's maken het verschil– download PerfView en probeer het uit.
Het gaat allemaal om toewijzingen: het platformteam van de compiler heeft het grootste deel van hun tijd besteed aan het verbeteren van de prestaties van de nieuwe compilers.