Partilhar via


Reduza as alocações de memória usando os novos recursos do C#

Importante

As técnicas descritas nesta seção melhoram o desempenho quando aplicadas a hot paths em seu código. Os caminhos quentes são as seções da sua base de código que são executadas com frequência e repetidamente em operações normais. A aplicação dessas técnicas a códigos que não são frequentemente executados terá um impacto mínimo. Antes de fazer qualquer alteração para melhorar o desempenho, é fundamental medir uma linha de base. Em seguida, analise essa linha de base para determinar onde ocorrem gargalos de memória. Você pode aprender sobre muitas ferramentas multiplataforma para medir o desempenho do seu aplicativo na seção Diagnóstico e instrumentação. Você pode praticar uma sessão de criação de perfil no tutorial para medir o uso de memória na documentação do Visual Studio.

Depois de medir o uso de memória e determinar que é possível reduzir as alocações, use as técnicas desta seção para reduzir as alocações. Após cada alteração sucessiva, meça novamente o uso da memória. Certifique-se de que cada alteração tem um impacto positivo no uso de memória em seu aplicativo.

O trabalho de desempenho no .NET geralmente significa remover alocações do seu código. Cada bloco de memória alocado deve ser eventualmente liberado. Menos alocações reduzem o tempo gasto na coleta de lixo. Ele permite um tempo de execução mais previsível, removendo coleções de lixo de caminhos de código específicos.

Uma tática comum para reduzir as alocações é alterar estruturas de dados críticas de class tipos para struct tipos. Essa mudança afeta a semântica do uso desses tipos. Parâmetros e retornos agora são passados por valor em vez de por referência. O custo de copiar um valor é insignificante se os tipos forem pequenos, três palavras ou menos (considerando que uma palavra é de tamanho natural de um inteiro). É mensurável e pode ter um impacto real no desempenho de tipos maiores. Para combater o efeito da cópia, os desenvolvedores podem passar esses tipos para ref recuperar a semântica pretendida.

Os recursos do C# ref oferecem a capacidade de expressar a semântica desejada para struct tipos sem afetar negativamente sua usabilidade geral. Antes desses aprimoramentos, os desenvolvedores precisavam recorrer a unsafe estruturas com ponteiros e memória não processada para obter o mesmo efeito no desempenho. O compilador gera código seguro verificável para os novos ref recursos relacionados. Código verificável e seguro significa que o compilador deteta possíveis saturações de buffer ou acessa memória não alocada ou liberada. O compilador deteta e previne alguns erros.

Passagem e retorno por referência

As variáveis em C# armazenam valores. Em struct tipos, o valor é o conteúdo de uma instância do tipo. Em class tipos, o valor é uma referência a um bloco de memória que armazena uma instância do tipo. Adicionar o ref modificador significa que a variável armazena a referência ao valor. Em struct tipos, a referência aponta para o armazenamento que contém o valor. Em tipos class, a referência aponta para o armazenamento que contém o bloco de memória.

Em C#, os parâmetros para métodos são passados por valor, e os valores de retorno são retornados por valor. O valor do argumento é passado para o método. O valor do argumento de retorno é o valor de retorno.

O ref, in, ref readonly, ou out modificador indica que o argumento é passado por referência. Uma referência ao local de armazenamento é passada para o método. Adicionar ref à assinatura do método significa que o valor de retorno é retornado por referência. Uma referência ao local de armazenamento é o valor de retorno.

Você também pode usar a atribuição ref para que uma variável se refira a outra variável. Uma atribuição típica copia o valor do lado direito para a variável no lado esquerdo da atribuição. Uma atribuição ref copia a localização da memória da variável no lado direito para a variável no lado esquerdo. O ref agora refere-se à variável original:

int anInteger = 42; // assignment.
ref int location = ref anInteger; // ref assignment.
ref int sameLocation = ref location; // ref assignment

Console.WriteLine(location); // output: 42

sameLocation = 19; // assignment

Console.WriteLine(anInteger); // output: 19

Ao atribuir uma variável, você altera seu valor. Quando atribuí ref uma variável, alteras a que se refere.

Você pode trabalhar diretamente com o armazenamento para valores usando ref variáveis, passar por referência e atribuição ref. As regras de escopo impostas pelo compilador garantem a segurança ao trabalhar diretamente com o armazenamento.

Os ref readonly modificadores e in indicam que o argumento deve ser passado por referência e não pode ser reatribuído no método. A diferença é que ref readonly indica que o método usa o parâmetro como uma variável. O método pode capturar o parâmetro ou pode retornar o parâmetro por referência de somente leitura. Nesses casos, você deve usar o ref readonly modificador. Caso contrário, o in modificador oferece mais flexibilidade. Não é necessário adicionar o in modificador a um argumento para um in parâmetro, para que você possa atualizar as assinaturas de API existentes com segurança usando o in modificador. O compilador emite um aviso se não adicionar o modificador ref ou in a um argumento para um parâmetro ref readonly.

Contexto seguro de referência

O C# inclui regras para ref expressões para garantir que uma ref expressão não possa ser acessada onde o armazenamento ao qual ela se refere não é mais válido. Considere o seguinte exemplo:

public ref int CantEscape()
{
    int index = 42;
    return ref index; // Error: index's ref safe context is the body of CantEscape
}

O compilador relata um erro porque você não pode retornar uma referência a uma variável local de um método. O chamador não pode acessar o armazenamento ao qual está sendo referido. O contexto ref safe define o escopo no qual uma ref expressão é segura para acessar ou modificar. A tabela a seguir lista os contextos ref safe para tipos de variáveis. ref Os campos não podem ser declarados em um class ou um struct não-ref, portanto, essas filas não estão na tabela:

