Melhorando a performance em .NET

Pequenas mudanças podem deixar seu código 10x mais rápido

Recentemente surgiu uma necessidade em um projeto que estava trabalhando: formatar um CPF , tão simples como parece:

Entrada: “12345678909”

Resultado: “123.456.789–09”

Pois bem, existem inúmeras formas de realizar esta implementação, em uma primeira versão optei talvez pelo caminho mais óbvio:

public string FormatarCpf(string cpf)
{
    // cpf: 12345678909        
    return $"{cpf.Substring(0, 3)}.{cpf.Substring(3, 3)}.{cpf.Substring(6, 3)}-{cpf.Substring(9, 2)}";
    // resultado: 123.456.789-09
}

Basicamente apenas usei Substring para dividir o CPF em 4 partes e inseri os separadores correspondentes. Esta implementação apesar de eficaz não parecia muito eficiente, então resolvi melhorá-la.

Let’s google it

Como bom programador fui pesquisar para ver se já tinha algo pronto no Google e caí no bom e velho Stack Overflow. Encontrei uma implementação bem diferente e até mais simples:

public string FormatarCpf(string cpf)
{
    return Convert.ToUInt64(cpf).ToString(@"000\.000\.000\-00");
}

Esta versão também funciona perfeitamente e a primeira vista parece “ok”, porém quando analisamos mais de perto percebemos um detalhe importante: Para usar a formatação de números (que permite definir uma máscara para um número no ToString) a string recebida é convertida para UInt64 — isto me deixou com dúvidas em relação a performance e para tirar a prova resolvi usar o Benchmark.DotNet para comparar as implementações. O resultado foi preocupante:

image.png

A primeira versão utilizando Substring demora menos da metade do tempo do que a segunda utilizando Convert.ToInt64.

Ou seja, não vale nenhum pouco a pena mudar para esta versão, porém uma média de 115 nanossegundos para uma simples formatação de string ainda não parece o ideal.

Como podemos melhorar?

O principal problema da solução usando Substring é que cada “pedaço” do CPF ao ser dividido gera uma nova alocação na memória.

Para aplicar melhorias precisamos voltar ao “problema” original pensando na utilização da memória: retornar a string do CPF sem formatação em uma nova cadeia de caracteres, adicionando apenas os separadores. Para atingir este objetivo devemos evitar novas alocações e qualquer tipo de conversão.

Felizmente existem novas estruturas como o Span<T> e stackalloc que foram pensadas justamente para ajudar em situações como essa.

Usando essa nova abordagem cheguei na seguinte solução:

public ReadOnlySpan<char> FormatarCpf(string cpf)
{
    Span<char> formattedValue = stackalloc char[14];

    Copy(cpf, startIndex: 0, length: 3, ref formattedValue, insertIndex: 0);
    formattedValue[3] = '.';

    Copy(cpf, startIndex: 3, length: 3, ref formattedValue, insertIndex: 4);
    formattedValue[7] = '.';

    Copy(cpf, startIndex: 6, length: 3, ref formattedValue, insertIndex: 8);
    formattedValue[11] = '-';

    Copy(cpf, startIndex: 9, length: 2, ref formattedValue, insertIndex: 12);

    return formattedValue.ToArray();

    static void Copy(string origin, int startIndex, int length, ref Span<char> destination, int insertIndex)
    {
        for (int i = startIndex, j = insertIndex; i < (startIndex + length); i++, j++)
            destination[j] = origin[i];
    }
}

Principais mudanças:

  • Para armazenar o valor formatado estou usando um “array” de caracteres Span<char> alocado na memória stack e definindo o tamanho que ele terá: stackalloc char[14]  —  já que sabemos o tamanho final.
  • Os valores da string original são copiados um a um para as respectivas posições do array formatado. Usei uma “Local Function” apenas para evitar muita repetição de código e otimizar o processamento.
  • O retorno foi alterado para ReadOnlySpan — que nada mais é do que a cadeia de caracteres em read-only

Resultado final:

image.png

25 nanossegundos! 10x mais rápido que a versão com conversão para inteiro (Int) e mais de 4x mais rápido que a versão original (Substring).

Conclusão: vale a pena gastar um tempo entendendo melhor como a linguagem funciona e otimizando seu código para entregar software de qualidade e performático.

O código completo dos testes incluindo mais métricas e outras versões está disponível no GitHub.


Referências:

No Comments Yet