Руководство по сокращению выделения памяти с помощью ref безопасности

Часто настройка производительности для приложения .NET включает два метода. Во-первых, уменьшите количество и размер выделения кучи. Во-вторых, уменьшите частоту копирования данных. Visual Studio предоставляет отличные средства , которые помогают анализировать использование памяти приложения. Когда вы определите, где приложение делает ненужные выделения, вы вносите изменения, чтобы свести к минимуму эти выделения. Типы преобразуются class в struct типы. Вы используете ref функции безопасности для сохранения семантики и минимизации дополнительных копий.

Используйте Visual Studio 17.5 , чтобы лучше всего использовать этот учебник. Средство выделения объектов .NET, используемое для анализа использования памяти, является частью Visual Studio. Вы можете использовать Visual Studio Code и командную строку для запуска приложения и внесения всех изменений. Однако вы не сможете просмотреть результаты анализа изменений.

Используемое приложение — это моделирование приложения Интернета вещей, которое отслеживает несколько датчиков, чтобы определить, вошел ли злоумышленник в коллекцию секретов с ценными ценностями. Датчики Интернета вещей постоянно отправляют данные, которые измеряют сочетание кислорода (O2) и углекислого газа (CO2) в воздухе. Они также сообщают о температуре и относительной влажности. Каждое из этих значений изменяется немного все время. Однако, когда человек входит в комнату, изменение немного больше, и всегда в том же направлении: кислород уменьшается, углекислый газ увеличивается, температура увеличивается, как и относительная влажность. Когда датчики объединяются, чтобы показать увеличение, активируется тревога злоумышленника.

В этом руководстве вы запустите приложение, выполните измерения по выделению памяти, а затем улучшите производительность, уменьшая количество выделений. Исходный код доступен в браузере примеров.

Изучение начального приложения

Скачайте приложение и запустите начальный пример. Начальное приложение работает правильно, но так как оно выделяет множество небольших объектов с каждым циклом измерения, его производительность медленно снижается по мере выполнения с течением времени.

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

Многие строки удалены.

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

Вы можете изучить код, чтобы узнать, как работает приложение. Основная программа выполняет имитацию. После нажатия <Enter>клавиши он создает комнату и собирает некоторые начальные базовые данные:

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

После установки базовых данных он запускает имитацию в комнате, где генератор случайных чисел определяет, вошел ли злоумышленник в комнату:

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

Другие типы содержат измерения, разбалованное измерение, среднее из последних 50 измерений, а также среднее значение всех измерений.

Затем запустите приложение с помощью средства выделения объектов .NET. Убедитесь, что вы используете сборку Release , а не сборку Debug . В меню отладки откройте профилировщик производительности. Проверьте параметр отслеживания выделения объектов .NET, но ничего другого. Запустите приложение до завершения. Профилировщик измеряет выделение объектов и отчеты о выделении и циклах сборки мусора. Вы увидите граф, аналогичный следующему изображению:

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

На предыдущем графике показано, что работа с сокращением распределения обеспечит преимущества производительности. В графе динамических объектов отображается шаблон sawtooth. Это говорит о том, что создаются многочисленные объекты, которые быстро становятся мусором. Позже они собираются, как показано на разностном графе объекта. Вниз красные полосы указывают цикл сборки мусора.

Затем перейдите на вкладку "Выделения" под графами. В этой таблице показано, какие типы выделяются чаще всего:

Chart that shows which types are allocated most frequently.

Тип System.String учитывает большинство выделений. Наиболее важной задачей является минимизация частоты выделения строк. Это приложение постоянно печатает многочисленные отформатированные выходные данные в консоли. Для этого моделирования мы хотим сохранить сообщения, поэтому мы сосредоточимся на следующих двух строках: SensorMeasurement тип и IntruderRisk тип.

Дважды щелкните SensorMeasurement строку. Вы можете увидеть, что все выделения происходят в методе staticSensorMeasurement.TakeMeasurement. Этот метод можно увидеть в следующем фрагменте кода:

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

Каждое измерение выделяет новый SensorMeasurement объект, который является типом class . Каждое SensorMeasurement созданное вызывает выделение кучи.

Изменение классов на структуры

