Dela via


Minska minnesallokering med hjälp av nya C#-funktioner

Viktigt!

De tekniker som beskrivs i det här avsnittet förbättrar prestanda när de tillämpas på heta sökvägar i koden. Heta sökvägar är de avsnitt i din kodbas som körs ofta och upprepade gånger i normala åtgärder. Om du använder dessa tekniker för kod som inte körs ofta får du minimal påverkan. Innan du gör några ändringar för att förbättra prestandan är det viktigt att mäta en baslinje. Analysera sedan baslinjen för att avgöra var minnesflaskhalsar uppstår. Du kan lära dig om många plattformsoberoende verktyg för att mäta programmets prestanda i avsnittet om diagnostik och instrumentation. Du kan öva på en profileringssession i självstudiekursen för att mäta minnesanvändning i Visual Studio-dokumentationen.

När du har mätt minnesanvändningen och har fastställt att du kan minska allokeringen använder du teknikerna i det här avsnittet för att minska allokeringen. Efter varje efterföljande ändring mäter du minnesanvändningen igen. Kontrollera att varje ändring har en positiv inverkan på minnesanvändningen i ditt program.

Prestandaarbete i .NET innebär ofta att ta bort allokeringar från koden. Alla minnesblock som du allokerar måste så småningom frigöras. Färre allokeringar minskar tiden som ägnas åt skräpinsamling. Det ger mer förutsägbar exekveringstid genom att ta bort skräpsamlande processer från specifika kodvägar.

En vanlig taktik för att minska allokeringen är att ändra kritiska datastrukturer från class typer till struct typer. Den här ändringen påverkar semantiken för att använda dessa typer. Parametrar och returnvärden skickas nu som värde i stället för med referens. Kostnaden för att kopiera ett värde är försumbar om typerna är små, tre ord eller mindre (med tanke på att ett ord är av naturlig storlek på ett heltal). Det är mätbart och kan ha verklig prestandapåverkan för större typer. För att bekämpa effekten av kopiering kan utvecklare skicka dessa typer genom ref att få tillbaka den avsedda semantiken.

Med C# ref -funktionerna kan du uttrycka önskad semantik för struct typer utan att påverka deras allmänna användbarhet negativt. Före dessa förbättringar behövde utvecklare använda unsafe konstruktioner med pekare och råminne för att uppnå samma prestandaeffekt. Kompilatorn genererar verifierbart säker kod för de nya ref relaterade funktionerna. Verifierbart säker kod innebär att kompilatorn identifierar möjliga buffertöverskridningar eller åtkomst till oallokerat eller frigjort minne. Kompilatorn identifierar och förhindrar vissa fel.

Skicka och returnera med referens

Variabler i C# lagrar värden. I struct typer är värdet innehållet i en instans av typen. I class typer är värdet en referens till ett minnesblock som lagrar en instans av typen. ref Att lägga till modifieraren innebär att variabeln lagrar referensen till värdet. I struct typer pekar referensen på lagringen som innehåller värdet. I class typer pekar referensen på lagringsplatsen som innehåller referensen till minnesblocket.

I C# skickas parametrar till metoder efter värde och returvärden returneras efter värde. Argumentets värde skickas till metoden. Värdet för returargumentet är returvärdet.

Modifieraren ref, in, ref readonlyeller out anger att argumentet skickas med referens. En referens till lagringsplatsen skickas till metoden. Om du lägger ref till i metodsignaturen returneras returvärdet med referens. En referens till lagringsplatsen är returvärdet.

Du kan också använda referenstilldelning för att få en variabel att referera till en annan variabel. En typisk tilldelning kopierar värdet för den högra sidan av tilldelningen till variabeln på den vänstra sidan av tilldelningen. En referenstilldelning kopierar minnesplatsen för variabeln till höger till variabeln till vänster. Nu ref refererar till den ursprungliga variabeln:

