Esta publicação está disponível em vídeo, ampliada e revisada, no canal da EximiaCo.


Uma das causas mais comuns para problemas de performance em .NET é o descuido com o Garbage Collector. Mesmo funções simples, se implementadas de forma descuidada, podem ocasionar boas dores de cabeça.

Neste post, compartilha a análise e otimização de uma simples função para validação de CPF. Algo pequeno, frente ao tamanho de um sistema, mas sintomático quanto ao estilo de código que temos encontrado.

Como fizemos o teste

Acreditamos que é sempre importante ter mensurações para pautar qualquer otimização. Eis um exemplo simples de como mensurar uma função isolada:

static void Main(string[] args)
{
    var sw = new Stopwatch();
    var before2 = GC.CollectionCount(2);
    var before1 = GC.CollectionCount(1);
    var before0 = GC.CollectionCount(0);
    Func<string, bool> sut = Original.ValidarCPF;

    sw.Start();
    for (int i = 0; i < 1_000_000; i++)
    {
        if (!sut("771.189.500-33"))
        {
            throw new Exception("Error!");
        }

        if (sut("771.189.500-34"))
        {
            throw new Exception("Error!");
        }
    }
    sw.Stop();

    Console.WriteLine("Tempo total: {sw.ElapsedMilliseconds}ms");
    Console.WriteLine("GC Gen #2 : {GC.CollectionCount(2) - before2}");
    Console.WriteLine("GC Gen #1 : {GC.CollectionCount(1) - before1}"); 
    Console.WriteLine("GC Gen #0 : {GC.CollectionCount(0) - before0}");  
    Console.WriteLine("Done!"); 
}

Aqui, estamos apurando tempo aproximado de execução. Poderíamos utilizar algo mais preciso (como Benchmark.net), mas, geralmente começamos considerando uma abordagem mais simples (apenas para termos uma ordem de grandeza).

Como, geralmente, problemas de performance tem relação direta com o Garbage Collector, apuramos o comportamento do mesmo também.

Obviamente, testar performance validando 1_000_000 de CPFs é um exagero. Perceba, porém, que este número “exagerado”, serve para que a comparação seja perceptível.

Mais uma vez, a ideia não é focar na função em si, mas destacar problemas com o estilo de codificação adotado.

O código original

Aqui, o código original, encontrado em uma base de código quente de um cliente.

public static bool ValidarCPF(string sourceCPF)
{
    if (String.IsNullOrWhiteSpace(sourceCPF))
        return false;

    string clearCPF;
    clearCPF = sourceCPF.Trim(); 
    clearCPF = clearCPF.Replace("-", "");
    clearCPF = clearCPF.Replace(".", ""); 

    if (clearCPF.Length != 11)
    {
        return false;
    }

    int[] cpfArray;
    int totalDigitoI = 0;
    int totalDigitoII = 0;
    int modI;
    int modII;

    if (clearCPF.Equals("00000000000") ||
        clearCPF.Equals("11111111111") ||
        clearCPF.Equals("22222222222") ||
        clearCPF.Equals("33333333333") ||
        clearCPF.Equals("44444444444") ||
        clearCPF.Equals("55555555555") ||
        clearCPF.Equals("66666666666") ||
        clearCPF.Equals("77777777777") ||
        clearCPF.Equals("88888888888") ||
        clearCPF.Equals("99999999999"))
    {
        return false;
    }

    foreach (char c in clearCPF)
    {
        if (!char.IsNumber(c))
        {
            return false;
        }
    }

    cpfArray = new int[11];
    for (int i = 0; i < clearCPF.Length; i++)
    {
        cpfArray[i] = int.Parse(clearCPF[i].ToString());
    }

    for (int posicao = 0; posicao < cpfArray.Length - 2; posicao++)
    {
        totalDigitoI += cpfArray[posicao] * (10 - posicao);
        totalDigitoII += cpfArray[posicao] * (11 - posicao);
    }

    modI = totalDigitoI % 11;
    if (modI < 2) { modI = 0; }
    else { modI = 11 - modI; }

    if (cpfArray[9] != modI)
    {
        return false;
    }

    totalDigitoII += modI * 2;

    modII = totalDigitoII % 11;
    if (modII < 2) { modII = 0; }
    else { modII = 11 - modII; }
    if (cpfArray[10] != modII)
    {
        return false;
    }
    // CPF Válido!
    return true;
}

