Kurz: Omezení přidělení paměti s bezpečností ref
Ladění výkonu pro aplikaci .NET často zahrnuje dvě techniky. Nejprve zmenšete počet a velikost přidělení haldy. Za druhé zmenšete, jak často se data kopírují. Visual Studio poskytuje skvělé nástroje , které pomáhají analyzovat, jak vaše aplikace využívá paměť. Jakmile určíte, kde vaše aplikace provádí nepotřebné přidělení, provedete změny, abyste tyto přidělení minimalizovali. Typy převedete class
na struct
typy. Bezpečnostní funkce slouží ref
k zachování sémantiky a minimalizaci dodatečného kopírování.
V tomto kurzu využijte Sadu Visual Studio 17.5 pro co nejlepší prostředí. Nástroj pro přidělování objektů .NET používaný k analýze využití paměti je součástí sady Visual Studio. Ke spuštění aplikace a provedení všech změn můžete použít Visual Studio Code a příkazový řádek. Neuvidíte ale výsledky analýzy změn.
Aplikace, kterou použijete, je simulace aplikace IoT, která monitoruje několik senzorů a zjišťuje, jestli útočník vstoupil do tajné galerie s cennými informacemi. Senzory IoT neustále odesílají data, která měří kombinaci kyslíku (O2) a oxidu uhličitého (CO2) ve vzduchu. Také hlásí teplotu a relativní vlhkost. Každá z těchto hodnot neustále mírně kolísá. Nicméně, když osoba vstoupí do místnosti, změna trochu více a vždy ve stejném směru: Kyslík se sníží, oxidu uhličitého se zvýší, teplota se zvýší, stejně jako relativní vlhkost. Když senzory kombinují, aby ukazovaly nárůst, aktivuje se alarm vetřelce.
V tomto kurzu spustíte aplikaci, provedete měření přidělení paměti a pak zvýšíte výkon snížením počtu přidělení. Zdrojový kód je k dispozici v prohlížeči ukázek.
Prozkoumání úvodní aplikace
Stáhněte si aplikaci a spusťte úvodní ukázku. Počáteční aplikace funguje správně, ale protože přiděluje mnoho malých objektů s každým cyklem měření, její výkon se pomalu snižuje při běhu v čase.
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
Mnoho řádků bylo odebráno.
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
Kód můžete prozkoumat a zjistit, jak aplikace funguje. Hlavní program spouští simulaci. Po stisknutí <Enter>
se vytvoří místnost a shromáždí se počáteční základní data:
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;
});
Jakmile jsou tato data směrného plánu stanovena, spustí simulaci v místnosti, kde generátor náhodných čísel určí, jestli útočník vstoupil do místnosti:
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;
});
Jiné typy obsahují měření, debouncované měření, které je průměrem posledních 50 měření a průměrem všech provedených měření.
Dále spusťte aplikaci pomocí nástroje pro přidělování objektů .NET. Ujistěte se, že používáte Release
sestavení, ne Debug
sestavení. V nabídce Ladění otevřete profiler výkonu. Zkontrolujte možnost Sledování přidělování objektů .NET, ale nic jiného. Spusťte aplikaci, abyste ji mohli dokončit. Profiler měří přidělení objektů a sestavy o přiděleních a cyklech uvolňování paměti. Měl by se zobrazit graf podobný následujícímu obrázku:
Předchozí graf ukazuje, že práce na minimalizaci přidělení bude poskytovat výhody výkonu. V grafu živých objektů se zobrazí graf sawtooth. To vám říká, že se vytvořilo mnoho objektů, které se rychle stanou odpadky. Později se shromažďují, jak je znázorněno v rozdílovém grafu objektu. Červené pruhy dolů označují cyklus uvolňování paměti.
Dále se podívejte na kartu Přidělení pod grafy. Tato tabulka ukazuje, jaké typy jsou přiděleny nejvíce:
Typ System.String představuje nejvíce přidělení. Nejdůležitějším úkolem by mělo být minimalizace četnosti přidělování řetězců. Tato aplikace vypíše do konzoly neustále mnoho formátovaných výstupů. Pro tuto simulaci chceme uchovávat zprávy, takže se zaměříme na další dva řádky: typ SensorMeasurement
a IntruderRisk
typ.
Poklikejte na čáru SensorMeasurement
. Můžete vidět, že všechny přidělení probíhají v static
metodě SensorMeasurement.TakeMeasurement
. Metodu můžete zobrazit v následujícím fragmentu kódu:
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
};
}
Každé měření přidělí nový SensorMeasurement
objekt, což je class
typ. Každé SensorMeasurement
vytvoření způsobí přidělení haldy.
Změna tříd na struktury
Následující kód ukazuje počáteční deklaraci 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}
""";
}
Typ byl původně vytvořen jako typ class
, protože obsahuje řadu double
měření. Je větší, než byste chtěli kopírovat v horkých cestách. Toto rozhodnutí ale znamenalo velký počet přidělení. Změňte typ z a class
na .struct
Změna z class
hodnoty na struct
několik chyb kompilátoru, protože původní kód použil null
referenční kontroly na několika místech. První je ve DebounceMeasurement
třídě v AddMeasurement
metodě:
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);
}
Typ DebounceMeasurement
obsahuje pole 50 měření. Hodnoty pro senzor jsou hlášeny jako průměr posledních 50 měření. Tím se snižuje šum ve čtení. Před provedením úplných 50 čtení jsou null
tyto hodnoty . Kód vyhledá odkaz na null
hlášení správného průměru při spuštění systému. Po změně typu na SensorMeasurement
strukturu je nutné použít jiný test. Typ SensorMeasurement
obsahuje string
identifikátor místnosti, takže místo toho můžete použít tento test:
if (recentMeasurements[i].Room is not null)
Další tři chyby kompilátoru jsou všechny v metodě, která opakovaně provádí měření v místnosti:
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));
}
V počáteční metodě je místní proměnná pro odkaz s možnou SensorMeasurement
hodnotou null:
SensorMeasurement? measure = default;
Teď, když SensorMeasurement
je místo struct
class
, je nullable typ hodnoty null. Pokud chcete opravit zbývající chyby kompilátoru, můžete deklaraci změnit na typ hodnoty:
SensorMeasurement measure = default;
Teď, když byly vyřešeny chyby kompilátoru, byste měli prozkoumat kód a ujistit se, že se sémantika nezměnila. Vzhledem k tomu, že struct
typy jsou předány podle hodnoty, úpravy parametrů metody nejsou viditelné po vrácení metody.
Důležité
Změna typu z typu class
na typ struct
může změnit sémantiku programu. class
Pokud je typ předán metodě, všechny muty provedené v metodě jsou provedeny v argumentu. struct
Když je typ předán metodě a mutace vytvořené v metodě jsou vyrobeny do kopie argumentu. To znamená, že jakákoli metoda, která upraví argumenty podle návrhu, by se měla aktualizovat tak, aby používala ref
modifikátor u libovolného typu argumentu, který jste změnili z argumentu na class
.struct
Tento SensorMeasurement
typ neobsahuje žádné metody, které mění stav, takže to v této ukázce není problém. Můžete to prokázat přidáním readonly
modifikátoru SensorMeasurement
do struktury:
public readonly struct SensorMeasurement
Kompilátor vynucuje readonly
povahu SensorMeasurement
struktury. Pokud kontrola kódu vynechala nějakou metodu, která změnila stav, kompilátor vám to řekne. Vaše aplikace se stále vytváří bez chyb, takže tento typ je readonly
. Přidání modifikátoru readonly
při změně typu z objektu na class
typ struct
vám může pomoct najít členy, které upravují stav objektu struct
.
Vyhněte se vytváření kopií
Z aplikace jste odebrali velký počet nepotřebných přidělení. Typ SensorMeasurement
se v tabulce nikde nezobrazí.
Teď dělá další funkční kopírování SensorMeasurement
struktury pokaždé, když se používá jako parametr nebo návratová hodnota. Struktura SensorMeasurement
obsahuje čtyři dvojité, a DateTime a .string
Tato struktura je měřitelná větší než odkaz. Pojďme přidat ref
nebo in
modifikátory na místa, kde SensorMeasurement
se typ používá.
Dalším krokem je najít metody, které vrací měření, nebo jako argument vzít měření a tam, kde je to možné, použít odkazy. Začněte ve struktuře SensorMeasurement
. Statická TakeMeasurement
metoda vytvoří a vrátí novou 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
};
}
Tuto hodnotu necháme tak, jak je, a vrátíme ji hodnotou. Pokud jste se pokusili vrátit, ref
zobrazila by se chyba kompilátoru. V metodě nelze vrátit ref
novou strukturu vytvořenou místně. Návrh neměnné struktury znamená, že můžete nastavit pouze hodnoty měření při konstrukci. Tato metoda musí vytvořit novou strukturu měření.
Pojďme se znovu podívat na DebounceMeasurement.AddMeasurement
. Do parametru in
measurement
byste měli přidat modifikátor:
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);
}
Tím se uloží jedna operace kopírování. Parametr in
je odkaz na kopii, kterou již vytvořil volající. Kopii můžete uložit také s metodou TakeMeasurement
v typu Room
. Tato metoda ukazuje, jak kompilátor poskytuje bezpečnost při předávání argumentů .ref
Počáteční TakeMeasurement
metoda v Room
typu přebírá argument .Func<SensorMeasurement, bool>
Pokud se pokusíte přidat in
nebo ref
modifikátor do této deklarace, kompilátor hlásí chybu. Do výrazu lambda nelze předat ref
argument. Kompilátor nemůže zaručit, že volaný výraz nekopíroval odkaz. Pokud výraz lambda zachycuje odkaz, může mít odkaz životnost delší, než je hodnota, na kterou odkazuje. Přístup k němu mimo jeho bezpečný kontext ref by vedlo k poškození paměti. Bezpečnostní ref
pravidla to neumožňují. Další informace najdete v přehledu bezpečnostních funkcí ref.
Zachování sémantiky
Konečné sady změn nebudou mít velký vliv na výkon této aplikace, protože typy se nevytvořily v horkých cestách. Tyto změny ilustrují některé z dalších technik, které byste použili při ladění výkonu. Pojďme se podívat na počáteční Room
třídu:
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));
}
}
Tento typ obsahuje několik vlastností. Některé jsou class
typy. Vytvoření objektu Room
zahrnuje více přidělení. Jeden pro Room
sebe a jeden pro každý člen class
typu, který obsahuje. Dvě z těchto vlastností můžete převést z class
typů na struct
typy: typy DebounceMeasurement
a AverageMeasurement
typy. Pojďme si ji projít s oběma typy.
DebounceMeasurement
Změňte typ z class
do struct
. To představuje chybu CS8983: A 'struct' with field initializers must include an explicitly declared constructor
kompilátoru . Tento problém můžete vyřešit přidáním prázdného konstruktoru bez parametrů:
public DebounceMeasurement() { }
Další informace o tomto požadavku najdete v článku s referenčními informacemi o strukturách jazyka.
Přepsání Object.ToString() neupravuje žádné hodnoty struktury. Do deklarace této metody můžete přidat readonly
modifikátor. Typ DebounceMeasurement
je proměnlivý, takže budete muset zajistit, aby změny neovlivní kopie, které jsou zahozeny. Metoda AddMeasurement
upravuje stav objektu. Volá se z Room
třídy v TakeMeasurements
metodě. Chcete, aby tyto změny zůstaly po volání metody zachovány. Vlastnost můžete změnit Room.Debounce
tak, aby vracela odkaz na jednu instanci DebounceMeasurement
typu:
private DebounceMeasurement debounce = new();
public ref readonly DebounceMeasurement Debounce { get { return ref debounce; } }
V předchozím příkladu je několik změn. Nejprve je vlastnost jen pro čtení, která vrací jen pro čtení odkaz na instanci vlastněnou touto místností. Nyní je podporován deklarovaným polem, které se inicializuje při vytvoření instance objektu Room
. Po provedení těchto změn aktualizujete implementaci AddMeasurement
metody. Používá privátní backing pole , debounce
ne readonly vlastnost Debounce
. Tímto způsobem se změny provádí u jedné instance vytvořené během inicializace.
Stejná technika pracuje s Average
vlastností. Nejprve upravíte AverageMeasurement
typ z a class
do struct
a a přidáte readonly
modifikátor metody ToString
:
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}
""";
}
Potom upravíte Room
třídu podle stejné techniky, jakou jste použili Debounce
pro vlastnost. Vlastnost Average
vrátí readonly ref
hodnotu do soukromého pole pro průměrné měření. Metoda AddMeasurement
upraví interní pole.
private AverageMeasurement average = new();
public ref readonly AverageMeasurement Average { get { return ref average; } }
Vyhněte se balení
Existuje jedna konečná změna pro zlepšení výkonu. Hlavním programem je tisk statistik pro místnost, včetně posouzení rizik:
Console.WriteLine($"Current intruders: {room.Intruders}");
Console.WriteLine($"Calculated intruder risk: {room.RiskStatus}");
Volání vygenerovaných ToString
polí výčtu. Tomu se můžete vyhnout napsáním přepsání ve Room
třídě, která formátuje řetězec na základě hodnoty odhadovaného rizika:
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()}";
Pak upravte kód v hlavním programu tak, aby volal tuto novou ToString
metodu:
Console.WriteLine(room.ToString());
Spusťte aplikaci pomocí profileru a podívejte se na aktualizovanou tabulku pro přidělení.
Odebrali jste řadu přidělení a poskytli aplikaci zvýšení výkonu.
Použití bezpečnosti ref ve vaší aplikaci
Tyto techniky jsou ladění výkonu nízké úrovně. Při použití na horké cesty můžou zvýšit výkon vaší aplikace a kdy jste změřili dopad před a po změnách. Ve většině případů následuje cyklus:
- Přidělení měr: Určete, jaké typy se přidělují nejvíce a kdy můžete snížit přidělení haldy.
- Převést třídu na strukturu: Mnohokrát lze typy převést z typu
class
nastruct
. Vaše aplikace využívá místo přidělení haldy místo haldy. - Zachování sémantiky: Převedení na
class
sémantikustruct
může mít vliv na sémantiku parametrů a návratových hodnot. Každá metoda, která upravuje své parametry, by teď měla tyto parametry označit modifikátoremref
. Tím zajistíte, že se změny provede ve správném objektu. Podobně pokud by volající měl změnit návratovou hodnotu vlastnosti nebo metody, měla by být vrácena označena modifikátoremref
. - Vyhněte se kopiím: Když předáte velkou strukturu jako parametr, můžete parametr označit modifikátorem
in
. Odkaz můžete předat méně bajtů a zajistit, aby metoda neupravuje původní hodnotu. Můžete také vrátit hodnoty vrácenímreadonly ref
odkazu, který nelze upravit.
Pomocí těchto technik můžete zlepšit výkon v horkých cestách kódu.