Validando CNPJ respeitando o Garbage Collector

Nos últimos dois posts demonstrei como as alocações podem implicar em penalidades de performance.

Nesse post, parto, novamente, de código que está em produção, dessa vez uma validação de CNPJ, fazendo mudanças incrementais para obter melhorias de performance. O objetivo é começar a estabelecer um método.

O código original

O código que segue tem origem na mesma base de código onde encontrei a validação de CPF que otimizamos outro dia. Logo, há um bocado de similaridades.

public static bool ValidarCNPJ(string cnpj)
{
    if (String.IsNullOrWhiteSpace(cnpj))
        return false;

    cnpj = cnpj.Trim();
    cnpj = cnpj.Replace(".", "").Replace("-", "").Replace("/", "");

    if (cnpj.Length != 14)
        return false;

    int[] multiplicador1 = new[] { 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2 };
    int[] multiplicador2 = new[] { 6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2 };
    int soma;
    int resto;
    string digito;
    string tempCnpj;

    // Verifica os Patterns mais Comuns para CNPJ's Inválidos
    if (cnpj.Equals("00000000000000") ||
        cnpj.Equals("11111111111111") ||
        cnpj.Equals("22222222222222") ||
        cnpj.Equals("33333333333333") ||
        cnpj.Equals("44444444444444") ||
        cnpj.Equals("55555555555555") ||
        cnpj.Equals("66666666666666") ||
        cnpj.Equals("77777777777777") ||
        cnpj.Equals("88888888888888") ||
        cnpj.Equals("99999999999999"))
    {
        return false;
    }

    tempCnpj = cnpj.Substring(0, 12);
    soma = 0;

    for (int i = 0; i < 12; i++)
        soma += int.Parse(tempCnpj[i].ToString()) * multiplicador1[i];

    resto = (soma % 11);

    if (resto < 2)
        resto = 0;
    else
        resto = 11 - resto;

    digito = resto.ToString();
    tempCnpj = tempCnpj + digito;
    soma = 0;

    for (int i = 0; i < 13; i++)
        soma += int.Parse(tempCnpj[i].ToString()) * multiplicador2[i];

    resto = (soma % 11);

    if (resto < 2)
        resto = 0;
    else
        resto = 11 - resto;

    digito = digito + resto.ToString();

    return cnpj.EndsWith(digito);
}

Vamos ver como esse código se comporta frente a 1_000_000 de processamentos.

Nada bom! Mais uma vez, mais de mil ciclos de coleta.

Removendo as alocações obviamente desnecessárias

Não vamos fazer nada demais, para começar. Vamos apenas remover as alocações visivelmente desnecessárias (converter chars com dígitos para os correspondentes numéricos, e o array de multiplicadores que é constante).

static readonly int[] multiplicador1 = { 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2 };
static readonly int[] multiplicador2 = { 6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2 };
public static bool ValidarCNPJ(string cnpj)
{
    if (String.IsNullOrWhiteSpace(cnpj))
        return false;

    cnpj = cnpj.Trim();
    cnpj = cnpj.Replace(".", "").Replace("-", "").Replace("/", "");

    if (cnpj.Length != 14)
        return false;

    // Verifica os Patterns mais Comuns para CNPJ's Inválidos
    if (cnpj.Equals("00000000000000") ||
        cnpj.Equals("11111111111111") ||
        cnpj.Equals("22222222222222") ||
        cnpj.Equals("33333333333333") ||
        cnpj.Equals("44444444444444") ||
        cnpj.Equals("55555555555555") ||
        cnpj.Equals("66666666666666") ||
        cnpj.Equals("77777777777777") ||
        cnpj.Equals("88888888888888") ||
        cnpj.Equals("99999999999999"))
    {
        return false;
    }

    var tempCnpj = cnpj.Substring(0, 12);
    var soma = 0;

    for (var i = 0; i < 12; i++)
        soma += (tempCnpj[i] - '0') * multiplicador1[i];

    var resto = (soma % 11);

    if (resto < 2)
        resto = 0;
    else
        resto = 11 - resto;

    var digito = resto.ToString();
    tempCnpj = tempCnpj + digito;
    soma = 0;

    for (var i = 0; i < 13; i++)
        soma += (tempCnpj[i] - '0') * multiplicador2[i];

    resto = (soma % 11);

    if (resto < 2)
        resto = 0;
    else
        resto = 11 - resto;

    digito = digito + resto;

    return cnpj.EndsWith(digito);
}

Vejamos o resultado:

Tivemos um ganho de 4x apenas removendo alocações bobas e, mais importante, reduzindo a carga sobre o GC.

Substituindo alocações por processamento

Seguindo a linha de raciocínio dos posts anteriores, vamos substituir alocações por processamento.

public struct Cnpj
{
    private readonly string _value;