int anInteger = 42; // assignment.
ref int location = ref anInteger; // ref assignment.
ref int sameLocation = ref location; // ref assignment

Console.WriteLine(location); // output: 42

sameLocation = 19; // assignment

Console.WriteLine(anInteger); // output: 19

När du tilldelar en variabel ändrar du dess värde. När du refererar till en variabel ändrar du vad den refererar till.

Du kan arbeta direkt med lagring för värden med ref variabler, passera genom referens, och referenstilldelning. Omfångsregler som tillämpas av kompilatorn säkerställer säkerheten när du arbetar direkt med lagring.

Modifierarna ref readonly och in anger båda att argumentet ska skickas med referens och inte kan tilldelas om i -metoden. Skillnaden är att ref readonly metoden använder parametern som en variabel. Metoden kan fånga parametern eller returnera den via skrivskyddad referens. I sådana fall bör du använda ref readonly modifieraren. Annars erbjuder in modifieraren mer flexibilitet. Du behöver inte lägga till modifieraren in i ett argument för in-parametern, så du kan uppdatera befintliga API-signaturer säkert med hjälp av modifieraren in. Kompilatorn utfärdar en varning om du inte lägger till antingen ref eller in modifieraren till ett argument för en ref readonly parameter.

Referenssäker kontext

I C# finns det regler för ref-uttryck som säkerställer att ett ref-uttryck inte kan nås om den lagring det hänvisar till inte längre är giltig. Tänk på följande exempel:

public ref int CantEscape()
{
    int index = 42;
    return ref index; // Error: index's ref safe context is the body of CantEscape
}

Kompilatorn rapporterar ett fel eftersom du inte kan returnera en referens till en lokal variabel från en metod. Anroparen kan inte komma åt lagringen som hänvisas till. Referenssäker kontext definierar omfånget där ett ref uttryck är säkert att komma åt eller ändra. I följande tabell visas referenssäkra kontexter för variabeltyper. ref variabler kan inte deklareras i class eller utan referens struct, och därför finns inte dessa rader i tabellen.

Deklaration referenssäker kontext
icke-refererad lokal block där lokal variabel deklareras
icke-referensparameter nuvarande metod
ref, ref readonly, in parameter anropsmetod
out-parametern nuvarande metod
class fält anropsmetod
icke-referensfält struct nuvarande metod
ref fält av ref struct anropsmetod

En variabel kan returneras ref om dess referenssäkra kontext är anropande metod. Om den aktuella metoden eller ett block är dess säker kontext för referens är retur inte tillåten. Följande kodfragment visar två exempel. Ett medlemsfält kan nås från omfånget som kallar på en metod, så ett klass- eller structfälts referenssäkra kontext är den metod som anropar. Referenssäker kontext för en parameter med modifierarna ref eller in är hela metoden. Båda kan returneras ref från en medlemsmetod:

private int anIndex;

public ref int RetrieveIndexRef()
{
    return ref anIndex;
}

public ref int RefMin(ref int left, ref int right)
{
    if (left < right)
        return ref left;
    else
        return ref right;
}

Anmärkning

När ref readonly- eller in-modifieraren tillämpas på en parameter, kan parametern returneras av ref readonly, inte av ref.

Kompilatorn ser till att en referens inte kan komma undan sin referenssäkra kontext. Du kan använda ref parametrar, ref returnoch ref lokala variabler på ett säkert sätt eftersom kompilatorn identifierar om du av misstag har skrivit kod där ett ref uttryck kan nås när dess lagring inte är giltig.

Säker kontext och referensstrukturer

ref struct kräver fler regler för att säkerställa att de kan användas på ett säkert sätt. En ref struct typ kan innehålla ref fält. Det kräver införandet av en säker kontext. För de flesta typer är den säkra kontexten den metod som anropar. Med andra ord kan ett värde som inte är ett ref struct alltid returneras från en metod.

