Tutorial: Reduzieren von Speicherzuordnungen mit ref-Sicherheit

Häufig beinhaltet die Leistungsoptimierung für eine .NET-Anwendung zwei Techniken. Erstens: Reduzieren von Anzahl und Größe der Heapzuordnungen. Zweitens: Reduzieren der Anzahl von Datenkopiervorgängen. Visual Studio bietet großartige Tools, mit denen Sie analysieren können, wie Ihre Anwendung den Arbeitsspeicher verwendet. Nachdem Sie ermittelt haben, wo Ihre App unnötige Zuordnungen vornimmt, können Sie Änderungen vornehmen, um diese Zuordnungen zu minimieren. Sie konvertieren class-Typen in struct-Typen. Sie verwenden ref-Sicherheitsfeatures, um die Semantik beizubehalten und zusätzliche Kopiervorgänge zu minimieren.

Verwenden Sie Visual Studio 17.5, um optimalen Nutzen aus diesem Tutorial zu erzielen. Das .NET-Tool für die Objektzuordnung, das zum Analysieren der Speicherauslastung verwendet wird, ist Teil von Visual Studio. Sie können Visual Studio Code und die Befehlszeile verwenden, um die Anwendung auszuführen und alle Änderungen vorzunehmen. Sie können die Analyseergebnisse Ihrer Änderungen jedoch nicht sehen.

Die verwendete Anwendung ist eine Simulation einer IoT-Anwendung, die mehrere Sensoren überwacht, um festzustellen, ob ein Eindringling in eine geheime Galerie mit wertvollen Gegenständen gelangt ist. Die IoT-Sensoren senden ständig Daten, die den Mix von Sauerstoff (O2) und Kohlendioxid (CO2) in der Luft messen. Sie melden auch die Temperatur und die relative Luftfeuchtigkeit. Alle diese Werte schwanken ständig geringfügig. Wenn jedoch eine Person den Raum betritt, ist die Schwankung etwas größer und immer in die gleiche Richtung: Sauerstoff wird weniger, Kohlendioxid wird mehr, Temperatur sowie relative Luftfeuchtigkeit steigen. Wenn die Sensoren zusammen eine Zunahme anzeigen, wird die Einbruchmeldung ausgelöst.

In diesem Tutorial führen Sie die Anwendung aus, nehmen Messungen für Speicherzuordnungen vor und verbessern dann die Leistung, indem Sie die Anzahl der Zuordnungen reduzieren. Der Quellcode ist im Beispielbrowser verfügbar.

Erkunden der Startanwendung

Laden Sie die Anwendung herunter, und führen Sie das Startbeispiel aus. Die Startanwendung funktioniert ordnungsgemäß, da aber mit jedem Messzyklus viele kleine Objekte zugeordnet werden, sinkt die Leistung im Laufe der Zeit langsam.

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

Viele Zeilen entfernt.

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

Sie können den Code erkunden, um festzustellen, wie die Anwendung funktioniert. Das Hauptprogramm führt die Simulation aus. Nachdem Sie <Enter> gedrückt haben, wird ein Raum erstellt, und es werden erste Baselinedaten erfasst:

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;
    });

Nachdem die Baselinedaten erstellt wurden, wird die Simulation für den Raum ausgeführt. Dabei ermittelt ein Zufallszahlengenerator, ob ein Eindringling den Raum betreten hat:

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;
    });

Andere Typen enthalten die Messungen: ein gleitender mittlerer Messwert, der den Mittelwert der letzten 50 Messungen darstellt, und den Mittelwert aller durchgeführten Messungen.

