Sdílet prostřednictvím


Návod: Snižte přidělování paměti bezpečným způsobem 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í ref slouží k zachování sémantiky a minimalizaci dodatečného kopírování.

V tomto návodu použijte Sadu Visual Studio 17.5 pro nejlepší zážitek. 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, dochází ke změně, a to vždy stejným směrem: sníží se množství kyslíku, zvýší se oxid uhličitý, teplota se zvýší, stejně jako relativní vlhkost. Když se senzory spojí a ukážou zvýšení, spustí 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 výchozí data stanovena, spustí simulaci v místnosti, kde generátor náhodných čísel určí, zda do místnosti vstoupil vetřelec.

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 hlásí přidělení a cykly uvolňování paměti. Měl by se zobrazit graf podobný následujícímu obrázku:

Graf alokace pro spuštění aplikace pro upozornění na narušitele před všemi optimalizacemi.

Předchozí graf ukazuje, že práce na minimalizaci přidělení bude přinášet výhody výkonu. Vidíte zubatý vzor v grafu živých objektů. 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 grafu změn objektu. Sestupné červené pruhy 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:

Graf znázorňující, které typy se přidělují nejčastěji

Typ System.String představuje nejvíce přidělení. Nejdůležitějším úkolem by měl být minimalizovat četnost 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ěňte třídy 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 často používaný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 na struct způsobí několik chyb kompilátoru, protože původní kód používal kontroly odkazů null 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 měření jsou tyto hodnoty null. Kód kontroluje referenci na null a oznamuje správný průměr 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 SensorMeasurementodkaz, který může mít hodnotu null:

SensorMeasurement? measure = default;

Teď, když je SensorMeasurement místo structclass, má nullable hodnotu typu nulová hodnota. 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 Když je typ předán metodě, všechny mutace provedené v metodě se aplikují na argument. struct Když je typ předán metodě, mutace provedené v metodě jsou aplikovány na kopii argumentu. To znamená, že jakákoli metoda, která modifikuje 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 ze class na 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 class na struct vám může pomoci najít členy, které mění stav 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á dodatečné kopírování struktury SensorMeasurement pokaždé, když je použita jako parametr nebo návratová hodnota. Struktura SensorMeasurement obsahuje čtyři double, DateTime a string. Tato struktura je měřitelně větší než referenční velikost. Přidejme ref nebo in modifikátory na místa, kde se typ SensorMeasurement 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 byste se pokusili vrátit pomocí ref, zobrazila by se chyba kompilátoru. Nelze vrátit ref strukturu lokálně vytvořenou v metodě. 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 inmeasurement 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 ušetří 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 zajišťuje bezpečnost při předávání argumentů pomocí ref. Počáteční TakeMeasurement metoda v typu Room přijímá 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 projít transformaci 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 constructorkompilá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á skryté pole debounce, nikoli vlastnost readonly 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 structa 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 boxu

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ého ToString uzavírá hodnotu výčtu do typu object. 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í.

Graf přidělení pro spuštění aplikace pro upozornění na neoprávněný vstup po provedených úpravách

Odebrali jste řadu přidělení a poskytli aplikaci zvýšení výkonu.

Použití bezpečnosti referencí ve vaší aplikaci

Tyto techniky jsou ladění výkonu na nízké úrovni. Pokud je použijete na kritické úseky a pokud jste změřili dopad před a po změnách, mohou zvýšit výkon vaší aplikace. 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 na struct. Vaše aplikace používá zásobník místo alokací na haldě.
  • Zachování sémantiky: Převedení class na struct může ovlivnit sémantiku parametrů a návratových hodnot. Každá metoda, která upravuje své parametry, by teď měla tyto parametry označit modifikátorem ref . 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 tato hodnota označena modifikátorem ref.
  • 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 s menším množstvím bajtů a zajistit, aby metoda neopravovala původní hodnotu. Hodnoty můžete také vrátit pomocí readonly ref, aby se vrátila reference, kterou nelze upravit.

Pomocí těchto technik můžete zlepšit výkon v horkých cestách kódu.