Eis o os resultados .

O número que chama atenção aqui é a quantidade de coletas na Gen0 (algumas centenas).

Eliminando alocações obviamente desnecessárias

A primeira coisa que fizemos para diminuir a pressão sobre o Garbage Collector foi reduzir a quantidade de alocações. Começamos pela parte mais óbvia: todo digito do CPF, antes de ser convertido para número, gerava um string. Além disso, um array era gerado para armazenar a “versão numérica” do CPF. Optamos por uma estratégia sem essas alocações.

public static class Version2
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static int ObterDigito(
        string value,
        int pos
    ) => value[pos] - '0';

    public static bool ValidarCPF(string sourceCPF)
    {
        if (String.IsNullOrWhiteSpace(sourceCPF))
            return false;

        var clearCPF = sourceCPF.Trim();
        clearCPF = clearCPF.Replace("-", "");
        clearCPF = clearCPF.Replace(".", ""); 

        if (clearCPF.Length != 11)
        {
            return false;
        }

        int totalDigitoI = 0;
        int totalDigitoII = 0;

        if (clearCPF.Equals("00000000000") ||
            clearCPF.Equals("11111111111") ||
            clearCPF.Equals("22222222222") ||
            clearCPF.Equals("33333333333") ||
            clearCPF.Equals("44444444444") ||
            clearCPF.Equals("55555555555") ||
            clearCPF.Equals("66666666666") ||
            clearCPF.Equals("77777777777") ||
            clearCPF.Equals("88888888888") ||
            clearCPF.Equals("99999999999"))
        {
            return false;
        }

        foreach (char c in clearCPF)
        {
            if (!char.IsNumber(c))
            {
                return false;
            }
        }
        for (int posicao = 0; posicao < clearCPF.Length - 2; posicao++)
        {
            totalDigitoI += ObterDigito(clearCPF, posicao) * (10 - posicao);
            totalDigitoII += ObterDigito(clearCPF, posicao) * (11 - posicao);
        }

        var modI = totalDigitoI % 11;
        if (modI < 2) { modI = 0; }
        else { modI = 11 - modI; }

        if (ObterDigito(clearCPF, 9) != modI)
        {
            return false;
        }

        totalDigitoII += modI * 2;
        var modII = totalDigitoII % 11;
        if (modII < 2) { modII = 0; }
        else { modII = 11 - modII; }

        if (ObterDigito(clearCPF, 10) != modII)
        {
            return false;
        }
        return true;
    }
}

O resultado obtido foi animador.

Reduzimos o volume de alocações, logo, reduzimos a quantidade de coletas de lixo e melhoramos a performance do nosso código em mais de 4x.

Trocando alocações por processamento

Agora, vamos remover as 3 alocações de strings que ocorrem logo no início do código. VAmos pagar o preço com processamento para ver o resultado.

public static class Version3
{
    public struct Cpf
    {
        private readonly string _value;
            
        private Cpf(string value)
        {
            _value = value;

        }

        public int CalculaNumeroDeDigitos()
        {
            if (_value == null)
            {
                return 0;
            }

            var result = 0;
            for (var i = 0; i < _value.Length; i++)
            {
                if (char.IsDigit(_value[i]))
                {
                    result++;
                }
            }

            return result;
        }

        public bool VerficarSeTodosOsDigitosSaoIdenticos()
        {
            var previous = -1;
            for (var i = 0; i < _value.Length; i++)
            {
                if (char.IsDigit(_value[i]))
                {
                    var digito = _value[i] - '0';
                    if (previous == -1)
                    {
                        previous = digito;
                    }
                    else
                    {
                        if (previous != digito)
                        {
                            return false;
                        }
                    }
                }
            }
            return true;
        }