Führen Sie als Nächstes die Anwendung mit dem .NET-Tool für die Objektzuordnung aus. Achten Sie darauf, dass Sie den Release-Build und nicht den Debug-Build verwenden. Öffnen Sie im Menü Debuggen den Leistungsprofiler. Aktivieren Sie die Option Nachverfolgung der .NET-Objektzuordnung, aber keine andere Option. Führen Sie Ihre Anwendung bis zum Abschluss aus. Der Profiler misst Objektzuordnungen und meldet Zuordnungen und Garbage Collection-Zyklen. Es sollte ein ähnliches Diagramm wie in der folgenden Abbildung angezeigt werden:

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

Das obige Diagramm zeigt, dass die Minimierung von Zuordnungen Leistungsvorteile bringt. Im Live-Diagramm für Objekte wird ein Sägezahnmuster angezeigt. Dies weist darauf hin, dass zahlreiche Objekte erstellt werden, die schnell veraltet sind. Sie werden später bereinigt, wie im Deltadiagramm für Objekte gezeigt. Die roten Balken nach unten weisen auf einen Garbage Collection-Zyklus hin.

Sehen Sie sich als Nächstes die Registerkarte Zuordnungen unter den Diagrammen an. Diese Tabelle zeigt, welche Typen am häufigsten zugeordnet werden:

Chart that shows which types are allocated most frequently.

Der System.String-Typ weist die meisten Zuordnungen auf. Die wichtigste Aufgabe besteht darin, die Häufigkeit von Zeichenfolgenzuordnungen zu minimieren. Diese Anwendung gibt laufend zahlreiche formatierte Ausgaben in der Konsole aus. Für diese Simulation möchten wir die Meldungen beibehalten, sodass wir uns auf die nächsten beiden Zeilen konzentrieren: den SensorMeasurement-Typ und den IntruderRisk-Typ.

Doppelklicken Sie auf die SensorMeasurement-Zeile. Sie können sehen, dass alle Zuordnungen in der static-Methode SensorMeasurement.TakeMeasurement erfolgen. Der folgende Codeschnipsel zeigt die Methode:

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
    };
}

Jede Messung ordnet ein neues SensorMeasurement-Objekt zu, bei dem es sich um einen class-Typ handelt. Jede erstellte SensorMeasurement-Instanz verursacht eine Heapzuordnung.

Ändern von Klassen in Strukturen

