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:

Minha opção para “vender” os meus serviços, bem como os da EximiaCo, sempre foi buscar o reconhecimento, no lugar do...
Are you interested to know more about the internals of the .NET Runtime? So you should spend some time reading...
In the previous post, I mentioned Mario Fusco who wrote a blog post series questioning the way Java developers are...
Tentando ser “Júnior” Minha carreira como amador remunerado em programação começou em 1992. Eu tinha quase 13 anos e era...
Um servidor de identidades é um artefato de sofware que centraliza os dados de usuários, bem como o processo para...
I wrote this post in 2016. Unfortunately, I lost it when I “rebooted” my blog. Anyway, I have a good...
× Precisa de ajuda?