В следующем коде показано начальное объявление 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}
            """;
}

Тип изначально был создан как объект class , так как он содержит многочисленные double измерения. Это больше, чем вы хотите скопировать в горячие пути. Однако это решение означает большое количество выделений. Измените тип с a class на .struct

Переход с class точки на struct несколько ошибок компилятора, так как исходный код использовал null ссылочные проверка в нескольких местах. Первый — в DebounceMeasurement классе, в методе AddMeasurement :

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

Тип DebounceMeasurement содержит массив из 50 измерений. Значения для датчика отображаются в среднем за последние 50 измерений. Это снижает шум в чтениях. До того, как были приняты полные 50 чтений, эти значения являются null. Код проверка для ссылки, null чтобы сообщить правильное среднее значение при запуске системы. После изменения SensorMeasurement типа в структуру необходимо использовать другой тест. Тип SensorMeasurement содержит string идентификатор комнаты, поэтому вместо этого можно использовать этот тест:

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

Остальные три ошибки компилятора находятся в методе, который неоднократно принимает измерения в комнате:

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

В начальном методе локальная переменная для этой SensorMeasurement переменной является ссылкой , допускающей значение NULL:

SensorMeasurement? measure = default;

Теперь, когда объект SensorMeasurement является struct вместо classзначения NULL, является типом значений, допускающим значение NULL. Вы можете изменить объявление на тип значения, чтобы исправить оставшиеся ошибки компилятора:

SensorMeasurement measure = default;

Теперь, когда были устранены ошибки компилятора, необходимо проверить код, чтобы убедиться, что семантика не изменилась. Так как struct типы передаются по значению, изменения, внесенные в параметры метода, не отображаются после возврата метода.

Внимание

Изменение типа с a на a classstruct может изменить семантику программы. class При передаче типа в метод все изменения, внесенные в метод, вносятся в аргумент. struct Когда тип передается методу, и изменения, внесенные в метод, вносятся в копию аргумента. Это означает, что любой метод, изменяющий свои аргументы с помощью конструктора, следует обновить, чтобы использовать ref модификатор для любого типа аргумента, который вы изменили с a classstructна .

Тип SensorMeasurement не включает методы, которые изменяют состояние, поэтому это не проблема в этом примере. Вы можете доказать, что добавьте readonly модификатор в структуру SensorMeasurement :

public readonly struct SensorMeasurement

Компилятор применяет readonly характер SensorMeasurement структуры. Если проверка кода пропустила какой-то метод, который изменил состояние, компилятор сообщит вам. Ваше приложение по-прежнему выполняет сборку без ошибок, поэтому этот тип является readonly. readonly Добавление модификатора при изменении типа на элемент classstruct управления может помочь найти элементы, изменяющие состояние structобъекта.

Избегайте копирования

Вы удалили большое количество ненужных выделений из приложения. Тип SensorMeasurement не отображается в таблице нигде.

Теперь это делает дополнительную работу, копируя SensorMeasurement структуру каждый раз, когда она используется в качестве параметра или возвращаемого значения. Структуру SensorMeasurement содержит четыре двойника, a DateTime и a string. Эта структура измеримо превышает ссылку. Добавим ref модификаторы или in модификаторы в местах, где SensorMeasurement используется тип.

Следующий шаг — найти методы, возвращающие измерение, или принять измерение в качестве аргумента, и использовать ссылки, где это возможно. Запустите структуру SensorMeasurement . Статический TakeMeasurement метод создает и возвращает новое 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
    };
}

Мы оставим этот, как есть, возвращая по значению. Если вы попытались вернуться ref, вы получите ошибку компилятора. Невозможно вернуть ref новую структуру, созданную локально в методе. Конструкция неизменяемой структуры означает, что можно задать только значения измерения при построении. Этот метод должен создать новую структуру измерения.

Давайте еще раз DebounceMeasurement.AddMeasurementрассмотрим. Необходимо добавить модификатор в inmeasurement параметр:

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

Это сохраняет одну операцию копирования. Параметр in является ссылкой на копию, уже созданную вызывающим оператором. Вы также можете сохранить копию с TakeMeasurement помощью метода в типе Room . Этот метод иллюстрирует, как компилятор обеспечивает безопасность при передаче аргументов.ref Начальный TakeMeasurement метод в типе Room принимает аргумент Func<SensorMeasurement, bool>. Если вы попытаетесь добавить inref модификатор в это объявление, компилятор сообщает об ошибке. Нельзя передать аргумент в ref лямбда-выражение. Компилятор не может гарантировать, что вызываемое выражение не копирует ссылку. Если лямбда-выражение захватывает ссылку, ссылка может иметь время существования дольше, чем значение, к нему относится. Доступ к нему за пределами безопасного контекста ссылки приведет к повреждению памяти. Правила ref безопасности не позволяют ему. Дополнительные сведения см. в обзоре функций безопасности ссылок.

Сохранение семантики

Окончательные наборы изменений не влияют на производительность этого приложения, так как типы не создаются в горячих путях. Эти изменения иллюстрируют некоторые другие методы, которые вы использовали бы в настройке производительности. Рассмотрим начальный Room класс:

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

Этот тип содержит несколько свойств. Некоторые из них являются class типами. Room Создание объекта включает несколько выделений. Один для Room себя и один для каждого члена class типа, который он содержит. Вы можете преобразовать два из этих свойств из типов в class типы struct : DebounceMeasurement и AverageMeasurement типы. Давайте рассмотрим это преобразование с обоими типами.

Измените DebounceMeasurement тип с типа classstructна . Это представляет ошибку CS8983: A 'struct' with field initializers must include an explicitly declared constructorкомпилятора. Это можно исправить, добавив пустой конструктор без параметров:

public DebounceMeasurement() { }

Дополнительные сведения об этом требовании см. в справочной статье по языкам по структурым.

Переопределение Object.ToString() не изменяет ни одно из значений структуры. Модификатор можно добавить в readonly это объявление метода. Тип DebounceMeasurementизменяемый, поэтому вам потребуется заботиться о том, что изменения не влияют на копии карта. Метод AddMeasurement изменяет состояние объекта. Он вызывается из Room класса в методе TakeMeasurements . Эти изменения необходимо сохранить после вызова метода. Свойство можно изменитьRoom.Debounce, чтобы вернуть ссылку на один экземпляр DebounceMeasurement типа:

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

В предыдущем примере есть несколько изменений. Во-первых, свойство является свойством чтения, которое возвращает ссылку на экземпляр, принадлежащий этому помещению. Теперь она поддерживается объявленным полем, инициализируемым при создании экземпляра Room объекта. После внесения этих изменений вы обновите реализацию AddMeasurement метода. Он использует частное резервное поле, debounceа не свойство Debouncereadonly. Таким образом, изменения происходят на одном экземпляре, созданном во время инициализации.

Тот же метод работает со свойством Average . Во-первых, вы измените AverageMeasurement тип из a class в a structи добавьте readonly модификатор в 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}
        """;
}