Der folgende Code veranschaulicht die anfängliche Deklaration von 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}
            """;
}

Der Typ wurde ursprünglich als class erstellt, da er zahlreiche double-Messwerte enthält. Dieser ist größer, als Sie in langsamsten Pfaden kopieren wollten. Diese Entscheidung bedeutete jedoch eine Vielzahl von Zuordnungen. Ändern Sie den Typ von class in struct.

Die Änderung von class in struct führt zu einigen Compilerfehlern, da an einigen Stellen im ursprünglichen Code null-Referenzüberprüfungen verwendet wurden. Die erste befindet sich in der DebounceMeasurement-Klasse in der AddMeasurement-Methode:

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);
}

Der DebounceMeasurement-Typ enthält ein Array mit 50 Messungen. Die Messwerte für einen Sensor werden als Mittelwert der letzten 50 Messungen gemeldet. Dadurch wird das Rauschen in den Messwerten reduziert. Bevor alle 50 Messwerte erfasst wurden, sind diese Werte null. Der Code führt eine null-Verweisprüfung durch, um beim Systemstart den richtigen Mittelwert zu melden. Nachdem der SensorMeasurement-Typ in eine Struktur geändert wurde, müssen Sie eine andere Prüfung verwenden. Der SensorMeasurement-Typ enthält eine string-Zeichenfolge für den Raumbezeichner, sodass Sie stattdessen diese Prüfung verwenden können:

if (recentMeasurements[i].Room is not null)

Die anderen drei Compilerfehler sind alle in der Methode vorhanden, die wiederholt Messungen in einem Raum ausführt:

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));
}

In der Startmethode ist die lokale Variable für SensorMeasurement ein Nullable-Verweis:

SensorMeasurement? measure = default;

Da SensorMeasurement ein struct- und kein class-Typ ist, ist jetzt ein Nullable-Werttyp vorhanden. Sie können die Deklaration in einen Werttyp ändern, um die verbleibenden Compilerfehler zu beheben:

SensorMeasurement measure = default;

Nachdem die Compilerfehler behoben wurden, sollten Sie den Code untersuchen, um sicherzustellen, dass die Semantik nicht geändert wurde. Da struct-Typen als Wert übergeben werden, sind Änderungen an Methodenparametern nach der Rückkehr von der Methode nicht sichtbar.

Wichtig

Das Ändern eines Typs von class in struct kann die Semantik des Programms ändern. Wenn ein class-Typ an eine Methode übergeben wird, werden alle in der Methode vorgenommenen Änderungen für das Argument durchgeführt. Wenn ein struct-Typ an eine Methode übergeben wird, werden alle in der Methode vorgenommenen Änderungen für eine Kopie des Arguments durchgeführt. Das bedeutet, dass jede Methode, die Argumente entwurfsbedingt ändert, so aktualisiert werden muss, dass der ref-Modifizierer für alle Argumenttypen verwendet wird, die von class in struct geändert wurden.

Der SensorMeasurement-Typ enthält keine Methoden, die den Zustand ändern, sodass dies in diesem Beispiel kein Problem ist. Sie können dies nachweisen, indem Sie den readonly-Modifizierer zu SensorMeasurement-Struktur hinzufügen:

public readonly struct SensorMeasurement

Der Compiler erzwingt, dass die SensorMeasurement-Struktur schreibgeschützt (readonly) ist. Wenn bei der Überprüfung des Codes eine Methode übersehen wurde, die den Zustand geändert hat, werden Sie vom Compiler entsprechend informiert. Ihre App wird weiterhin ohne Fehler erstellt, sodass dieser Typ readonly ist. Das Hinzufügen des readonly-Modifizierers bei der Änderung eines Typs von class ein struct kann hilfreich sein, um Member zu finden, die den struct-Zustand ändern.

Vermeiden von Kopiervorgängen

Sie haben eine Vielzahl unnötiger Zuordnungen aus Ihrer App entfernt. Der SensorMeasurement-Typ ist in der Tabelle nicht mehr vorhanden.

Jetzt wird die SensorMeasurement-Struktur bei jeder Verwendung als Parameter oder Rückgabewert kopiert, sodass der Verarbeitungsaufwand steigt. Die SensorMeasurement-Struktur enthält vier double-Werte, einen DateTime-Wert und eine string-Zeichenfolge. Diese Struktur ist deutlich größer als ein Verweis. Fügen Sie jetzt den ref- oder in-Modifizierer an den Stellen hinzu, an denen der SensorMeasurement-Typ verwendet wird.

Der nächste Schritt besteht darin, Methoden zu finden, die eine Messung zurückgeben oder eine Messung als Argument verwenden und nach Möglichkeit Verweise verwenden. Beginnen Sie in der SensorMeasurement-Struktur. Die statische Methode TakeMeasurement erstellt ein neues SensorMeasurement-Objekt und gibt es zurück:

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
    };
}

Wir lassen sie unverändert, sodass weiterhin ein Wert zurückgegeben wird. Wenn Sie versucht hätten, einen Verweis (ref) zurückzugeben, wäre ein Compilerfehler aufgetreten. Eine ref-Rückgabe an eine neue Struktur, die lokal in der Methode erstellt wurde, ist nicht möglich. Der Entwurf der unveränderlichen Struktur bedeutet, dass Sie die Werte der Messung nur bei der Konstruktion festlegen können. Diese Methode muss eine neue Struktur für Messungen erstellen.

Sehen wir uns noch einmal DebounceMeasurement.AddMeasurement an. Sie sollten dem measurement-Parameter den in-Modifizierer hinzufügen:

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);
}

Dadurch wird ein Kopiervorgang gespart. Der in-Parameter ist ein Verweis auf die Kopie, die bereits vom Aufrufer erstellt wurde. Sie können einen Kopiervorgang auch mit der TakeMeasurement-Methode im Room-Typ einsparen. Diese Methode veranschaulicht, wie der Compiler Sicherheit bietet, wenn Sie Argumente als ref übergeben. Die anfängliche TakeMeasurement-Methode im Room-Typ übernimmt Func<SensorMeasurement, bool> als Argument. Wenn Sie versuchen, den in- oder ref-Modifizierer zu dieser Deklaration hinzuzufügen, meldet der Compiler einen Fehler. Sie können ein ref-Argument nicht an einen Lambdaausdruck übergeben. Der Compiler kann nicht garantieren, dass der aufgerufene Ausdruck den Verweis nicht kopiert. Wenn der Lambdaausdruck den Verweis erfasst, kann die Lebensdauer des Verweises länger sein als die des Werts, auf den er verweist. Ein Zugriff außerhalb des ref-safe-Kontexts würde zu einer Beschädigung des Arbeitsspeichers führen. Die ref-Sicherheitsregeln lassen dies nicht zu. Weitere Informationen finden Sie in der Übersicht zu ref-Sicherheitsfeatures.

Beibehalten der Semantik

Die letzten Änderungen haben keine großen Auswirkungen auf die Leistung dieser Anwendung, da die Typen nicht in den langsamsten Pfaden erstellt werden. Diese Änderungen veranschaulichen einige andere Techniken, die Sie bei der Leistungsoptimierung verwenden können. Sehen wir uns die anfängliche Room-Klasse an:

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));
    }
}

Dieser Typ enthält mehrere Eigenschaften. Einige sind class-Typen. Das Erstellen eines Room-Objekts beinhaltet mehrere Zuordnungen. Eine für Room selbst und ein für jeden Member eines enthaltenen class-Typs. Sie können zwei dieser Eigenschaften von class-Typen in struct-Typen konvertieren: DebounceMeasurement und AverageMeasurement. Lassen Sie uns die Transformation mit beiden Typen durcharbeiten.

Ändern Sie den DebounceMeasurement-Typ von class in struct. Dadurch tritt der Compilerfehler CS8983: A 'struct' with field initializers must include an explicitly declared constructor auf. Sie können diesen Fehler beheben, indem Sie einen leeren parameterlosen Konstruktor hinzufügen:

public DebounceMeasurement() { }

Weitere Informationen zu dieser Anforderung finden Sie im Artikel der Sprachreferenz zu Strukturen.

Die Object.ToString()-Überschreibung ändert keinen Wert der Struktur. Sie können den readonly-Modifizierer zu dieser Methodendeklaration hinzufügen. Der DebounceMeasurement-Typ ist änderbar, sodass Sie darauf achten müssen, dass Sich Änderungen nicht auf verworfene Kopien auswirken. Die AddMeasurement-Methode ändert den Zustand des Objekts. Sie wird von der Room-Klasse in der TakeMeasurements-Methode aufgerufen. Diese Änderungen sollen nach dem Aufruf der Methode beibehalten werden. Sie können die Room.Debounce-Eigenschaft ändern, um einen Verweis auf eine einzelne Instanz des DebounceMeasurement-Typs zurückzugeben:

private DebounceMeasurement debounce = new();
public ref readonly DebounceMeasurement Debounce { get { return ref debounce; } }

Im obigen Beispiel wurden einige Änderungen vorgenommen. Erstens ist die Eigenschaft eine schreibgeschützte Eigenschaft, die einen schreibgeschützten Verweis auf die zum jeweiligen Raum gehörende Instanz zurückgibt. Sie wird jetzt durch ein deklariertes Feld unterstützt, das beim Instanziieren des Room-Objekts initialisiert wird. Nachdem diese Änderungen vorgenommen wurden, aktualisieren Sie die Implementierung der AddMeasurement-Methode. Sie verwendet das private Unterstützungsfeld, debounce, und nicht die schreibgeschützte Eigenschaft Debounce. Auf diese Weise werden die Änderungen jeweils an der Instanz vorgenommen, die bei der Initialisierung erstellt wurde.

Die gleiche Technik funktioniert auch für die Average-Eigenschaft. Ändern Sie zunächst den AverageMeasurement-Typ von class in struct, und fügen Sie den readonly-Modifizierer für die ToString-Methode hinzu:

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}
        """;
}

