Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Tutorial: Reducir asignaciones de memoria de forma segura con
A menudo, el ajuste del rendimiento de una aplicación .NET implica dos técnicas. Primero, la reducción del número y el tamaño de las asignaciones del montón. En segundo lugar, reduzca la frecuencia con la que se copian los datos. Visual Studio proporciona excelentes herramientas que ayudan a analizar cómo usa la aplicación la memoria. Una vez que haya determinado dónde realiza la aplicación asignaciones innecesarias, realice cambios para minimizar esas asignaciones. Tú conviertes los tipos class
en tipos struct
. Usas ref
características de seguridad para conservar la semántica y minimizar copias adicionales.
Use Visual Studio 17.5 para obtener la mejor experiencia con este tutorial. La herramienta de asignación de objetos .NET que se usa para analizar el uso de memoria forma parte de Visual Studio. Puede usar Visual Studio Code y la línea de comandos para ejecutar la aplicación y realizar todos los cambios. Sin embargo, no podrá ver los resultados del análisis de los cambios.
La aplicación que usará es una simulación de una aplicación de IoT que supervisa varios sensores para determinar si un intruso ha introducido una galería secreta con objetos valiosos. Los sensores de IoT envían datos constantemente que miden la combinación de oxígeno (O2) y dióxido de carbono (CO2) en el aire. También notifican la temperatura y la humedad relativa. Cada uno de estos valores fluctúa ligeramente todo el tiempo. Sin embargo, cuando una persona entra en la habitación, cambia un poco más y siempre en la misma dirección: Oxígeno disminuye, Dióxido de carbono aumenta, aumenta la temperatura, como hace la humedad relativa. Cuando los sensores se combinan para mostrar aumentos, se desencadena la alarma del intruso.
En este tutorial, ejecutará la aplicación, tomará medidas sobre las asignaciones de memoria y, a continuación, mejorará el rendimiento reduciendo el número de asignaciones. El código fuente está disponible en el explorador de ejemplos.
Exploración de la aplicación de inicio
Descargue la aplicación y ejecute el ejemplo de inicio. La aplicación de inicio funciona correctamente, pero dado que asigna muchos objetos pequeños con cada ciclo de medición, su rendimiento se degrada lentamente a medida que se ejecuta con el tiempo.
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
Se han quitado muchas filas.
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
Puede explorar el código para obtener información sobre cómo funciona la aplicación. El programa principal ejecuta la simulación. Después de presionar <Enter>
, crea una sala y recopila algunos datos iniciales de línea base:
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;
});
Una vez que se hayan establecido los datos de línea base, ejecuta la simulación en la sala, donde un generador de números aleatorios determina si un intruso ha entrado en la sala:
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;
});
Otros tipos contienen las medidas, una medida sin rebotes, que es el promedio de las últimas 50 medidas, y el promedio de todas las medidas tomadas.
A continuación, ejecute la aplicación mediante la herramienta de asignación de objetos .NET. Asegúrese de que usa la Release
compilación, no la Debug
compilación. En el menú Depurar , abra el generador de perfiles de rendimiento. Active la opción Seguimiento de asignación de objetos de .NET , pero nada más. Ejecute la aplicación para completarla. El generador de perfiles mide las asignaciones de objetos e informa sobre las asignaciones y los ciclos de recolección de elementos no utilizados. Debería ver un gráfico similar a la siguiente imagen:
En el gráfico anterior se muestra que trabajar para minimizar las asignaciones proporcionará ventajas de rendimiento. Verá un patrón de sierra en el gráfico de objetos activos. Esto le indica que se crean numerosos objetos que rápidamente se convierten en basura. Más adelante se recopilan, como se muestra en el gráfico delta del objeto. Las barras rojas descendentes indican un ciclo de recolección de elementos no utilizados.
A continuación, examine la pestaña Asignaciones debajo de los gráficos. En esta tabla se muestran los tipos que son asignados más frecuentemente.
El System.String tipo representa la mayoría de las asignaciones. La tarea más importante debe ser minimizar la frecuencia de las asignaciones de cadenas. Esta aplicación imprime numerosas salidas con formato en la consola de forma constante. Para esta simulación, queremos mantener los mensajes, por lo que nos centraremos en las dos filas siguientes: el SensorMeasurement
tipo y el IntruderRisk
tipo.
Haga doble clic en la SensorMeasurement
línea. Puede ver que todas las asignaciones tienen lugar en el static
método SensorMeasurement.TakeMeasurement
. Puede ver el método en el siguiente fragmento de código:
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
};
}
Cada medida asigna un nuevo SensorMeasurement
objeto, que es un class
tipo. Cada SensorMeasurement
creado provoca una asignación de montón.
Convertir clases en estructuras
El código siguiente muestra la declaración inicial de 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}
""";
}
El tipo se creó originalmente como class
porque contiene numerosas double
mediciones. Es mayor de lo que desea copiar en rutas de acceso activas. Sin embargo, esa decisión significaba un gran número de asignaciones. Cambie el tipo de class
a struct
.
Al cambiar de class
a struct
se producen algunos errores del compilador porque el código original usó null
comprobaciones de referencia en varios puntos. La primera está en la clase DebounceMeasurement
, en el método 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);
}
El DebounceMeasurement
tipo contiene una matriz de 50 medidas. Las lecturas de un sensor se notifican como el promedio de las últimas 50 medidas. Esto reduce el ruido en las lecturas. Antes de que se realicen las 50 lecturas completas, estos valores son null
. El código comprueba la existencia de la referencia null
para informar del promedio correcto al iniciar el sistema. Después de cambiar el tipo SensorMeasurement
a una estructura, debe usar una prueba diferente. El SensorMeasurement
tipo incluye un string
para el identificador de sala, por lo que use esa prueba como alternativa.
if (recentMeasurements[i].Room is not null)
Los otros tres errores del compilador están todos en el método que toma repetidamente medidas en una sala:
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));
}
En el método starter, la variable local de SensorMeasurement
es una referencia que acepta valores NULL:
SensorMeasurement? measure = default;
Ahora que SensorMeasurement
es struct
en lugar de class
, el que admite un valor NULL es un tipo de valor que admite un valor NULL. Puede cambiar la declaración a un tipo de valor para corregir los errores restantes del compilador:
SensorMeasurement measure = default;
Ahora que se han solucionado los errores del compilador, debe examinar el código para asegurarse de que la semántica no ha cambiado. Dado que struct
los tipos se pasan por valor, las modificaciones realizadas en los parámetros del método no son visibles después de que el método devuelva.
Importante
Cambiar un tipo de a class
a struct
puede cambiar la semántica del programa. Cuando se pasa un tipo class
a un método, cualquier mutación que se realice dentro del método afectará al argumento. Cuando un struct
tipo se pasa a un método, las mutaciones realizadas en el método se efectúan sobre una copia del argumento. Esto significa que cualquier método que por diseño modifique sus argumentos debe actualizarse para usar el modificador ref
en cualquier tipo de argumento que haya cambiado de un class
a un struct
.
El SensorMeasurement
tipo no incluye ningún método que cambie el estado, por lo que no es un problema en este ejemplo. Puede demostrarlo agregando el readonly
modificador a la estructura SensorMeasurement
.
public readonly struct SensorMeasurement
El compilador aplica la naturaleza readonly
de struct SensorMeasurement
. Si la inspección del código perdió algún método que modificó el estado, el compilador le avisaría. La aplicación sigue compilando sin errores, por lo que este tipo es readonly
. Agregar el readonly
modificador al cambiar un tipo de class
a struct
puede ayudarle a encontrar miembros que modifiquen el estado del struct
.
Evitar la realización de copias
Ha quitado un gran número de asignaciones innecesarias de la aplicación. El SensorMeasurement
tipo no aparece en la tabla en ningún lugar.
Ahora, está haciendo un trabajo adicional copiando la SensorMeasurement
estructura cada vez que se usa como un parámetro o un valor devuelto. La SensorMeasurement
estructura contiene cuatro dobles, un DateTime y un string
. Esa estructura es notablemente más grande que una referencia. Vamos a agregar los modificadores ref
o in
en los lugares donde se usa el tipo SensorMeasurement
.
El siguiente paso consiste en buscar métodos que devuelven una medida o toman una medida como argumento y usan referencias siempre que sea posible. Comience en la estructura SensorMeasurement
. El método estático TakeMeasurement
crea y devuelve un nuevo 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
};
}
Lo dejaremos tal como está, devolviendo por valor. Si intentaras retornar por ref
, obtendrías un error del compilador. No se puede devolver un ref
elemento a una nueva estructura creada localmente en el método . El diseño de la estructura inmutable significa que solo se pueden establecer los valores de la medida en la construcción. Este método debe crear una nueva estructura de medida.
Echemos un vistazo de nuevo a DebounceMeasurement.AddMeasurement
. Debe agregar el in
modificador al measurement
parámetro :
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);
}
Eso ahorra una operación de copia. El in
parámetro es una referencia a la copia ya creada por el autor de la llamada. También puede guardar una copia con el método TakeMeasurement
en el tipo Room
. Este método muestra cómo el compilador proporciona seguridad al pasar argumentos por ref
. El método inicial TakeMeasurement
del Room
tipo toma un argumento de Func<SensorMeasurement, bool>
. Si intenta agregar el in
modificador o ref
a esa declaración, el compilador notifica un error. No se puede pasar un ref
argumento a una expresión lambda. El compilador no puede garantizar que la expresión llamada no copie la referencia. Si la expresión lambda captura la referencia, la referencia podría tener una duración mayor que el valor al que hace referencia. Acceder a él fuera de su contexto seguro ref produciría daños en la memoria. Las ref
reglas de seguridad no lo permiten. Puede obtener más información en la introducción a las características de seguridad ref.
Conservar la semántica
Los conjuntos finales de cambios no tendrán un impacto importante en el rendimiento de esta aplicación porque los tipos no se crean en rutas de acceso activas. Estos cambios muestran algunas de las otras técnicas que usaría en el ajuste del rendimiento. Echemos un vistazo a la clase inicial 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));
}
}
Este tipo contiene varias propiedades. Algunos son tipos class
. La creación de un Room
objeto implica varias asignaciones. Uno para el propio objeto Room
y otro para cada uno de los miembros de tipo class
que contiene. Puede convertir dos de estas propiedades de tipos class
a struct
: los tipos DebounceMeasurement
y AverageMeasurement
. Vamos a trabajar con esa transformación con ambos tipos.
Cambie el tipo DebounceMeasurement
de class
a struct
. Esto introduce un error CS8983: A 'struct' with field initializers must include an explicitly declared constructor
del compilador . Para corregirlo, agregue un constructor sin parámetros vacío:
public DebounceMeasurement() { }
Puede obtener más información sobre este requisito en el artículo de referencia del lenguaje sobre estructuras.
La invalidación Object.ToString() no modifica ninguno de los valores de la estructura. Puede agregar el readonly
modificador a esa declaración de método. El DebounceMeasurement
tipo es mutable, por lo que deberá tener cuidado de que las modificaciones no afecten a las copias que se descartan. El AddMeasurement
método modifica el estado del objeto . Se llama desde la clase Room
, en el método TakeMeasurements
. Quiere que esos cambios se conserven después de llamar al método. Puede cambiar la Room.Debounce
propiedad para devolver una referencia a una sola instancia del DebounceMeasurement
tipo :
private DebounceMeasurement debounce = new();
public ref readonly DebounceMeasurement Debounce { get { return ref debounce; } }
Hay algunos cambios en el ejemplo anterior. En primer lugar, la propiedad es una propiedad readonly que devuelve una referencia de solo lectura a la instancia de propiedad de esta sala. Ahora está respaldado por un campo declarado que se inicializa cuando se crea una instancia del Room
objeto. Después de realizar estos cambios, actualizará la implementación del método AddMeasurement
. Usa el campo de respaldo privado, debounce
, no la propiedad readonly Debounce
. De este modo, los cambios tienen lugar en la instancia única creada durante la inicialización.
La misma técnica funciona con la Average
propiedad . Primero, modifique el tipo AverageMeasurement
de class
a struct
y agregue el modificador readonly
en el método 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}
""";
}
A continuación, modifique la Room
clase siguiendo la misma técnica que usó para la Debounce
propiedad . La propiedad Average
devuelve un objeto readonly ref
al campo privado para la medición promedio. El AddMeasurement
método modifica los campos internos.
private AverageMeasurement average = new();
public ref readonly AverageMeasurement Average { get { return ref average; } }
Evasión de la conversión boxing
Hay un cambio final para mejorar el rendimiento. El programa principal es imprimir estadísticas para la sala, incluida la evaluación de riesgos:
Console.WriteLine($"Current intruders: {room.Intruders}");
Console.WriteLine($"Calculated intruder risk: {room.RiskStatus}");
La llamada a los cuadros ToString
generados establece el valor de enumeración. Puede evitarlo escribiendo una invalidación en la Room
clase que da formato a la cadena en función del valor de riesgo estimado:
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()}";
A continuación, modifique el código del programa principal para llamar a este nuevo ToString
método:
Console.WriteLine(room.ToString());
Ejecute la aplicación con el generador de perfiles y examine la tabla actualizada para las asignaciones.
Has eliminado numerosas asignaciones y mejorado el rendimiento de su aplicación.
Uso de la seguridad ref en la aplicación
Estas técnicas son de bajo nivel para la optimización del rendimiento. Pueden aumentar el rendimiento de su aplicación cuando se aplican a las rutas críticas y se ha medido el impacto tanto antes como después de los cambios. En la mayoría de los casos, el ciclo que seguirá es:
- Asignaciones de medida: determine qué tipos se asignan más y cuándo puede reducir las asignaciones del montón.
-
Convertir la clase en struct: Muchas veces, los tipos se pueden convertir de un
class
a unstruct
. La aplicación usa espacio de pila en lugar de realizar asignaciones de montón. -
Conservar la semántica: la conversión de un
class
objeto astruct
puede afectar a la semántica de los parámetros y los valores devueltos. Cualquier método que modifique sus parámetros ahora debe marcar esos parámetros con elref
modificador . Esto garantiza que las modificaciones se realicen en el objeto correcto. Del mismo modo, si el autor de la llamada debe modificar una propiedad o un valor devuelto de método, ese valor devuelto debe marcarse con elref
modificador . -
Evitar copias: cuando se pasa una estructura grande como parámetro, puede marcarlo con el modificador
in
. Puede pasar una referencia en menos bytes y asegurarse de que el método no modifica el valor original. También puede devolver valores usandoreadonly ref
para devolver una referencia que no se pueda modificar.
Con estas técnicas puedes mejorar el rendimiento en las rutas críticas de tu código.