Затем вы измените Room класс, следуя тому же методу, который использовался для Debounce свойства. Свойство Average возвращает readonly ref частное поле для среднего измерения. Метод AddMeasurement изменяет внутренние поля.

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

Избегайте бокса

Существует одно окончательное изменение для повышения производительности. Основная программа — печать статистики для комнаты, включая оценку рисков:

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

Вызов созданных ToString полей задает значение перечисления. Это можно избежать, написав переопределение в Room классе, который форматирует строку на основе значения предполагаемого риска:

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

Затем измените код в главной программе, чтобы вызвать этот новый ToString метод:

Console.WriteLine(room.ToString());

Запустите приложение с помощью профилировщика и просмотрите обновленную таблицу для выделения.

Allocation graph for running the intruder alert app after modifications.

Вы удалили многочисленные выделения и предоставили приложению повышение производительности.

Использование безопасности ссылок в приложении

Эти методы являются низкоуровневой настройкой производительности. Они могут повысить производительность приложения при применении к горячим путям, а также при измерении влияния до и после изменений. В большинстве случаев цикл, который вы будете следовать:

  • Распределение мер: определите, какие типы выделяются чаще всего, и когда можно уменьшить выделение кучи.
  • Преобразование класса в структуру: во многих случаях типы можно преобразовать из типа class в структуру struct. Приложение использует пространство стека вместо выделения кучи.
  • Сохранение семантики: преобразование в нее classstruct может повлиять на семантику для параметров и возвращаемых значений. Теперь любой метод, изменяющий его параметры, должен пометить эти параметры модификатором ref . Это гарантирует, что изменения вносятся в правильный объект. Аналогичным образом, если значение возвращаемого свойства или метода должно быть изменено вызывающей стороной, то возвращаемое значение должно быть отмечено модификатором ref .
  • Избегайте копирования. При передаче большой структуры в качестве параметра можно пометить параметр модификатором in . Можно передать ссылку в меньшем количестве байтов и убедиться, что метод не изменяет исходное значение. Можно также возвращать значения, readonly ref возвращая ссылку, которая не может быть изменена.

С помощью этих методов можно повысить производительность в горячих путях кода.