Freigeben über


Tutorial: Reduzieren von Speicherzuordnungen mit ref-Sicherheit

Häufig umfasst die Leistungsoptimierung für eine .NET-Anwendung zwei Techniken. Erstens: Reduzieren von Anzahl und Größe der Heapzuordnungen. Reduzieren Sie zweitens, wie oft Daten kopiert werden. Visual Studio bietet großartige Tools , mit denen Sie analysieren können, wie Ihre Anwendung Arbeitsspeicher verwendet. Nachdem Sie festgestellt haben, wo Ihre App unnötige Zuordnungen vorschreibt, nehmen Sie Änderungen vor, um diese Zuordnungen zu minimieren. Sie konvertieren class Typen in struct Typen. Sie verwenden refSicherheitsfeatures , um Semantik zu erhalten und zusätzliches Kopieren zu minimieren.

Verwenden Sie Visual Studio 17.5 für eine optimale Erfahrung mit diesem Lernprogramm. Das .NET-Objektzuweisungstool, 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 Anwendung, die Sie verwenden werden, ist eine Simulation einer IoT-Anwendung, die mehrere Sensoren überwacht, um festzustellen, ob ein Eindringling eine geheime Galerie mit Wertgegenständen betreten hat. Die IoT-Sensoren senden ständig Daten, die den Mix aus Sauerstoff (O2) und Kohlendioxid (CO2) in der Luft messen. Sie melden auch die Temperatur und relative Feuchtigkeit. Jeder dieser Werte schwankt immer leicht. Wenn eine Person jedoch in den Raum eintritt, verändert sich die Situation etwas mehr und immer in die gleiche Richtung: Der Sauerstoffgehalt verringert sich, der Kohlendioxidgehalt erhöht sich, die Temperatur steigt, ebenso wie die relative Luftfeuchtigkeit. Wenn die Sensoren eine Zunahme zeigen, wird der Eindringlingsalarm ausgelöst.

In diesem Lernprogramm führen Sie die Anwendung aus, nehmen Messungen für Speicherzuweisungen vor und verbessern dann die Leistung, indem Sie die Anzahl der Zuordnungen verringern. 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äß, aber da sie viele kleine Objekte mit jedem Messzyklus zuordnet, verschlechtert sich die Leistung langsam, wenn sie im Laufe der Zeit ausgeführt wird.

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 wurden 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 zu erfahren, wie die Anwendung funktioniert. Das Hauptprogramm führt die Simulation aus. Wenn Sie <Enter> drücken, wird ein Raum erstellt und einige anfängliche Basisdaten gesammelt.

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 diese Basisdaten eingerichtet wurden, wird die Simulation im Raum ausgeführt, wobei ein Zufallszahlengenerator bestimmt, 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-Objektzuweisungstool aus. Stellen Sie sicher, dass Sie den Release Build verwenden, nicht den Debug Build. Öffnen Sie im Menü "Debuggen " den Performance Profiler. 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 Diagramm wie in der folgenden Abbildung angezeigt werden:

Zuordnungsdiagramm für die Ausführung der Eindringlingsbenachrichtigungs-App vor optimierungen.

Das vorherige Diagramm zeigt, dass die Bemühungen zur Reduzierung von Zuweisungen Leistungsvorteile bieten. Sie sehen ein Sägezahnmuster im Liveobjektdiagramm. Das sagt Ihnen, dass zahlreiche Objekte erstellt werden, die schnell zu Garbage werden. Sie werden später bereinigt, wie im Deltadiagramm für Objekte gezeigt. Die nach unten zeigenden roten Balken deuten auf einen Müllsammelzyklus hin.

Sehen Sie sich als Nächstes die Registerkarte " Zuordnungen " unter den Diagrammen an. In dieser Tabelle wird gezeigt, welche Typen am häufigsten zugewiesen werden:

Diagramm, das zeigt, welche Typen am häufigsten zugewiesen werden.

Der System.String Typ wird für die meisten Zuordnungen verwendet. Die wichtigste Aufgabe sollte es sein, die Häufigkeit von Speicherzuweisungen für Zeichenfolgen zu minimieren. Diese Anwendung gibt laufend zahlreiche formatierte Ausgaben in der Konsole aus. Für diese Simulation möchten wir Nachrichten beibehalten, daher konzentrieren wir uns auf die nächsten beiden Zeilen: 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.TakeMeasurementstattfinden. Die Methode wird im folgenden Codeausschnitt angezeigt:

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

Jedes Maß weist ein neues SensorMeasurement Objekt zu, bei dem es sich um einen class Typ handelt. Jede erstellte SensorMeasurement-Instanz verursacht eine Heapzuordnung.

Ändern von Klassen zu Strukturen