    public Cnpj(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 Cnpj(string value)
        => new Cnpj(value);

    public override string ToString()
        => _value;
}

static readonly int[] multiplicador1 = { 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2 };
static readonly int[] multiplicador2 = { 6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2 };

public static bool ValidarCNPJ(Cnpj cnpj)
{
    if (cnpj.CalculaNumeroDeDigitos() != 14)
        return false;

    // Verifica os Patterns mais Comuns para CNPJ's Inválidos
    if (cnpj.VerficarSeTodosOsDigitosSaoIdenticos())
    {
        return false;
    }

    var soma1 = 0;
    var soma2 = 0;
    for (var i = 0; i < 12; i++)
    {
        var d = cnpj.ObterDigito(i);
        soma1 += d * multiplicador1[i];
        soma2 += d * multiplicador2[i];
    }

    var resto = (soma1 % 11);

    if (resto < 2)
        resto = 0;
    else
        resto = 11 - resto;

    var dv1 = resto;
    //var digito = resto.ToString();
    soma2 += resto * multiplicador2[12];
            
    resto = (soma2 % 11);

    if (resto < 2)
        resto = 0;
    else
        resto = 11 - resto;

    var dv2 = resto;
            
    return cnpj.ObterDigito(12) == dv1 && cnpj.ObterDigito(13) == dv2;
}

No lugar de "limpar" a string (aliás, repare que no post anterior compartilhei uma forma mais eficiente para limpar strings), optamos por processamento.

Vejamos o resultado:

O que mais eu posso dizer?

Evitando processamento desnecessário

Nosso código já não "ofende" o GC. Entretanto, há muito processamento desnecessário sendo feito. Vamos a uma versão que elimina boa parte desse tipo de processamento.

public struct Cnpj
{
    private readonly string _value;

    public readonly bool EhValido;
    static readonly int[] Multiplicador1 = { 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2 };
    static readonly int[] Multiplicador2 = { 6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2 };

    public Cnpj(string value)
    {
        _value = value;

        if (value == null)
        {
            EhValido = false;
            return;
        }

        var digitosIdenticos = true;
        var ultimoDigito = -1;
        var posicao = 0;
        var totalDigito1 = 0;
        var totalDigito2 = 0;

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

                ultimoDigito = digito;
                if (posicao < 12)
                {
                    totalDigito1 += digito * Multiplicador1[posicao];
                    totalDigito2 += digito * Multiplicador2[posicao];
                }
                else if (posicao == 12)
                {
                    var dv1 = (totalDigito1 % 11);
                    dv1 = dv1 < 2 
                        ? 0 
                        : 11 - dv1;

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

                    totalDigito2 += dv1 * Multiplicador2[12];
                }
                else if (posicao == 13)
                {
                    var dv2 = (totalDigito2 % 11);

                    dv2 = dv2 < 2 
                        ? 0 
                        : 11 - dv2;

                    if (digito != dv2)
                    {
                        EhValido = false;
                        return;
                    }
                }

                posicao++;
            }
        }

        EhValido = (posicao == 14) && !digitosIdenticos;
    }

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

    public override string ToString()
        => _value;
}

public static bool ValidarCNPJ(Cnpj cnpj) 
    => cnpj.EhValido;

Resultado:

Resumindo

Melhoras de performance significativas podem ser facilmente alcançadas removendo alocações. Em muitos cenários, o resultado desse tipo de otimização é bem mais percebido (nesse exemplo mais de 90% da melhoria ocorreu removendo alocações) do que em melhoras de algoritmo.

Compartilhe este insight:

Elemar Júnior

Sou fundador e CEO da EximiaCo e atuo como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia.

Elemar Júnior

Sou fundador e CEO da EximiaCo e atuo como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia.

Mais insights para o seu negócio

Veja mais alguns estudos e reflexões que podem gerar alguns insights para o seu negócio:

A ausência de padrões leves para externar (seja para documentação ou na elaboração) a arquitetura de um software sempre é...
In a previous post, I wrote about how to support sagas using a Workflow Engine as Saga Execution Coordinator (if...
As professionals, we should focus on developing great technical solutions. But, these solutions need to solve real problems. At the...
Este exemplo é inspirado no livro do Ayende Se você deseja aprender RavenDB, recomendo que se inscreva no RavenDB bootcamp...
In this post, I would like to share my first attempt to create a (still) elementary search library. The source...
I believe that, from time to time, it is interesting to learn a new language or framework that takes us...

Inscrição realizada com sucesso!

No dia da masterclass você receberá um e-mail com um link para acompanhar a aula ao vivo. Até lá!

A sua subscrição foi enviada com sucesso!

Aguarde, em breve entraremos em contato com você para lhe fornecer mais informações sobre como participar da mentoria.

Crie sua conta

Preencha os dados para iniciar o seu cadastro no plano anual do Clube de Estudos:

Crie sua conta

Preencha os dados para iniciar o seu cadastro no plano mensal do Clube de Estudos:

× Precisa de ajuda?