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:

Nessa semana, em uma dessas conversas que valem a pena, surgiu uma breve discussão sobre um texto antigo de Rubem...
Mais preocupante que o posicionamento do presidente é sua falta de constância. Há dois dias, uma medida provisória proposta pelo...
When designing systems that need to scale you always need to remember that using better resources could help you to...
Entropia é um conceito oriundo da física que trata da quantidade de “desordem” em um sistema. As leis da termodinâmica...
That is a question that I have been answering for years. The answer is an emphatic “NO” in most cases....
In the previous post, I mentioned Mario Fusco who wrote a blog post series questioning the way Java developers are...
Masterclass

O Poder do Metamodelo para Profissionais Técnicos Avançarem

Nesta masterclass aberta ao público, vamos explorar como o Metamodelo para a Criação, desenvolvido por Elemar Júnior, pode ser uma ferramenta poderosa para alavancar sua carreira técnica em TI.

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?