Declaração ref contexto seguro
local não referenciado bloco onde a variável local é declarada
parâmetro non-ref Método atual
ref, ref readonly, in parâmetro Método de chamada
parâmetro out Método atual
class campo Método de chamada
campo não-referência struct Método atual
ref campo de ref struct Método de chamada

Uma variável pode ser ref retornada se o seu contexto seguro por referência for o método de chamada. Se seu contexto ref safe for o método atual ou um bloco, ref o retorno não será permitido. O trecho a seguir mostra dois exemplos. Um campo de membro pode ser acessado a partir do escopo que chama um método, portanto, o método de chamada é o contexto seguro de referência para um campo de classe ou struct. O contexto seguro de ref para um parâmetro com os modificadores ref ou in é o método inteiro. Ambos podem ser ref retornados de um método membro:

private int anIndex;

public ref int RetrieveIndexRef()
{
    return ref anIndex;
}

public ref int RefMin(ref int left, ref int right)
{
    if (left < right)
        return ref left;
    else
        return ref right;
}

Observação

Quando o ref readonly modificador ou in é aplicado a um parâmetro, esse parâmetro pode ser retornado por ref readonly, não ref.

O compilador garante que uma referência não possa escapar de seu contexto ref safe. Você pode usar ref parâmetros, ref returne ref variáveis locais com segurança porque o compilador deteta se você escreveu acidentalmente código onde uma ref expressão pode ser acessada quando seu armazenamento não é válido.

Contexto seguro e estruturas de referência

ref struct Os tipos exigem mais regras para garantir que podem ser utilizados com segurança. Um ref struct tipo pode incluir ref campos. Isso exige a introdução de um contexto seguro. Para a maioria dos tipos, o contexto seguro é o método de chamada. Em outras palavras, um valor que não é um ref struct sempre pode ser retornado de um método.

Informalmente, o contexto seguro para um ref struct é o escopo onde todos os seus ref campos podem ser acessados. Em outras palavras, é a interseção do contexto seguro de referência de todos os seus ref campos. O método a seguir retorna um ReadOnlySpan<char> para um campo membro, portanto, seu contexto seguro é o método:

private string longMessage = "This is a long message";

public ReadOnlySpan<char> Safe()
{
    var span = longMessage.AsSpan();
    return span;
}

Em contraste, o código a seguir emite um erro porque o membro do ref field refere-se à matriz de inteiros alocada na pilha do Span<int>. Ele não pode escapar do método:

public Span<int> M()
{
    int length = 3;
    Span<int> numbers = stackalloc int[length];
    for (var i = 0; i < length; i++)
    {
        numbers[i] = i;
    }
    return numbers; // Error! numbers can't escape this method.
}

Unificar tipos de memória

A introdução de System.Span<T> e System.Memory<T> fornecem um modelo unificado para trabalhar com memória. System.ReadOnlySpan<T> e System.ReadOnlyMemory<T> fornecem versões apenas leitura para aceder à memória. Todos eles fornecem uma abstração sobre um bloco de memória armazenando uma matriz de elementos semelhantes. A diferença é que Span<T> e ReadOnlySpan<T> são ref struct tipos enquanto Memory<T> e ReadOnlyMemory<T> são struct tipos. Spans contêm um ref fieldarquivo . Portanto, as instâncias de um span não podem sair de seu contexto seguro. O contexto seguro de um ref struct é o contexto seguro de referência do seu ref field. A implementação de Memory<T> e ReadOnlyMemory<T> removem esta restrição. Use esses tipos para acessar diretamente buffers de memória.

Melhore o desempenho com segurança de referência

O uso desses recursos para melhorar o desempenho envolve estas tarefas:

  • Evite alocações: quando você altera um tipo de a class para um struct, você altera a forma como ele é armazenado. As variáveis locais são armazenadas na pilha. Os membros são armazenados em linha quando o objeto de contêiner é alocado. Essa mudança significa menos alocações e isso diminui o trabalho que o coletor de lixo faz. Também pode diminuir a pressão da memória para que o coletor de lixo seja executado com menos frequência.
  • Preservar semântica de referência: alterar um tipo de a class para a struct altera a semântica de passar uma variável para um método. O código que modificou o estado de seus parâmetros precisa ser modificado. Agora que o parâmetro é um struct, o método está modificando uma cópia do objeto original. Você pode restaurar a semântica original passando esse parâmetro como um ref parâmetro. Após essa alteração, o método modifica o original struct novamente.
  • Evite copiar dados: copiar tipos maiores struct pode afetar o desempenho em alguns caminhos de código. Você também pode adicionar o ref modificador para passar estruturas de dados maiores para métodos por referência em vez de por valor.
  • Restringir modificações: Quando um struct tipo é passado por referência, o método chamado pode modificar o estado da estrutura. Você pode substituir o modificador ref pelo modificador ref readonly ou in para indicar que o argumento não pode ser modificado. Prefira ref readonly quando o método captura o parâmetro ou o retorna por referência apenas de leitura. Você também pode criar readonly struct tipos ou struct tipos com readonly membros para ter um maior controle sobre quais membros de um struct podem ser modificados.
  • Manipular diretamente a memória: Alguns algoritmos são mais eficientes ao tratar estruturas de dados como um bloco de memória contendo uma sequência de elementos. Os Span tipos e Memory fornecem acesso seguro a blocos de memória.

Nenhuma dessas técnicas requer unsafe código. Usado com sabedoria, você pode obter características de desempenho de código seguro que anteriormente só era possível usando técnicas inseguras. Você mesmo pode experimentar as técnicas no tutorial sobre como reduzir as alocações de memória.