Anschließend ändern Sie die Room-Klasse mit derselben Technik, die Sie für die Debounce-Eigenschaft verwendet haben. Die Average-Eigenschaft gibt ein readonly ref an das private Feld für den Mittelwert der Messungen zurück. Die AddMeasurement-Methode ändert die internen Felder.

private AverageMeasurement average = new();
public  ref readonly AverageMeasurement Average { get { return ref average; } }

Vermeiden von Boxing

Es gibt eine letzte Änderung, um die Leistung zu verbessern. Das Hauptprogramm gibt Statistiken für den Raum einschließlich Risikobewertung aus:

Console.WriteLine($"Current intruders: {room.Intruders}");
Console.WriteLine($"Calculated intruder risk: {room.RiskStatus}");

Der Aufruf von ToString bewirkt ein Boxing des Enumerationswerts. Sie können dies vermeiden, indem Sie eine Überschreibung in die Room-Klasse einfügen, die die Zeichenfolge basierend auf dem Wert des geschätzten Risikos formatiert:

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()}";

Ändern Sie dann den Code im Hauptprogramm, damit diese neue ToString-Methode aufgerufen wird:

Console.WriteLine(room.ToString());

Führen Sie die App mit dem Profiler aus, und schauen Sie sich die aktualisierte Tabelle für Zuordnungen an.

