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.

Mais posts da série “Cases de Melhoria de Performance”:

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *