Självstudie: Minska minnesallokering med ref
säkerhet
Prestandajustering för ett .NET-program omfattar ofta två tekniker. Minska först antalet och storleken på heapallokeringar. För det andra kan du minska hur ofta data kopieras. Visual Studio innehåller bra verktyg som hjälper dig att analysera hur ditt program använder minne. När du har fastställt var din app gör onödiga allokeringar gör du ändringar för att minimera dessa allokeringar. Du konverterar class
typer till struct
typer. Du använder ref
säkerhetsfunktioner för att bevara semantik och minimera extra kopiering.
Använd Visual Studio 17.5 för att få bästa möjliga upplevelse med den här självstudien. Det .NET-objektallokeringsverktyg som används för att analysera minnesanvändning är en del av Visual Studio. Du kan använda Visual Studio Code och kommandoraden för att köra programmet och göra alla ändringar. Du kommer dock inte att kunna se analysresultaten för dina ändringar.
Programmet du använder är en simulering av ett IoT-program som övervakar flera sensorer för att avgöra om en inkräktare har gått in i ett hemligt galleri med värdesaker. IoT-sensorerna skickar ständigt data som mäter blandningen av syre (O2) och koldioxid (CO2) i luften. De rapporterar också temperatur och relativ luftfuktighet. Vart och ett av dessa värden varierar något hela tiden. Men när en person kommer in i rummet, förändringen lite mer, och alltid i samma riktning: Syre minskar, Koldioxid ökar, temperatur ökar, liksom relativ luftfuktighet. När sensorerna kombineras för att visa ökningar utlöses inbrottslarmet.
I den här självstudien kör du programmet, mäter minnesallokeringar och förbättrar sedan prestandan genom att minska antalet allokeringar. Källkoden är tillgänglig i exempelwebbläsaren.
Utforska startprogrammet
Ladda ned programmet och kör startexemplet. Startprogrammet fungerar korrekt, men eftersom det allokerar många små objekt med varje mätcykel försämras dess prestanda långsamt när det körs över tid.
Press <return> to start simulation
Debounced measurements:
Temp: 67.332
Humidity: 41.077%
Oxygen: 21.097%
CO2 (ppm): 404.906
Average measurements:
Temp: 67.332
Humidity: 41.077%
Oxygen: 21.097%
CO2 (ppm): 404.906
Debounced measurements:
Temp: 67.349
Humidity: 46.605%
Oxygen: 20.998%
CO2 (ppm): 408.707
Average measurements:
Temp: 67.349
Humidity: 46.605%
Oxygen: 20.998%
CO2 (ppm): 408.707
Många rader har tagits bort.
Debounced measurements:
Temp: 67.597
Humidity: 46.543%
Oxygen: 19.021%
CO2 (ppm): 429.149
Average measurements:
Temp: 67.568
Humidity: 45.684%
Oxygen: 19.631%
CO2 (ppm): 423.498
Current intruders: 3
Calculated intruder risk: High
Debounced measurements:
Temp: 67.602
Humidity: 46.835%
Oxygen: 19.003%
CO2 (ppm): 429.393
Average measurements:
Temp: 67.568
Humidity: 45.684%
Oxygen: 19.631%
CO2 (ppm): 423.498
Current intruders: 3
Calculated intruder risk: High
Du kan utforska koden för att lära dig hur programmet fungerar. Huvudprogrammet kör simuleringen. När du trycker <Enter>
skapar det ett rum och samlar in några inledande baslinjedata:
Console.WriteLine("Press <return> to start simulation");
Console.ReadLine();
var room = new Room("gallery");
var r = new Random();
int counter = 0;
room.TakeMeasurements(
m =>
{
Console.WriteLine(room.Debounce);
Console.WriteLine(room.Average);
Console.WriteLine();
counter++;
return counter < 20000;
});
När baslinjedata har upprättats kör den simuleringen i rummet, där en slumptalsgenerator avgör om en inkräktare har kommit in i rummet:
counter = 0;
room.TakeMeasurements(
m =>
{
Console.WriteLine(room.Debounce);
Console.WriteLine(room.Average);
room.Intruders += (room.Intruders, r.Next(5)) switch
{
( > 0, 0) => -1,
( < 3, 1) => 1,
_ => 0
};
Console.WriteLine($"Current intruders: {room.Intruders}");
Console.WriteLine($"Calculated intruder risk: {room.RiskStatus}");
Console.WriteLine();
counter++;
return counter < 200000;
});
Andra typer innehåller mätningarna, en bounced mätning som är genomsnittet av de senaste 50 mätningarna och genomsnittet av alla mätningar som gjorts.
Kör sedan programmet med hjälp av .NET-objektallokeringsverktyget. Kontrollera att du använder bygget Release
, inte bygget Debug
. Öppna Prestandaprofilering på menyn Felsök. Kontrollera alternativet .NET-objektallokeringsspårning, men inget annat. Kör programmet till slutförande. Profileraren mäter objektallokeringar och rapporter om allokeringar och skräpinsamlingscykler. Du bör se ett diagram som liknar följande bild:
Föregående diagram visar att arbete för att minimera allokeringar ger prestandafördelar. Du ser ett sawtooth-mönster i diagrammet med levande objekt. Det talar om för dig att många objekt skapas som snabbt blir skräp. De samlas senare in, som visas i objektdeltagrafen. De nedåtgående röda staplarna anger en skräpinsamlingscykel.
Titta sedan på fliken Allokeringar under graferna. Den här tabellen visar vilka typer som är mest allokerade:
Typen System.String står för flest allokeringar. Den viktigaste uppgiften bör vara att minimera frekvensen för strängallokeringar. Det här programmet skriver ut många formaterade utdata till konsolen hela tiden. För den här simuleringen vill vi behålla meddelanden, så vi koncentrerar oss på nästa två rader: SensorMeasurement
typen och IntruderRisk
typen.
Dubbelklicka på raden SensorMeasurement
. Du kan se att alla allokeringar sker i static
metoden SensorMeasurement.TakeMeasurement
. Du kan se metoden i följande kodfragment:
public static SensorMeasurement TakeMeasurement(string room, int intruders)
{
return new SensorMeasurement
{
CO2 = (CO2Concentration + intruders * 10) + (20 * generator.NextDouble() - 10.0),
O2 = (O2Concentration - intruders * 0.01) + (0.005 * generator.NextDouble() - 0.0025),
Temperature = (TemperatureSetting + intruders * 0.05) + (0.5 * generator.NextDouble() - 0.25),
Humidity = (HumiditySetting + intruders * 0.005) + (0.20 * generator.NextDouble() - 0.10),
Room = room,
TimeRecorded = DateTime.Now
};
}
Varje mått allokerar ett nytt SensorMeasurement
objekt, vilket är en class
typ. Varje SensorMeasurement
skapad orsakar en heap-allokering.
Ändra klasser till structs
Följande kod visar den första deklarationen av SensorMeasurement
:
public class SensorMeasurement
{
private static readonly Random generator = new Random();
public static SensorMeasurement TakeMeasurement(string room, int intruders)
{
return new SensorMeasurement
{
CO2 = (CO2Concentration + intruders * 10) + (20 * generator.NextDouble() - 10.0),
O2 = (O2Concentration - intruders * 0.01) + (0.005 * generator.NextDouble() - 0.0025),
Temperature = (TemperatureSetting + intruders * 0.05) + (0.5 * generator.NextDouble() - 0.25),
Humidity = (HumiditySetting + intruders * 0.005) + (0.20 * generator.NextDouble() - 0.10),
Room = room,
TimeRecorded = DateTime.Now
};
}
private const double CO2Concentration = 409.8; // increases with people.
private const double O2Concentration = 0.2100; // decreases
private const double TemperatureSetting = 67.5; // increases
private const double HumiditySetting = 0.4500; // increases
public required double CO2 { get; init; }
public required double O2 { get; init; }
public required double Temperature { get; init; }
public required double Humidity { get; init; }
public required string Room { get; init; }
public required DateTime TimeRecorded { get; init; }
public override string ToString() => $"""
Room: {Room} at {TimeRecorded}:
Temp: {Temperature:F3}
Humidity: {Humidity:P3}
Oxygen: {O2:P3}
CO2 (ppm): {CO2:F3}
""";
}
Typen skapades ursprungligen som en class
eftersom den innehåller flera double
mått. Den är större än du vill kopiera i heta sökvägar. Detta beslut innebar dock ett stort antal allokeringar. Ändra typen från en class
till en struct
.
Om du ändrar från en class
till struct
introduceras några kompilatorfel eftersom den ursprungliga koden använde null
referenskontroller på några få punkter. Den första är i DebounceMeasurement
klassen i AddMeasurement
-metoden:
public void AddMeasurement(SensorMeasurement datum)
{
int index = totalMeasurements % debounceSize;
recentMeasurements[index] = datum;
totalMeasurements++;
double sumCO2 = 0;
double sumO2 = 0;
double sumTemp = 0;
double sumHumidity = 0;
for (int i = 0; i < debounceSize; i++)
{
if (recentMeasurements[i] is not null)
{
sumCO2 += recentMeasurements[i].CO2;
sumO2+= recentMeasurements[i].O2;
sumTemp+= recentMeasurements[i].Temperature;
sumHumidity += recentMeasurements[i].Humidity;
}
}
O2 = sumO2 / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
CO2 = sumCO2 / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
Temperature = sumTemp / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
Humidity = sumHumidity / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
}
Typen DebounceMeasurement
innehåller en matris med 50 mått. Avläsningarna för en sensor rapporteras som medelvärdet av de senaste 50 mätningarna. Det minskar bruset i avläsningarna. Innan hela 50 avläsningar har gjorts är null
dessa värden . Koden söker efter referens för null
att rapportera rätt genomsnitt vid systemstart. När du SensorMeasurement
har ändrat typen till en struct måste du använda ett annat test. Typen SensorMeasurement
innehåller en string
för rumsidentifieraren, så att du kan använda testet i stället:
if (recentMeasurements[i].Room is not null)
De andra tre kompilatorfelen finns i metoden som upprepade gånger utför mätningar i ett rum:
public void TakeMeasurements(Func<SensorMeasurement, bool> MeasurementHandler)
{
SensorMeasurement? measure = default;
do {
measure = SensorMeasurement.TakeMeasurement(Name, Intruders);
Average.AddMeasurement(measure);
Debounce.AddMeasurement(measure);
} while (MeasurementHandler(measure));
}
I startmetoden är den lokala variabeln för SensorMeasurement
en nullbar referens:
SensorMeasurement? measure = default;
Nu när SensorMeasurement
är en struct
i stället för en class
, är nullable en nullbar värdetyp. Du kan ändra deklarationen till en värdetyp för att åtgärda återstående kompilatorfel:
SensorMeasurement measure = default;
Nu när kompileringsfelen har åtgärdats bör du undersöka koden för att se till att semantiken inte har ändrats. Eftersom struct
typer skickas efter värde visas inte ändringar som gjorts i metodparametrar när metoden har returnerats.
Viktigt!
Om du ändrar en typ från en class
till en struct
kan du ändra programmets semantik. När en class
typ skickas till en metod görs alla mutationer som görs i metoden till argumentet. När en struct
typ skickas till en metod, och mutationer som görs i metoden görs till en kopia av argumentet. Det innebär att alla metoder som ändrar argumenten avsiktligt bör uppdateras för att använda ref
modifieraren för alla argumenttyper som du har ändrat från en class
till en struct
.
Typen SensorMeasurement
innehåller inga metoder som ändrar tillstånd, så det är inte ett problem i det här exemplet. Du kan bevisa det genom att lägga till readonly
modifieraren i struct:SensorMeasurement
public readonly struct SensorMeasurement
Kompilatorn framtvingar readonly
structens SensorMeasurement
natur. Om kontrollen av koden missade någon metod som ändrade tillstånd skulle kompilatorn berätta för dig. Appen byggs fortfarande utan fel, så den här typen är readonly
. readonly
Om du lägger till modifieraren när du ändrar en typ från en class
till en struct
kan du hitta medlemmar som ändrar tillståndet för struct
.
Undvik att göra kopior
Du har tagit bort ett stort antal onödiga allokeringar från din app. Typen SensorMeasurement
visas inte i tabellen någonstans.
Nu utför den extra arbete med att kopiera strukturen varje gång den SensorMeasurement
används som en parameter eller ett returvärde. Structen SensorMeasurement
innehåller fyra dubbelgångare, en DateTime och en string
. Den strukturen är mätbart större än en referens. Nu ska vi lägga till ref
modifierarna eller in
till platser där SensorMeasurement
typen används.
Nästa steg är att hitta metoder som returnerar en mätning eller ta en mätning som ett argument och använda referenser där det är möjligt. Starta i structen SensorMeasurement
. Den statiska TakeMeasurement
metoden skapar och returnerar en ny SensorMeasurement
:
public static SensorMeasurement TakeMeasurement(string room, int intruders)
{
return new SensorMeasurement
{
CO2 = (CO2Concentration + intruders * 10) + (20 * generator.NextDouble() - 10.0),
O2 = (O2Concentration - intruders * 0.01) + (0.005 * generator.NextDouble() - 0.0025),
Temperature = (TemperatureSetting + intruders * 0.05) + (0.5 * generator.NextDouble() - 0.25),
Humidity = (HumiditySetting + intruders * 0.005) + (0.20 * generator.NextDouble() - 0.10),
Room = room,
TimeRecorded = DateTime.Now
};
}
Vi lämnar den här som den är och returnerar efter värde. Om du försökte returnera med ref
får du ett kompilatorfel. Du kan inte returnera en ref
till en ny struktur som skapats lokalt i metoden. Utformningen av den oföränderliga structen innebär att du bara kan ange måttvärdena vid konstruktion. Den här metoden måste skapa en ny mått struct.
Låt oss titta igen på DebounceMeasurement.AddMeasurement
. Du bör lägga till in
modifieraren i parametern measurement
:
public void AddMeasurement(in SensorMeasurement datum)
{
int index = totalMeasurements % debounceSize;
recentMeasurements[index] = datum;
totalMeasurements++;
double sumCO2 = 0;
double sumO2 = 0;
double sumTemp = 0;
double sumHumidity = 0;
for (int i = 0; i < debounceSize; i++)
{
if (recentMeasurements[i].Room is not null)
{
sumCO2 += recentMeasurements[i].CO2;
sumO2+= recentMeasurements[i].O2;
sumTemp+= recentMeasurements[i].Temperature;
sumHumidity += recentMeasurements[i].Humidity;
}
}
O2 = sumO2 / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
CO2 = sumCO2 / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
Temperature = sumTemp / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
Humidity = sumHumidity / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
}
Det sparar en kopieringsåtgärd. Parametern in
är en referens till kopian som redan skapats av anroparen. Du kan också spara en kopia med TakeMeasurement
metoden i Room
typen . Den här metoden illustrerar hur kompilatorn ger säkerhet när du skickar argument av ref
. Den första TakeMeasurement
metoden i Room
typen tar argumentet Func<SensorMeasurement, bool>
. Om du försöker lägga in
till eller ref
modifieraren i den deklarationen rapporterar kompilatorn ett fel. Du kan inte skicka ett ref
argument till ett lambda-uttryck. Kompilatorn kan inte garantera att det anropade uttrycket inte kopierar referensen. Om lambda-uttrycket avbildar referensen kan referensen ha en livslängd som är längre än det värde som den refererar till. Åtkomst till den utanför referensens säkra kontext skulle leda till minnesskada. Säkerhetsreglerna ref
tillåter det inte. Du kan läsa mer i översikten över referenssäkerhetsfunktioner.
Bevara semantik
De sista uppsättningarna med ändringar har ingen större inverkan på programmets prestanda eftersom typerna inte skapas i heta sökvägar. Dessa ändringar illustrerar några av de andra tekniker som du använder i prestandajusteringen. Nu ska vi ta en titt på den inledande Room
klassen:
public class Room
{
public AverageMeasurement Average { get; } = new ();
public DebounceMeasurement Debounce { get; } = new ();
public string Name { get; }
public IntruderRisk RiskStatus
{
get
{
var CO2Variance = (Debounce.CO2 - Average.CO2) > 10.0 / 4;
var O2Variance = (Average.O2 - Debounce.O2) > 0.005 / 4.0;
var TempVariance = (Debounce.Temperature - Average.Temperature) > 0.05 / 4.0;
var HumidityVariance = (Debounce.Humidity - Average.Humidity) > 0.20 / 4;
IntruderRisk risk = IntruderRisk.None;
if (CO2Variance) { risk++; }
if (O2Variance) { risk++; }
if (TempVariance) { risk++; }
if (HumidityVariance) { risk++; }
return risk;
}
}
public int Intruders { get; set; }
public Room(string name)
{
Name = name;
}
public void TakeMeasurements(Func<SensorMeasurement, bool> MeasurementHandler)
{
SensorMeasurement? measure = default;
do {
measure = SensorMeasurement.TakeMeasurement(Name, Intruders);
Average.AddMeasurement(measure);
Debounce.AddMeasurement(measure);
} while (MeasurementHandler(measure));
}
}
Den här typen innehåller flera egenskaper. Vissa är class
typer. Att skapa ett Room
objekt omfattar flera allokeringar. En för Room
sig själv och en för var och en av medlemmarna av en class
typ som den innehåller. Du kan konvertera två av dessa egenskaper från class
typer till struct
typer: typerna DebounceMeasurement
och AverageMeasurement
. Nu ska vi gå igenom den omvandlingen med båda typerna.
DebounceMeasurement
Ändra typen från en class
till struct
. Det introducerar ett kompilatorfel CS8983: A 'struct' with field initializers must include an explicitly declared constructor
. Du kan åtgärda detta genom att lägga till en tom parameterlös konstruktor:
public DebounceMeasurement() { }
Du kan läsa mer om det här kravet i språkreferensartikeln om structs.
Åsidosättningen Object.ToString() ändrar inte något av värdena för structen. Du kan lägga till modifieraren i readonly
metoddeklarationen. Typen DebounceMeasurement
är föränderlig, så du måste vara noga med att ändringar inte påverkar kopior som ignoreras. Metoden AddMeasurement
ändrar objektets tillstånd. Den anropas från Room
klassen i TakeMeasurements
-metoden. Du vill att ändringarna ska sparas efter att du har anropat metoden. Du kan ändra Room.Debounce
egenskapen för att returnera en referens till en enda instans av DebounceMeasurement
typen:
private DebounceMeasurement debounce = new();
public ref readonly DebounceMeasurement Debounce { get { return ref debounce; } }
Det finns några ändringar i föregående exempel. För det första är egenskapen en skrivskyddad egenskap som returnerar en skrivskyddad referens till den instans som ägs av det här rummet. Det backas nu upp av ett deklarerat fält som initieras när Room
objektet instansieras. När du har gjort de här ändringarna uppdaterar du implementeringen av AddMeasurement
metoden. Det använder det privata bakgrundsfältet , debounce
inte readonly-egenskapen Debounce
. På så sätt sker ändringarna på den enskilda instans som skapades under initieringen.
Samma teknik fungerar med egenskapen Average
. Först ändrar AverageMeasurement
du typen från en class
till en struct
och lägger till readonly
modifieraren på ToString
metoden:
namespace IntruderAlert;
public struct AverageMeasurement
{
private double sumCO2 = 0;
private double sumO2 = 0;
private double sumTemperature = 0;
private double sumHumidity = 0;
private int totalMeasurements = 0;
public AverageMeasurement() { }
public readonly double CO2 => sumCO2 / totalMeasurements;
public readonly double O2 => sumO2 / totalMeasurements;
public readonly double Temperature => sumTemperature / totalMeasurements;
public readonly double Humidity => sumHumidity / totalMeasurements;
public void AddMeasurement(in SensorMeasurement datum)
{
totalMeasurements++;
sumCO2 += datum.CO2;
sumO2 += datum.O2;
sumTemperature += datum.Temperature;
sumHumidity+= datum.Humidity;
}
public readonly override string ToString() => $"""
Average measurements:
Temp: {Temperature:F3}
Humidity: {Humidity:P3}
Oxygen: {O2:P3}
CO2 (ppm): {CO2:F3}
""";
}
Sedan ändrar Room
du klassen efter samma teknik som du använde för egenskapen Debounce
. Egenskapen Average
returnerar ett readonly ref
till det privata fältet för genomsnittlig mätning. Metoden AddMeasurement
ändrar de interna fälten.
private AverageMeasurement average = new();
public ref readonly AverageMeasurement Average { get { return ref average; } }
Undvik boxning
Det finns en sista ändring för att förbättra prestanda. Huvudprogrammet är utskriftsstatistik för rummet, inklusive riskbedömningen:
Console.WriteLine($"Current intruders: {room.Intruders}");
Console.WriteLine($"Calculated intruder risk: {room.RiskStatus}");
Anropet till de genererade ToString
rutorna innehåller uppräkningsvärdet. Du kan undvika det genom att skriva en åsidosättning i Room
klassen som formaterar strängen baserat på värdet för den uppskattade risken:
public override string ToString() =>
$"Calculated intruder risk: {RiskStatus switch
{
IntruderRisk.None => "None",
IntruderRisk.Low => "Low",
IntruderRisk.Medium => "Medium",
IntruderRisk.High => "High",
IntruderRisk.Extreme => "Extreme",
_ => "Error!"
}}, Current intruders: {Intruders.ToString()}";
Ändra sedan koden i huvudprogrammet för att anropa den här nya ToString
metoden:
Console.WriteLine(room.ToString());
Kör appen med profileraren och titta på den uppdaterade tabellen för allokeringar.
Du har tagit bort många allokeringar och gett din app en prestandaökning.
Använda referenssäkerhet i ditt program
Dessa tekniker är prestandajustering på låg nivå. De kan öka prestandan i ditt program när de tillämpas på frekventa sökvägar och när du har mätt effekten före och efter ändringarna. I de flesta fall är cykeln du följer:
- Mät allokeringar: Bestäm vilka typer som tilldelas mest och när du kan minska heap-allokeringarna.
- Konvertera klassen till struct: Många gånger kan typer konverteras från en
class
till enstruct
. Din app använder stackutrymme i stället för att göra heap-allokeringar. - Bevara semantik: Om du konverterar en
class
till enstruct
kan semantiken påverkas för parametrar och returvärden. Alla metoder som ändrar dess parametrar bör nu markera dessa parametrar medref
modifieraren. Det säkerställer att ändringarna görs i rätt objekt. På samma sätt, om en egenskap eller ett metodreturvärde ska ändras av anroparen, bör den returenref
markeras med modifieraren. - Undvik kopior: När du skickar en stor struct som en parameter kan du markera parametern
in
med modifieraren. Du kan skicka en referens i färre byte och se till att metoden inte ändrar det ursprungliga värdet. Du kan också returnera värden genomreadonly ref
att returnera en referens som inte kan ändras.
Med hjälp av dessa tekniker kan du förbättra prestanda i heta sökvägar i koden.