Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Tutorial: Reduza as alocações de memória com
Muitas vezes, 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. Visual Studio fornece ótimas ferramentas que ajudam a analisar como seu aplicativo está usando a memória. Depois de determinar onde seu aplicativo faz alocações desnecessárias, você faz alterações para minimizar essas alocações. Você converte class
tipos para struct
tipos. Você usa ref
funcionalidades 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 objetos .NET usada para analisar o uso de 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 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 um pouco mais, e sempre na mesma direção: o oxigênio diminui, o dióxido de carbono aumenta, a temperatura aumenta, assim como a umidade relativa. Quando os sensores se combinam para mostrar aumentos, o alarme de intrusão é acionado.
Neste tutorial, você executará o aplicativo, fará medições nas 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.
Explore a aplicação inicial
Baixe o aplicativo e execute o exemplo inicial. O aplicativo inicial funciona corretamente, mas como aloca muitos objetos pequenos a cada ciclo de medição, seu desempenho se degrada 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 eliminadas.
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 reúne alguns dados iniciais da linha de 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;
});
Uma vez que os dados da linha de base tenham sido estabelecidos, ele executa a simulação na sala, onde um gerador de números aleatórios 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 incluem as medições, a média amortecida das últimas 50 medições, e a média de todas as medições realizadas.
Em seguida, execute o aplicativo usando a ferramenta de alocação de objetos .NET. Certifique-se de que você está usando a Release
compilação, não a Debug
compilação. No menu Depurar, abra o perfilador de desempenho. Marque apenas a opção .NET Object Allocation Tracking e nada mais. Execute seu aplicativo até a conclusão. O analisador de desempenho mede as alocações de objetos e relata as alocações e os ciclos de coleta de lixo. Você verá um gráfico semelhante à imagem a seguir:
O gráfico anterior mostra que trabalhar para minimizar as alocações proporcionará benefícios de desempenho. Você vê um padrão de dente de serra no gráfico de objetos ativos. Isso diz que inúmeros objetos são criados que rapidamente se tornam lixo. Eles são recolhidos mais tarde, como é mostrado no gráfico delta do objeto. As barras vermelhas para baixo indicam um ciclo de coleta de lixo.
Em seguida, observe a guia Alocações abaixo dos gráficos. Esta tabela mostra quais tipos são mais alocados:
O System.String tipo é responsável pela maioria das alocações. Minimizar a frequência das alocações de cadeias de caracteres deveria ser a tarefa mais importante. Esta aplicação imprime várias saídas formatadas para a consola constantemente. Para esta simulação, queremos manter as mensagens, por isso vamos concentrar-nos nas duas linhas seguintes: 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 trecho:
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 inúmeras double
medições. É maior do que você gostaria de copiar em caminhos críticos. No entanto, essa decisão implicou um grande número de atribuições. Altere o tipo de class
para um struct
.
Mudar de class
para struct
introduz alguns erros do compilador porque o código original usava referências de verificação null
em alguns pontos. A primeira 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 medições. As leituras para um sensor são relatadas como a média das últimas 50 medições. Isso reduz o ruído nas leituras. Antes de terem sido feitas 50 leituras completas, estes 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 SensorMeasurement
tipo inclui um string
para o identificador de sala, para que você possa usar esse teste em vez disso:
if (recentMeasurements[i].Room is not null)
Os outros três erros do compilador estão todos no método que repetidamente faz medições 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 starter, a variável local para o SensorMeasurement
é uma referência anulável:
SensorMeasurement? measure = default;
Agora que o SensorMeasurement
é um struct
em vez de um class
, o anulável é 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 depois que o método retorna.
Importante
Alterar um tipo de a class
para a struct
pode alterar a semântica do seu programa. Quando um class
tipo é passado para um método, quaisquer mutações feitas no método afetam o argumento. Quando um struct
tipo é passado para um método, e as mutações feitas no método são realizadas para uma cópia do argumento. Isso significa que qualquer método que modifique os seus argumentos por design deverá ser atualizado para usar o modificador ref
em qualquer tipo de argumento que 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 à SensorMeasurement
estrutura:
public readonly struct SensorMeasurement
O compilador impõe a readonly
natureza da SensorMeasurement
estrutura. Se sua inspeção do código perdeu algum método que modificou o estado, o compilador lhe dirá. Seu aplicativo ainda é compilado sem erros, portanto, esse tipo é readonly
. Adicionar o modificador readonly
ao alterar um tipo de class
para struct
pode ajudar 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 um trabalho extra copiando a SensorMeasurement
estrutura toda vez que ela é usada como parâmetro ou valor de retorno. A estrutura SensorMeasurement
contém quatro valores 'double', um DateTime e um string
. Essa estrutura é mensuravelmente maior do que uma referência. Vamos adicionar os modificadores ref
ou in
nos locais onde o tipo SensorMeasurement
é utilizado.
O próximo passo é encontrar métodos que retornem uma medição, ou tomar uma medida como um argumento, e usar referências sempre que possível. Comece na SensorMeasurement
estrutura. 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
};
}
Vamos deixar este como está, retornando por valor. Se você tentasse retornar pelo ref
, obteria um erro de compilador. Não é possível retornar um ref
para uma nova estrutura criada localmente no método. O projeto da estrutura imutável significa que você só pode definir os valores da medição na construção. Este método deve criar uma nova estrutura de medição.
Vejamos novamente DebounceMeasurement.AddMeasurement
. 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 relata um erro. Não é possível 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 mais longo do que o valor a que 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 o permitem. Você pode saber mais no resumo dos ref recursos de segurança.
Preservar semântica
Os conjuntos finais de alterações não terão um grande impacto no desempenho deste aplicativo porque os tipos não são criados em caminhos quentes. 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));
}
}
Este tipo contém várias propriedades. Alguns são tipos class
. A criação de um Room
objeto envolve várias alocações. Um para o próprio Room
, e um para cada membro do tipo class
que contém. Você pode converter duas dessas propriedades de tipos class
para tipos struct
: os tipos DebounceMeasurement
e AverageMeasurement
. Vamos trabalhar essa transformação com os dois tipos.
Altere o tipo de DebounceMeasurement
de class
para struct
. Isso introduz um erro CS8983: A 'struct' with field initializers must include an explicitly declared constructor
de compilador. Você pode corrigir isso adicionando um construtor vazio sem parâmetros:
public DebounceMeasurement() { }
Você pode saber mais sobre esse requisito no artigo de referência de idioma sobre estruturas.
A Object.ToString() substituição não modifica nenhum dos valores da estrutura. 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 as cópias que são descartadas. O AddMeasurement
método modifica o estado do objeto. É chamado a partir 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 para a instância pertencente a esta sala. Agora é 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. Ele usa o campo de suporte privado, debounce
, não a propriedade Debounce
de somente leitura. 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, modifica o tipo de AverageMeasurement
de um class
para um struct
, e adiciona o modificador readonly
no 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}
""";
}
Em seguida, modifique a Room
classe seguindo a mesma técnica usada para a Debounce
propriedade. A Average
propriedade retorna a readonly ref
para o 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; } }
Evite o boxe
Há uma última mudança para melhorar o desempenho. O programa principal é a impressão de 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 para as caixas geradas ToString
encaixota o valor enum. Você pode evitar isso escrevendo uma substituição na classe que formata Room
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 esse novo ToString
método:
Console.WriteLine(room.ToString());
Execute a aplicação usando o perfilador e verifique a tabela atualizada das alocações.
Você removeu várias alocações e forneceu ao seu aplicativo um aumento de desempenho.
Usando a segurança de referência na sua aplicação
Essas técnicas são ajustes de desempenho de baixo nível. Eles podem aumentar o desempenho na sua aplicação quando aplicados a caminhos críticos e quando tiver medido o impacto antes e depois das alterações. Na maioria dos casos, o ciclo que você seguirá é:
- Medir alocações: determine quais tipos estão sendo alocados mais e quando você pode reduzir as alocações de heap.
-
Converter classe em struct: Muitas vezes, os tipos podem ser convertidos de a
class
para umstruct
. A sua aplicação usa espaço de pilha em vez de fazer alocações de heap. -
Preservar semântica: a conversão de a
class
em astruct
pode afetar a semântica de parâmetros e valores de retorno. Qualquer método que modifique seus parâmetros deve agora marcar esses parâmetros com oref
modificador. Isso garante que as modificações sejam feitas no objeto correto. Da mesma forma, se um valor de retorno de propriedade ou método deve ser modificado pelo chamador, esse retorno deve ser marcado com oref
modificador. -
Evitar cópias: Quando você passa uma estrutura grande como parâmetro, você pode marcar o parâmetro com o
in
modificador. 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 valores comreadonly ref
para retornar uma referência que não pode ser modificada.
Usando essas técnicas, você pode melhorar o desempenho em caminhos quentes do seu código.