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:

O ano era 2001 ou 2002. Não lembro ao certo. Eu era um jovem programador, pai recente, tentando “encontrar meu...
Inspecting the code repository of a client, I found something like this: var customer = new { Id = default(int),...
Neste post, compartilho mais algumas ideias que tenho adotado, com êxito, em meus projetos envolvendo Microsserviços e que podem ajudar...
Empresas modernas, com estilo de gestão diferente e resultados espetaculares, estão desafiando tudo o que sabemos sobre estratégia e execução....
Ao utilizar recursos não gerenciados, em .NET, precisamos garantir que estes sejam devidamente liberados. Para isso, quando criamos classes que...
Autorização, em qualquer aplicação não é processo simples. Quando estamos implementando Microsserviços, o desafio pode ser um pouco maior. Neste...
Oferta de pré-venda!

Mentoria em
Arquitetura de Software

Práticas, padrões & técnicas para Arquitetura de Software, de maneira efetiva, com base em cenários reais para profissionais envolvidos no projeto e implantação de software.

× Precisa de ajuda?