Informellt är den säkra kontexten för en ref struct omfattningen där alla dess ref fält kan nås. Med andra ord är det skärningspunkten mellan referenssäkra kontexten för alla dess ref fält. Metoden nedan returnerar ett ReadOnlySpan<char> till ett medlemsfält, vilket gör att dess säkra kontext är metoden:

private string longMessage = "This is a long message";

public ReadOnlySpan<char> Safe()
{
    var span = longMessage.AsSpan();
    return span;
}

Däremot genererar följande kod ett fel eftersom ref field medlemmen Span<int> i refererar till den stackallokerade matrisen med heltal. Det går inte att komma ifrån metoden:

public Span<int> M()
{
    int length = 3;
    Span<int> numbers = stackalloc int[length];
    for (var i = 0; i < length; i++)
    {
        numbers[i] = i;
    }
    return numbers; // Error! numbers can't escape this method.
}

Förena minnestyper

Introduktionen av System.Span<T> och System.Memory<T> ger en enhetlig modell för att arbeta med minne. System.ReadOnlySpan<T> och System.ReadOnlyMemory<T> tillhandahåller skrivskyddade versioner för minnesåtkomst. De ger alla en abstraktion över ett minnesblock som lagrar en matris med liknande element. Skillnaden är att Span<T> och ReadOnlySpan<T> är ref struct typer medan Memory<T> och ReadOnlyMemory<T> är struct typer. Intervall innehåller ett ref field. Därför kan instanser av ett spann inte lämna sin säkra kontext. Den säkra kontexten för en ref struct är referenssäker kontext för dess ref field. Implementeringen av Memory<T> och ReadOnlyMemory<T> ta bort den här begränsningen. Du använder dessa typer för direkt åtkomst till minnesbuffertar.

Förbättra prestanda med referenssäkerhet

Att använda dessa funktioner för att förbättra prestandan omfattar följande uppgifter:

  • Undvik allokeringar: När du ändrar en typ från en class till en structändrar du hur den lagras. Lokala variabler lagras i stacken. Medlemmar lagras infogade när containerobjektet allokeras. Den här ändringen innebär färre allokeringar och det minskar arbetet som skräpinsamlaren utför. Det kan också minska minnestrycket så att sophämtaren körs mindre ofta.
  • Bevara referenssemantik: Om du ändrar en typ från en class till en struct ändras semantiken för att skicka en variabel till en metod. Kod som ändrade tillståndet för dess parametrar behöver ändras. Nu när parametern är en structändrar metoden en kopia av det ursprungliga objektet. Du kan återställa den ursprungliga semantiken genom att skicka parametern som en ref parameter. Efter den ändringen ändrar metoden originalet struct igen.
  • Undvik att kopiera data: Kopiering av större struct typer kan påverka prestanda i vissa kodsökvägar. Du kan också lägga till ref modifieraren för att skicka större datastrukturer till metoder med referens i stället för efter värde.
  • Begränsa ändringar: När en struct typ skickas med referens kan den anropade metoden ändra structens tillstånd. Du kan ersätta ref modifieraren med ref readonly modifierarna eller in för att ange att argumentet inte kan ändras. Föredra ref readonly när metoden hanterar parametern eller returnerar den som en skrivskyddad referens. Du kan också skapa readonly struct typer eller struct typer med readonly medlemmar för att ge mer kontroll över vilka medlemmar i en struct som kan ändras.
  • Manipulera minnet direkt: Vissa algoritmer är mest effektiva när datastrukturer behandlas som ett minnesblock som innehåller en sekvens med element. Typerna Span och Memory ger säker åtkomst till minnesblock.

Ingen av dessa tekniker kräver unsafe kod. Används klokt, kan du få prestandaegenskaper från säker kod som tidigare endast var möjligt att uppnå med hjälp av osäkra tekniker. Du kan prova teknikerna själv i självstudien om att minska minnesallokeringen.