Der folgende Code zeigt 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 ein class erstellt, da er zahlreiche double Messungen enthält. Dieser ist größer, als Sie in langsamsten Pfaden kopieren wollten. Diese Entscheidung bedeutete jedoch eine große Anzahl von Zuteilungen. Ändern Sie den Typ von a class in ein struct.

Beim Wechsel von einem class zu struct treten einige Compilerfehler auf, da der ursprüngliche Code an einigen Stellen null Referenzüberprüfungen verwendet hat. Der 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 von 50 Maßen. 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 Messungen durchgeführt wurden, sind diese Werte null. Der Code führt eine null-Verweisprüfung durch, um beim Systemstart den richtigen Mittelwert zu melden. Nachdem Sie den SensorMeasurement Typ in eine Struktur geändert haben, müssen Sie einen anderen Test verwenden. Der SensorMeasurement Typ enthält einen string für den Raumbezeichner, sodass Sie stattdessen diesen Test verwenden können:

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

Die anderen drei Compilerfehler befinden sich alle in der Methode, 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));
}

Bei der Startmethode ist die lokale Variable für die SensorMeasurement eine nullable Referenz:

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 sich die Semantik nicht geändert hat. Da struct Typen nach Wert übergeben werden, sind Änderungen, die an Methodenparametern vorgenommen werden, nach der Rückkehr der Methode nicht mehr sichtbar.

Von Bedeutung

Das Ändern eines Typs von einem class in eine struct kann die Semantik Ihres Programms ändern. Wenn ein class Typ an eine Methode übergeben wird, werden alle in der Methode vorgenommenen Mutationen an das Argument vorgenommen. Wenn ein struct-Typ an eine Methode übergeben wird, werden alle in der Methode vorgenommenen Änderungen für eine Kopie des Arguments durchgeführt. Dies bedeutet, dass jede Methode, die ihre Argumente nach Entwurf ändert, aktualisiert werden sollte, um den ref Modifizierer für jeden Argumenttyp zu verwenden, den Sie von einer class zu einer structgeändert haben.

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

public readonly struct SensorMeasurement

Der Compiler erzwingt, dass die readonly-Struktur schreibgeschützt (SensorMeasurement) ist. Wenn Ihre Überprüfung des Codes einige Methoden übersehen haben sollte, die den Zustand geändert haben, würde der Compiler Ihnen das mitteilen. Ihre App erstellt weiterhin ohne Fehler, sodass dieser Typ lautet readonly. Das Hinzufügen des readonly Modifizierers, wenn Sie einen Typ von class zu struct ändern, kann Ihnen helfen, Mitglieder zu finden, die den Zustand von struct verändern.

Vermeiden des Erstellens von Kopien

Sie haben eine große Anzahl von unnötigen Zuordnungen aus Ihrer App entfernt. Der SensorMeasurement Typ wird nirgendwo in der Tabelle angezeigt.

Jetzt erfordert das Kopieren der SensorMeasurement Struktur zusätzliche Arbeit, jedes Mal wenn sie als Parameter oder Rückgabewert verwendet wird. Die SensorMeasurement Struktur enthält vier Doubles, a DateTime und a string. Diese Struktur ist deutlich größer als ein Verweis. Fügen wir die refin Modifizierer an Stellen hinzu, an denen der SensorMeasurement Typ verwendet wird.

Der nächste Schritt besteht darin, Methoden zu finden, die ein Maß zurückgeben oder ein Maß als Argument verwenden und soweit möglich Verweise verwenden. Beginnen Sie in der SensorMeasurement Struktur. Die statische TakeMeasurement Methode erstellt und gibt ein neues SensorMeasurementzurü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 versuchen, mittels ref zurückzugeben, erhalten Sie einen Compilerfehler. Eine ref-Rückgabe an eine neue Struktur, die lokal in der Methode erstellt wurde, ist nicht möglich. Die Konstruktion der unveränderlichen Struktur bedeutet, dass Sie nur die Werte der Messung im Bau festlegen können. Diese Methode muss eine neue Messstruktur erstellen.

Schauen wir uns DebounceMeasurement.AddMeasurement erneut an. Sie sollten dem Parameter den inmeasurement 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 eingespart. Der in Parameter ist ein Verweis auf die kopie, die bereits vom Aufrufer erstellt wurde. Sie können eine Kopie auch mit der TakeMeasurement Methode im Room Typ speichern. Diese Methode veranschaulicht, wie der Compiler Sicherheit bietet, wenn Sie Argumente über das System ref übergeben. Die erste TakeMeasurement Methode im Room Typ verwendet ein Argument von Func<SensorMeasurement, bool>. Wenn Sie versuchen, dieser Deklaration den inref Modifizierer hinzuzufügen, meldet der Compiler einen Fehler. Sie können ein ref Argument nicht an einen Lambda-Ausdruck übergeben. Der Compiler kann nicht garantieren, dass der aufgerufene Ausdruck den Verweis nicht kopiert. Wenn der Lambda-Ausdruck den Verweis erfasst, könnte der Verweis eine längere Lebensdauer haben als der Wert, auf den er verweist. Der Zugriff auf die Datei außerhalb des sicherheitssicheren Kontexts würde zu Speicherbeschädigungen führen. Die ref Sicherheitsregeln lassen sie nicht zu. Weitere Informationen finden Sie in der Übersicht über Sicherheitsfunktionen von Ref.

