Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Руководство по сокращению выделения памяти с помощью
Часто настройка производительности для приложения .NET включает два метода. Во-первых, уменьшите количество и размер выделений памяти в куче. Во-вторых, уменьшите частоту копирования данных. Visual Studio предоставляет отличные средства , которые помогают анализировать использование памяти приложения. Когда вы определите, где приложение делает ненужные выделения, вы вносите изменения, чтобы свести к минимуму эти выделения. Вы преобразуете типы class
в типы struct
. Вы используете ref
функции безопасности для сохранения семантики и минимизации дополнительных копий.
Используйте Visual Studio 17.5 для получения наилучшего опыта с этим учебником. Средство выделения объектов .NET, используемое для анализа использования памяти, является частью Visual Studio. Вы можете использовать Visual Studio Code и командную строку для запуска приложения и внесения всех изменений. Однако вы не сможете просмотреть результаты анализа изменений.
Приложение, которое вы будете использовать, — это симуляция IoT-приложения, которое отслеживает несколько датчиков, чтобы определить, вошел ли злоумышленник в секретную галерею с ценными экспонатами. Датчики Интернета вещей постоянно отправляют данные, которые измеряют сочетание кислорода (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 , но ничего другого. Запустите приложение до завершения. Профилировщик измеряет выделение объектов и сообщает о выделениях и цикле сборки мусора. Вы увидите граф, аналогичный следующему изображению:
На предыдущем графике показано, что работа с сокращением распределения обеспечит преимущества производительности. В графике активных объектов отображается шаблон пилы. Это говорит о том, что создаются многочисленные объекты, которые быстро становятся мусором. Позже их собирают, как показано на графике дельта объекта. Вниз красные полосы указывают цикл сборки мусора.
Затем перейдите на вкладку "Выделения" под графами. В этой таблице показано, какие типы выделяются чаще всего:
Тип 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
измерения. Это больше, чем вы хотите скопировать в горячие пути. Однако это решение означает большое количество выделений. Измените тип с 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
, nullable является типом значений, допускающим null. Вы можете изменить объявление на тип значения, чтобы исправить оставшиеся ошибки компилятора:
SensorMeasurement measure = default;
Теперь, когда были устранены ошибки компилятора, необходимо проверить код, чтобы убедиться, что семантика не изменилась. Так как struct
типы передаются по значению, изменения, внесенные в параметры метода, не отображаются после возврата метода.
Это важно
Изменение типа с class
на struct
может изменить семантику вашей программы.
class
При передаче типа в метод все изменения, внесенные в метод, вносятся в аргумент. Когда struct
тип передается в метод, изменения, сделанные в методе, применяются к копии аргумента. Это означает, что любой метод, который по задумке изменяет свои аргументы, следует обновить для использования модификатора ref
для любого типа аргумента, который вы изменили с 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
только для чтения. Таким образом, изменения происходят на одном экземпляре, созданном во время инициализации.
Тот же метод работает со свойством Average
. Во-первых, вы измените AverageMeasurement
тип из class
на 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
возвращая ссылку, которая не может быть изменена.
С помощью этих методов можно повысить производительность в горячих путях кода.