Allocation graph for running the intruder alert app after modifications.

Sie haben zahlreiche Zuordnungen entfernt und die Leistung der App erheblich gesteigert.

Verwenden der Ref-Sicherheit in Ihrer Anwendung

Bei diesen Techniken handelt es sich um eine Low-Level-Leistungsoptimierung. Sie können die Leistung in Ihrer Anwendung erhöhen, wenn sie auf die langsamsten Pfade angewendet werden und Sie die Auswirkungen vor und nach den Änderungen gemessen haben. In den meisten Fällen sieht der Zyklus wie folgt aus:

  • Zuordnungen messen: Bestimmen Sie, welche Typen am meisten zugeordnet werden, und wann Sie die Heapzuordnungen reduzieren können.
  • Klasse in Struktur konvertieren: Typen können oftmals von class in struct konvertiert werden. Ihre App verwendet Stapelspeicher, anstatt Heapzuordnungen vorzunehmen.
  • Semantik beibehalten: Das Konvertieren von class in struct kann sich auf die Semantik für Parameter und Rückgabewerte auswirken. Jede Methode, die ihre Parameter ändert, sollte diese Parameter jetzt mit dem ref-Modifizierer kennzeichnen. Dadurch wird sichergestellt, dass die Änderungen am richtigen Objekt vorgenommen werden. Wenn ein Eigenschafts- oder Methodenrückgabewert vom Aufrufer geändert werden soll, sollte die betreffende Rückgabe ebenfalls mit dem ref-Modifizierer gekennzeichnet werden.
  • Kopien vermeiden: Wenn Sie eine große Struktur als Parameter übergeben, können Sie den Parameter mit dem in-Modifizierer kennzeichnen. Sie können einen Verweis in weniger Bytes übergeben und sicherstellen, dass die Methode den ursprünglichen Wert nicht ändert. Sie können auch Werte als readonly ref zurückgeben, um einen Verweis zurückzugeben, der nicht geändert werden kann.

Mit diesen Techniken können Sie die Leistung in den langsamsten Pfaden Ihres Codes verbessern.