Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Tutorial: como reduzir alocações de memória com segurança
Geralmente, o ajuste de desempenho para um aplicativo .NET envolve duas técnicas. Primeiro, reduza o número e o tamanho das alocações de heap. Em segundo lugar, reduza a frequência com que os dados são copiados. O Visual Studio fornece ótimas ferramentas que ajudam a analisar como seu aplicativo está usando memória. Depois de determinar onde seu aplicativo faz alocações desnecessárias, você faz alterações para minimizar essas alocações. Você converte os tipos class
em tipos struct
. Você usa ref
de segurança para preservar a semântica e minimizar a cópia extra.
Use o Visual Studio 17.5 para obter a melhor experiência com este tutorial. A ferramenta de alocação de objeto .NET usada para analisar o uso da memória faz parte do Visual Studio. Você pode usar o Visual Studio Code e a linha de comando para executar o aplicativo e fazer todas as alterações. No entanto, você não poderá ver os resultados da análise de suas alterações.
O aplicativo que você usará é uma simulação de um aplicativo IoT que monitora vários sensores para determinar se um intruso entrou em uma galeria secreta com objetos de valor. Os sensores de IoT estão constantemente enviando dados que medem a mistura de Oxigênio (O2) e Dióxido de Carbono (CO2) no ar. Eles também relatam a temperatura e a umidade relativa. Cada um desses valores está flutuando ligeiramente o tempo todo. No entanto, quando uma pessoa entra na sala, a mudança torna-se um pouco mais acentuada, e sempre na mesma direção: Oxigênio diminui, Dióxido de Carbono aumenta, temperatura aumenta, tal como a umidade relativa. Quando os sensores se combinam para mostrar aumentos, o alarme de intruso é disparado.
Neste tutorial, você executará o aplicativo, fará medidas sobre alocações de memória e, em seguida, melhorará o desempenho reduzindo o número de alocações. O código-fonte está disponível no navegador de exemplos.
Explorar o aplicativo inicial
Baixe o aplicativo e execute o exemplo inicial. O aplicativo inicial funciona corretamente, mas como aloca muitos objetos pequenos com cada ciclo de medição, seu desempenho diminui lentamente à medida que é executado ao longo do tempo.
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
Muitas linhas removidas.
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
Você pode explorar o código para saber como o aplicativo funciona. O programa principal executa a simulação. Depois de pressionar <Enter>
, ele cria uma sala e coleta alguns dados de linha de base iniciais:
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;
});
Depois que os dados de linha de base tiverem sido estabelecidos, ele executará a simulação na sala, em que um gerador de número aleatório determina se um intruso entrou na 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;
});
Outros tipos contêm as medidas, uma medida esporádica que é a média das últimas 50 medidas e a média de todas as medidas realizadas.
Em seguida, execute o aplicativo usando a ferramenta de alocação de objeto .NET. Verifique se você está usando o Release
build, não o Debug
build. No menu Depuração, abra o Analisador de Desempenho. Verifique a opção Acompanhamento de Alocação de Objetos do .NET, mas nada mais. Execute seu aplicativo até a conclusão. O criador de perfil mede as alocações de objetos e relata as alocações e os ciclos de coleta de lixo. Você deverá ver um grafo semelhante à seguinte imagem:
O grafo anterior mostra que trabalhar para minimizar alocações fornecerá benefícios de desempenho. Você vê um padrão de serrilhado no grafo de objetos dinâmicos. Isso informa que vários objetos são criados que rapidamente se tornam lixo. Eles são coletados depois, como é mostrado no grafo delta de objeto. As barras vermelhas para baixo indicam um ciclo de coleta de lixo.
Em seguida, examine a guia Alocações abaixo dos grafos. Esta tabela mostra quais tipos são mais alocados:
O System.String tipo representa a maioria das alocações. A tarefa mais importante é minimizar a frequência das alocações de strings. Este aplicativo imprime várias saídas formatadas no console constantemente. Para esta simulação, queremos manter as mensagens; portanto, iremos nos concentrar nos dois próximos tipos de linha: o tipo SensorMeasurement
e o tipo IntruderRisk
.
Clique duas vezes na SensorMeasurement
linha. Você pode ver que todas as alocações ocorrem no static
método SensorMeasurement.TakeMeasurement
. Você pode ver o método no seguinte snippet:
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 aloca um novo SensorMeasurement
objeto, que é um class
tipo. Cada SensorMeasurement
criado causa uma alocação de heap.
Alterar classes para structs
O código a seguir mostra a declaração 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}
""";
}
O tipo foi originalmente criado como um class
porque contém várias double
medidas. Ele é maior do que o ideal para ser copiado em caminhos críticos. No entanto, essa decisão significou um grande número de alocações. Altere o tipo de um class
para um struct
.
A alteração de um class
para struct
introduz alguns erros de compilação porque o código original usava verificações de referência null
em alguns pontos. O primeiro está na DebounceMeasurement
classe, no AddMeasurement
método:
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);
}
O DebounceMeasurement
tipo contém uma matriz de 50 medidas. As leituras de um sensor são relatadas como a média das últimas 50 medidas. Isso reduz o ruído nas leituras. Antes de 50 leituras completas serem realizadas, esses valores são null
. O código verifica a null
referência para relatar a média correta na inicialização do sistema. Depois de alterar o SensorMeasurement
tipo para um struct, você deve usar um teste diferente. O tipo SensorMeasurement
inclui um string
para o identificador de sala, de forma que você possa usar esse teste no lugar desse.
if (recentMeasurements[i].Room is not null)
Os outros três erros do compilador estão todos no método que faz medidas repetidamente em uma 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));
}
No método inicializador, a variável local para a SensorMeasurement
é uma referência nula:
SensorMeasurement? measure = default;
Agora que o SensorMeasurement
é um struct
, em vez de um class
, o nullable é um tipo de valor anulável. Você pode alterar a declaração para um tipo de valor para corrigir os erros restantes do compilador:
SensorMeasurement measure = default;
Agora que os erros do compilador foram resolvidos, você deve examinar o código para garantir que a semântica não tenha sido alterada. Como struct
os tipos são passados por valor, as modificações feitas nos parâmetros do método não são visíveis após o retorno do método.
Importante
Alterar um tipo de um class
para um struct
pode alterar a semântica do seu programa. Quando um class
tipo é passado para um método, todas as mutações feitas no método são feitas no argumento. Quando um tipo struct
é passado a um método, as mutações feitas no método são feitas em uma cópia do argumento. Isso significa que qualquer método que, por padrão, modifique seus argumentos deve ser atualizado para usar o modificador ref
em qualquer tipo de argumento que você tenha alterado de class
para struct
.
O SensorMeasurement
tipo não inclui nenhum método que altere o estado, portanto, isso não é uma preocupação neste exemplo. Você pode provar isso adicionando o readonly
modificador ao SensorMeasurement
struct:
public readonly struct SensorMeasurement
O compilador impõe a natureza readonly
do struct SensorMeasurement
. Se a inspeção do código tiver perdido algum método que modificou o estado, o compilador informará. Seu aplicativo ainda é compilado sem erros, portanto, esse tipo é readonly
. Adicionar o modificador readonly
quando você altera um tipo de class
para struct
pode ajudá-lo a encontrar membros que modificam o estado de struct
.
Evite fazer cópias
Você removeu um grande número de alocações desnecessárias do seu aplicativo. O SensorMeasurement
tipo não aparece na tabela em nenhum lugar.
Agora, ele está fazendo trabalho extra copiando a SensorMeasurement
estrutura sempre que ela é usada como um parâmetro ou um valor retornado. O "struct" SensorMeasurement
contém quatro "doubles", um DateTime e um string
. Essa estrutura é mensuravelmente maior que uma referência. Vamos adicionar os modificadores ref
ou in
aos locais onde o tipo SensorMeasurement
é usado.
A próxima etapa é localizar métodos que retornam uma medida ou que façam uma medida como argumento e usem referências sempre que possível. Comece no struct SensorMeasurement
. O método estático TakeMeasurement
cria e retorna um novo 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
};
}
Deixaremos este como está, retornando por valor. Se você tentasse retornar por ref
, obteria um erro de compilação. Não é possível retornar um ref
a uma estrutura criada localmente no método. O design do struct imutável significa que você só pode definir os valores da medida na construção. Esse método deve criar um novo struct de medida.
Vamos analisar DebounceMeasurement.AddMeasurement
novamente. Você deve adicionar o in
modificador ao 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);
}
Isso salva uma operação de cópia. O in
parâmetro é uma referência à cópia já criada pelo chamador. Você também pode salvar uma cópia com o TakeMeasurement
método no Room
tipo. Este método ilustra como o compilador fornece segurança quando você passa argumentos por ref
. O método inicial TakeMeasurement
no Room
tipo usa um argumento de Func<SensorMeasurement, bool>
. Se você tentar adicionar o in
modificador ou ref
a essa declaração, o compilador relatará um erro. Você não pode passar um ref
argumento para uma expressão lambda. O compilador não pode garantir que a expressão chamada não copie a referência. Se a expressão lambda capturar a referência, a referência poderá ter um tempo de vida maior do que o valor ao qual se refere. Acessá-lo fora de seu contexto ref safe resultaria em corrupção de memória. As ref
regras de segurança não permitem isso. Saiba mais na visão geral de recursos de segurança de referência.
Preservar semântica
Os conjuntos finais de alterações não terão um grande impacto no desempenho desse aplicativo porque os tipos não são criados em caminhos críticos. Essas alterações ilustram algumas das outras técnicas que você usaria no ajuste de desempenho. Vamos dar uma olhada na classe 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));
}
}
Esse tipo contém várias propriedades. Alguns são tipos class
. A criação de um Room
objeto envolve várias alocações. Uma para o Room
em si e outra para cada um dos membros de um tipo class
que ele contém. Você pode converter duas dessas propriedades de tipos class
em tipos struct
: os tipos DebounceMeasurement
e AverageMeasurement
. Vamos trabalhar nessa transformação com ambos os tipos.
Altere o tipo DebounceMeasurement
de um class
para struct
. Isso apresenta um erro CS8983: A 'struct' with field initializers must include an explicitly declared constructor
do compilador. Você pode corrigir isso adicionando um construtor sem parâmetros vazio:
public DebounceMeasurement() { }
Você pode aprender mais sobre este requisito no artigo de referência linguística sobre structs.
A Object.ToString() substituição não modifica nenhum dos valores do struct. Você pode adicionar o readonly
modificador a essa declaração de método. O DebounceMeasurement
tipo é mutável, portanto, você precisará tomar cuidado para que as modificações não afetem cópias descartadas. O AddMeasurement
método modifica o estado do objeto. Ele é chamado da classe Room
, no método TakeMeasurements
. Você deseja que essas alterações persistam depois de chamar o método. Você pode alterar a Room.Debounce
propriedade para retornar uma referência a uma única instância do DebounceMeasurement
tipo:
private DebounceMeasurement debounce = new();
public ref readonly DebounceMeasurement Debounce { get { return ref debounce; } }
Há algumas alterações no exemplo anterior. Primeiro, a propriedade é uma propriedade somente leitura que retorna uma referência somente leitura à instância pertencente a essa sala. Agora ele é apoiado por um campo declarado que é inicializado quando o Room
objeto é instanciado. Depois de fazer essas alterações, você atualizará a implementação do AddMeasurement
método. Ela usa o campo de suporte privado, debounce
, não a propriedade somente leitura Debounce
. Dessa forma, as alterações ocorrem na única instância criada durante a inicialização.
A mesma técnica funciona com a Average
propriedade. Primeiro, modifique o AverageMeasurement
tipo de um class
para um struct
e adicione o readonly
modificador ao ToString
método:
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}
""";
}
Em seguida, modifique a Room
classe seguindo a mesma técnica usada para a Debounce
propriedade. A propriedade Average
retorna um readonly ref
ao campo privado para a medição média. O AddMeasurement
método modifica os campos internos.
private AverageMeasurement average = new();
public ref readonly AverageMeasurement Average { get { return ref average; } }
Evitar boxe
Há uma última mudança para melhorar o desempenho. O programa principal é imprimir estatísticas para a sala, incluindo a avaliação de risco:
Console.WriteLine($"Current intruders: {room.Intruders}");
Console.WriteLine($"Calculated intruder risk: {room.RiskStatus}");
A chamada ao ToString
gerado faz a conversão boxing para o valor de enumeração. Você pode evitar isso escrevendo uma substituição na Room
classe que formata a cadeia de caracteres com base no valor do risco 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()}";
Em seguida, modifique o código no programa principal para chamar este novo ToString
método:
Console.WriteLine(room.ToString());
Execute o aplicativo usando a ferramenta de análise de desempenho e examine a tabela atualizada de alocações.
Você removeu várias alocações e forneceu ao aplicativo um aumento de desempenho.
Como usar a segurança de referência no aplicativo
Essas técnicas são um ajuste de desempenho de nível baixo. Elas podem aumentar o desempenho no aplicativo quando aplicadas a caminhos críticos e quando o impacto é medido antes e depois das alterações. Na maioria dos casos, o ciclo que você seguirá é:
- Alocações de medidas: determine quais tipos estão sendo mais alocados e quando você pode reduzir as alocações de heap.
-
Converter classe em struct: muitas vezes, os tipos podem ser convertidos de um
class
para umstruct
. O aplicativo usa espaço de pilha em vez de fazer alocações de heap. -
Preservar semântica: converter um
class
em umstruct
pode afetar a semântica para parâmetros e valores retornados. Qualquer método que modifique seus parâmetros agora deve marcar esses parâmetros com oref
modificador. Isso garante que as modificações sejam feitas no objeto correto. Da mesma forma, se um valor retornado de propriedade ou método deve ser modificado pelo chamador, esse retorno deve ser marcado com oref
modificador. -
Evite cópias: Ao passar uma estrutura grande como parâmetro, você pode marcar o parâmetro com o modificador
in
. Você pode passar uma referência em menos bytes e garantir que o método não modifique o valor original. Você também pode retornar valoresreadonly ref
para retornar uma referência que não pode ser modificada.
Usando essas técnicas, você pode melhorar o desempenho em caminhos frequentes do seu código.