Semantik beibehalten

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 der anderen Techniken, die Sie in Ihrer Leistungsoptimierung verwenden sollten. Sehen wir uns die erste 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 umfasst 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: die DebounceMeasurement Typen und die AverageMeasurement Typen. Lassen Sie uns diese Transformation mit beiden Typen durcharbeiten.

Ändern Sie den DebounceMeasurement Typ von a class in struct. Dadurch wird ein Compilerfehler CS8983: A 'struct' with field initializers must include an explicitly declared constructoreingeführt. Sie können dies beheben, indem Sie einen leeren parameterlosen Konstruktor hinzufügen:

public DebounceMeasurement() { }

Weitere Informationen zu dieser Anforderung finden Sie im Sprachreferenzartikel zu Strukturen.

Die Object.ToString()-Überschreibung ändert keinen Wert der Struktur. Sie können dieser Methodendeklaration den readonly Modifizierer hinzufügen. Der DebounceMeasurement Typ ist änderbar, daher müssen Sie sicherstellen, dass Änderungen sich nicht auf Kopien auswirken, die verworfen werden. Die AddMeasurement Methode ändert den Status des Objekts. Sie wird aus der Room Klasse in der TakeMeasurements Methode aufgerufen. Sie möchten, dass diese Änderungen nach dem Aufrufen der Methode beibehalten werden. Sie können die Eigenschaft Room.Debounce ändern, sodass sie eine Referenz zu einer einzelnen Instanz des Typs zurückgibt.

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

Im vorherigen Beispiel gibt es einige Änderungen. Erstens ist die Eigenschaft eine schreibgeschützte Eigenschaft, die einen schreibgeschützten Verweis auf die zum jeweiligen Raum gehörende Instanz zurückgibt. Es wird jetzt von einem deklarierten Feld unterstützt, das initialisiert wird, wenn das Room Objekt instanziiert wird. Nachdem Sie diese Änderungen vorgenommen haben, aktualisieren Sie die Implementierung der AddMeasurement Methode. Sie verwendet das private Unterstützungsfeld, debounce, und nicht die schreibgeschützte Eigenschaft Debounce. Auf diese Weise erfolgen die Änderungen an der einzelnen Instanz, die während der Initialisierung erstellt wurde.

Die gleiche Technik funktioniert mit der Average Eigenschaft. Zuerst ändern Sie den AverageMeasurement-Typ von einem class in ein struct, und fügen den readonly-Modifizierer zur 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 nach demselben Verfahren, das 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; } }

Boxen vermeiden

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

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 Außerkraftsetzung in der Room Klasse schreiben, 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, um diese neue ToString Methode aufzurufen:

Console.WriteLine(room.ToString());

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

Zuordnungsdiagramm für die Ausführung der Eindringlingsbenachrichtigungs-App nach Änderungen.

Sie haben zahlreiche Zuordnungen entfernt und Ihre App hat einen Leistungsschub erhalten.

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 heiße Pfade angewendet wird, und wenn Sie die Auswirkung vor und nach den Änderungen gemessen haben. In den meisten Fällen lautet der folgende Zyklus:

  • Messen Sie Speicherzuweisungen: Bestimmen Sie, welche Speichertypen am häufigsten zugewiesen werden, und wann Sie die Heap-Speicherzuweisungen reduzieren können.
  • Klasse in Struktur konvertieren: Häufig können Typen von einem class in ein struct konvertiert werden. Ihre App verwendet Stapelplatz, anstatt Heap-Zuordnungen vorzunehmen.
  • Semantik beibehalten: Das Konvertieren einer class in eine struct kann sich auf die Semantik für Parameter und Rückgabewerte auswirken. Alle Methoden, die ihre Parameter ändern, sollten diese Parameter jetzt mit dem ref Modifizierer markieren. 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.
  • Vermeiden Sie Kopien: Wenn Sie eine große Struktur als Parameter übergeben, können Sie den Parameter mit dem in Modifizierer markieren. 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.

Mithilfe dieser Techniken können Sie die Leistung in heißen Pfaden ihres Codes verbessern.