Sdílet prostřednictvím


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:

Allocation graph for running the intruder alert app before any optimizations.

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:

Chart that shows which types are allocated most frequently.

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 nulltyto 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 SensorMeasurementhodnotou null:

SensorMeasurement? measure = default;

Teď, když SensorMeasurement je místo structclass, 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, refzobrazila 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 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 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 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á privátní backing pole , debouncene 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 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 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í.

Allocation graph for running the intruder alert app after modifications.

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 na struct. 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á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 vrácena 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 méně bajtů a zajistit, aby metoda neupravuje původní hodnotu. Můžete také vrátit hodnoty vrácením readonly ref odkazu, který nelze upravit.

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