Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
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 snel reageren terwijl de compiler voortdurend de code die ontwikkelaars bewerken modelleert.
Wanneer uw eindgebruikers met uw app communiceren, verwachten ze dat deze responsief is. Typen of opdrachtafhandeling mag nooit worden geblokkeerd. Help moet meteen verschijnen of stoppen zodra 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, brengt onderhouds-, foutopsporings- en polijstkosten met zich mee. 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
Je denkt misschien dat het bouwen van een responsieve .NET Framework-app allemaal draait om algoritmen, zoals het gebruik van quicksort in plaats van bubblesort, 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 te wijten aan garbage collection. 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 voorbeelduitdrukkingen in dit deel bevatten verborgen allocaties die klein lijken. Als een grote app echter de expressies vaak genoeg uitvoert, kunnen deze honderden megabytes, zelfs gigabytes, aan allocaties 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 boxen soms waarden vanwege de signatuur van een methode of het type van een 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 het geheugenmanagement in PerfView wilt inspecteren, opent u een tracering en bekijkt u de GC Heap Alloc Stacks onder de naam van het proces van uw app (let op, PerfView rapporteert over alle processen). Als u typen ziet zoals System.Int32 en System.Char onder toewijzingen, boxet u waardetypen. 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 naar string.Format verwijst naar de Format(String, Object, Object) overload.
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 van id.ToString() en size.ToString() en het 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 boxing-allocatie omdat deze naar Concat(Object, Object, Object) wordt gecompileerd. Het .NET Framework moet het letterlijke tekenvak gebruiken om aan te roepen Concat
Reparatie voor voorbeeld 1
De volledige oplossing is eenvoudig. Vervang het letterlijke teken door een letterlijke tekenreeks, waardoor er geen boxing optreedt omdat tekenreeksen al objecten zijn.
var s = id.ToString() + ":" + size.ToString();
Voorbeeld 2: opsomming boksen
Dit voorbeeld was verantwoordelijk voor een enorme hoeveelheid geheugenallocatie in de nieuwe C#- en Visual Basic-compilers vanwege het frequente gebruik van opsommingstypen, met name in opzoekbewerkingen voor woordenboeken.
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 bokst. 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.
Fix voor voorbeeld 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 aan HasFlag(Enum) doorgegeven argument moet worden ingepakt. In de meeste gevallen is het vervangen van aanroepen naar Enum.HasFlag(Enum) door 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 boxingkosten, maar pas uw code alleen aan na het profileren van uw app en het identificeren 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 een grote invloed hebben op de prestaties, zoek dan naar aanroepen van String methoden zoals Format, Concat, Split, Join, Substring, etc. Het gebruik van StringBuilder om de kosten van het creëren van één tekenreeks uit vele delen te vermijden, helpt, 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 WriteFormattedDocCommentwijst 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 naar Split wijst ook een tekenreeks toe 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.
Reparatie van voorbeeld 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 WriteFormattedDocComment() bevatte een array, verschillende subtekenreeksen en een afgeknipte subtekenreeks, samen met een lege params array. Het werd ook gecontroleerd op "///". De bijgewerkte code gebruikt alleen indexering en verdeelt niets. 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.
Fix voor voorbeeld 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, maken deze implementaties gebruik van een thread-statisch veld (ThreadStaticAttribute-attribuut) om de StringBuilder te cachen, waardoor het waarschijnlijk mogelijk is om de ThreadStatic-declaratie achterwege te laten. Het statische threadveld bevat een unieke waarde voor elke thread die deze code uitvoert.
AcquireBuilder() geeft het in de cache opgeslagen exemplaar StringBuilder terug als deze bestaat, nadat het gewist is en het veld of de cache op nul is ingesteld. Anders wordt er een nieuwe instantie gemaakt en geretourneerd, waarbij het veld of de cache op null wordt ingesteld.
Wanneer u klaar bent met StringBuilder, roept u GetStringAndReleaseBuilder() 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 verminderde de toewijzingen in de nieuwe compilers aanzienlijk. 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, List<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, roepen FindMatchingSymbol() zeer vaak aan, en er zijn meerdere verborgen toewijzingen in de enkele 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 voor de delegate die predicate vasthoudt, de code een statische klasse toewijst om de omgeving vast te houden die de waarde van name vastlegt. 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. Bij deze extensiemethode voor het System.Collections.Generic.IEnumerable<T> type leidt dit ook tot een toewijzing. 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 implementeert IEnumerable<T> en definieert op slimme wijze een enumerator (IEnumerator<T> interface) die List<T> implementeert 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 foreach-lus van de taal, die gebruikmaakt van de enumeratorstructuur zoals 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 uitgebreid FirstOrDefault aanroep moet de code een GetEnumerator() aanroep op een IEnumerable<T> doen. Het toewijzen van symbols aan de variabele van het type enumerableIEnumerable<Symbol> leidt tot het verlies van 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 boxen om deze toe te wijzen aan de enumerator variabele.
Oplossing voor voorbeeld 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 van GetSyntaxTreeAsync() een instantie van Parser creëert, de code parseert en vervolgens een Task object retourneert, namelijk Task<SyntaxTree>. Het dure deel is het toewijzen van een Parser instance 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 SyntaxTreeretourneert, 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.
Fix voor 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 cachedResult naar Task<SyntaxTree> en maakt gebruik van een async helper-functie die de oorspronkelijke code van GetSyntaxTreeAsync() bevat.
GetSyntaxTreeAsync() maakt nu gebruik van de null-coalescing-operator om cachedResult te retourneren als deze niet null is. Als cachedResult null is, roept GetSyntaxTreeAsync()GetSyntaxTreeUncachedAsync() aan en slaat het resultaat in de cache op. Merk op dat GetSyntaxTreeAsync() niet wacht op de aanroep van GetSyntaxTreeUncachedAsync() zoals de code normaal gesproken zou doen. Het niet gebruiken van await betekent dat wanneer GetSyntaxTreeUncachedAsync() zijn Task object retourneert, GetSyntaxTreeAsync() direct de Task retourneert. 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. Woordenboeken zijn geweldig wanneer u een toewijzings- of associatieve gegevensstructuur nodig hebt met zoeken in constante tijd. Als u echter maar een paar elementen hebt, verspilt u veel ruimte door een woordenlijst te gebruiken. In plaats daarvan kunt u bijvoorbeeld net zo snel iteratief door een List<KeyValuePair\<K,V>> heen lopen. 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 het is goedkoop om ze door te sturen, omdat er alleen een pointer nodig is om naar een klasse-instantie te verwijzen. Structuren hebben geen heap-toewijzingen als ze niet zijn ingekapseld, maar wanneer u grote structuren doorgeeft als functieargumenten of waarden retourneert, kost het de CPU tijd om alle gegevensleden van de structuren atomisch te kopiëren. Let op voor herhaalde aanroepen van eigenschappen die structuren retourneren en sla de waarde van de eigenschap op in een lokale variabele 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, kan het vasthouden van veel geheugen in caches er voor zorgen dat garbage collection de voordelen van uw cache-opzoekacties tenietdoet.
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 oorzaken zijn boxing, tekenreeksmanipulaties, LINQ en lambda, caching in asynchrone methoden, caching zonder een groottelimiet of verwijderingsbeleid, ongepast gebruik van dictionaries en het doorgeven van structuren in de context van programmatuur. 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 – als u niet meet, gokt u.
Goede hulpprogramma's maken het verschil– download PerfView en probeer het uit.
Het draait allemaal om toewijzingen – het platformteam van de compiler heeft het merendeel van hun tijd besteed aan het verbeteren van de prestaties van de nieuwe compilers.