Руководство по сокращению выделения памяти с помощью 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, но ничего другого. Запустите приложение до завершения. Профилировщик измеряет выделение объектов и отчеты о выделении и циклах сборки мусора. Вы увидите граф, аналогичный следующему изображению:
На предыдущем графике показано, что работа с сокращением распределения обеспечит преимущества производительности. В графе динамических объектов отображается шаблон sawtooth. Это говорит о том, что создаются многочисленные объекты, которые быстро становятся мусором. Позже они собираются, как показано на разностном графе объекта. Вниз красные полосы указывают цикл сборки мусора.
Затем перейдите на вкладку "Выделения" под графами. В этой таблице показано, какие типы выделяются чаще всего:
Тип System.String учитывает большинство выделений. Наиболее важной задачей является минимизация частоты выделения строк. Это приложение постоянно печатает многочисленные отформатированные выходные данные в консоли. Для этого моделирования мы хотим сохранить сообщения, поэтому мы сосредоточимся на следующих двух строках: SensorMeasurement
тип и IntruderRisk
тип.
Дважды щелкните SensorMeasurement
строку. Вы можете увидеть, что все выделения происходят в методе static
SensorMeasurement.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 class
struct
может изменить семантику программы. class
При передаче типа в метод все изменения, внесенные в метод, вносятся в аргумент. struct
Когда тип передается методу, и изменения, внесенные в метод, вносятся в копию аргумента. Это означает, что любой метод, изменяющий свои аргументы с помощью конструктора, следует обновить, чтобы использовать ref
модификатор для любого типа аргумента, который вы изменили с a class
struct
на .
Тип SensorMeasurement
не включает методы, которые изменяют состояние, поэтому это не проблема в этом примере. Вы можете доказать, что добавьте readonly
модификатор в структуру SensorMeasurement
:
public readonly struct SensorMeasurement
Компилятор применяет readonly
характер SensorMeasurement
структуры. Если проверка кода пропустила какой-то метод, который изменил состояние, компилятор сообщит вам. Ваше приложение по-прежнему выполняет сборку без ошибок, поэтому этот тип является readonly
. readonly
Добавление модификатора при изменении типа на элемент class
struct
управления может помочь найти элементы, изменяющие состояние 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
рассмотрим. Необходимо добавить модификатор в in
measurement
параметр:
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>
. Если вы попытаетесь добавить in
ref
модификатор в это объявление, компилятор сообщает об ошибке. Нельзя передать аргумент в 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
тип с типа class
struct
на . Это представляет ошибку 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
а не свойство Debounce
readonly. Таким образом, изменения происходят на одном экземпляре, созданном во время инициализации.
Тот же метод работает со свойством 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());
Запустите приложение с помощью профилировщика и просмотрите обновленную таблицу для выделения.
Вы удалили многочисленные выделения и предоставили приложению повышение производительности.
Использование безопасности ссылок в приложении
Эти методы являются низкоуровневой настройкой производительности. Они могут повысить производительность приложения при применении к горячим путям, а также при измерении влияния до и после изменений. В большинстве случаев цикл, который вы будете следовать:
- Распределение мер: определите, какие типы выделяются чаще всего, и когда можно уменьшить выделение кучи.
- Преобразование класса в структуру: во многих случаях типы можно преобразовать из типа
class
в структуруstruct
. Приложение использует пространство стека вместо выделения кучи. - Сохранение семантики: преобразование в нее
class
struct
может повлиять на семантику для параметров и возвращаемых значений. Теперь любой метод, изменяющий его параметры, должен пометить эти параметры модификаторомref
. Это гарантирует, что изменения вносятся в правильный объект. Аналогичным образом, если значение возвращаемого свойства или метода должно быть изменено вызывающей стороной, то возвращаемое значение должно быть отмечено модификаторомref
. - Избегайте копирования. При передаче большой структуры в качестве параметра можно пометить параметр модификатором
in
. Можно передать ссылку в меньшем количестве байтов и убедиться, что метод не изменяет исходное значение. Можно также возвращать значения,readonly ref
возвращая ссылку, которая не может быть изменена.
С помощью этих методов можно повысить производительность в горячих путях кода.