        public int ObterDigito(int posicao)
        {
            int count = 0;
            for (int i = 0; i < _value.Length; i++)
            {
                if (char.IsDigit(_value[i]))
                {
                    if (count == posicao)
                    {
                        return _value[i] - '0';
                    }
                    count++;
                }
            }

            return 0;
        }

        public static implicit operator Cpf(string value)
            => new Cpf(value);

        public override string ToString() => _value;
    }

    public static bool ValidarCPF(Cpf sourceCPF)
    {
        if (sourceCPF.CalculaNumeroDeDigitos() != 11)
        {
            return false;
        }
        
        int totalDigitoI = 0;
        int totalDigitoII = 0;

        if (sourceCPF.VerficarSeTodosOsDigitosSaoIdenticos())
        {
            return false;
        }

        for (int posicao = 0; posicao < 9; posicao++)
        {
            var digito = sourceCPF.ObterDigito(posicao);
            totalDigitoI += digito * (10 - posicao);
            totalDigitoII += digito * (11 - posicao);
        }

        var modI = totalDigitoI % 11;
        if (modI < 2) { modI = 0; }
        else { modI = 11 - modI; }

        if (sourceCPF.ObterDigito(9) != modI)
        {
            return false;
        }

        totalDigitoII += modI * 2;

        var modII = totalDigitoII % 11;
        if (modII < 2) { modII = 0; }
        else { modII = 11 - modII; }

        if (sourceCPF.ObterDigito(10) != modII)
        {
            return false;
        }

        return true;
    }
}

Vejamos o resultado:

Adeus alocações! Veja quanta melhoria na performance. Menos memória alocada, nenhuma chamada ao GC, e um código que entrega resultado em muito menos tempo.

Evitando processamento em duplicidade

Nosso processamento tirou o Garbage Collector de cena e melhorou a saúde do sistema. Mas, há um bocado de coisas que poderiam ser melhores. Não acha?

Vamos reescrever nosso código, agora removendo processamento desnecessário.

public static class Version5
{
    public struct Cpf
    {
        private readonly string _value;
            
        public readonly bool EhValido;
        private Cpf(string value)
        {
            _value = value;
               
            if (value == null)
            {
                EhValido = false;
                return;
            }

            var posicao = 0;
            var totalDigito1 = 0;
            var totalDigito2 = 0;
            var dv1 = 0;
            var dv2 = 0;

            bool digitosIdenticos = true;
            var ultimoDigito = -1;

            foreach (var c in value)
            {
                if (char.IsDigit(c))
                {
                    var digito = c - '0';
                    if (posicao != 0 && ultimoDigito != digito)
                    {
                        digitosIdenticos = false;
                    }

                    ultimoDigito = digito;
                    if (posicao < 9)
                    {
                        totalDigito1 += digito * (10 - posicao);
                        totalDigito2 += digito * (11 - posicao);
                    }
                    else if (posicao == 9)
                    {
                        dv1 = digito;
                    }
                    else if (posicao == 10)
                    {
                        dv2 = digito;
                    }

                    posicao++;
                }
            }

            if (posicao > 11)
            {
                EhValido = false;
                return;
            }

            if (digitosIdenticos)
            {
                EhValido = false;
                return;
            }
                
            var digito1 = totalDigito1 % 11;
            digito1 = digito1 < 2 
                ? 0 
                : 11 - digito1;

            if (dv1 != digito1)
            {
                EhValido = false;
                return;
            }

            totalDigito2 += digito1 * 2;
            var digito2 = totalDigito2 % 11;
            digito2 = digito2 < 2 
                ? 0 
                : 11 - digito2;

            EhValido = dv2 == digito2;
        }
            
        public static implicit operator Cpf(string value)
            => new Cpf(value);

        public override string ToString() => _value;
    }

    public static bool ValidarCPF(Cpf sourceCPF) => 
        sourceCPF.EhValido;
}

O código ficou um pouco mais complexo. Mas compreensível.

Aqui está o resultado:

Acho que chegamos a um bom ponto.

Finalizando

O objetivo desse post foi mostrar como o Garbage Collector pode influenciar na eficiência de um sistema. Acho que ficou claro. Não acha?

Você consegue identificar esse problema nos códigos de sua empresa? Podemos ajudar.

Compartilhe este insight:

Comentários

Participe deixando seu comentário sobre este artigo a seguir:

Subscribe
Notify of
guest
7 Comentários
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Alessandro
Alessandro
1 ano atrás

Olá Elemar, este último ficou mais rápido porém não está validando o CPF corretamente:

Exemplo: 3157645480 ou 31576454800 traz como verdadeiro

Abs

Elemar Júnior
Elemar Júnior
1 ano atrás

Você está certo. Eu estou pegando os 11 primeiros dígitos e ignorando os demais.

Renan S Moreira
Renan S Moreira
1 ano atrás

Olá, repliquei o teste em Java tomando cuidado de converter as implementações pra obter resultados similares e obtive as mesmas melhorias, mas rolou uma dúvida. Habilitei o log de GCs e contando os timestamps, vi que o tempo gasto coletando a sujeira era ínfimo, exemplos (tempo total de execução / quantidade de coletas / tempo total com as coletas):
– Validador 1 = 1800ms / 13 GC / 13ms em GCs;
– Validador 2 = 1518ms / 12 GC / 11ms em GCs;
– Validador 3 = 149ms / 0 GC.

De fato rolou uma boa melhoria dada evolução no algoritmo de validação, mas não me parece que o grande vilão do tempo de execução dos primeiros validadores foi o GC (no meu caso). No seu exemplo a quantidade de coletas é muito maior, sem comparações. Mas em relação ao tempo de execução, será mesmo que a maior parte do tempo rolou por conta das coletas? Qual foi a proporção?

Elemar Júnior
Elemar Júnior
1 ano atrás

O problema, no meu entender, não é o tempo das coletas, mas as interrupções. Seria interessante medir o tempo em interrupções.

Elemar Júnior
Elemar Júnior
1 ano atrás

Repare, inclusive, que há um novo post mostrando uma outra abordagem usando recursos novos, próprios de .net, mas com o mesmo algoritmo original onde o ganho também fica expresso.

Zote
Zote
1 ano atrás

Oi Elemar! Tudo bem?
Não achei esse outro post utilizando os novos recursos…

AUTOR

Elemar Júnior
Fundador e CEO da EximiaCo atua como tech trusted advisor ajudando empresas a gerar mais resultados através da tecnologia.

SOLUÇÕES EXIMIACO

ESTRATÉGIA & EXECUÇÃO EM TI

Simplificamos, potencializamos 
aceleramos resultados usando a tecnologia do jeito certo.

COMO PODEMOS LHE AJUDAR?

Vamos marcar uma conversa para que possamos entender melhor sua situação e juntos avaliar de que forma a tecnologia pode trazer mais resultados para o seu negócio.

COMO PODEMOS LHE AJUDAR?

Vamos marcar uma conversa para que possamos entender melhor sua situação e juntos avaliar de que forma a tecnologia pode trazer mais resultados para o seu negócio.

+55 51 3049-7890
[email protected]

+55 51 3049-7890 |  [email protected]

7
0
Queremos saber a sua opinião, deixe seu comentáriox
()
x

Tenho interesse em conversar

Se você está querendo gerar resultados através da tecnologia, preencha este formulário que um de nossos consultores entrará em contato com você:

O seu insight foi excluído com sucesso!

O seu insight foi excluído e não está mais disponível.

O seu insight foi salvo com sucesso!

Ele está na fila de espera, aguardando ser revisado para ter sua publicação programada.