No C# 8 ficou mais fácil alocar “arrays” na “Stack” e isso pode ter um impacto positivo tremendo na performance

Elemar Júnior

O operador stackalloc permite que aloquemos arrays na stack ao invés da heap. Dessa forma, esses arrays são descartados automaticamente quando a execução de um método se encerra (no momento do retorno), não gerando pressão para o garbage collector , podendo melhorar a performance de forma muito siginificativa.

Evolução do operador stackalloc ao longo dos anos

No passado, a utilização desse operador implicava que o trecho de código onde ele aparecia fosse marcado como unsafe – o que restringia bastante seu uso.

public static unsafe string StackAllocReverseString(string input)
{
    var reversedCharArray = stackalloc char[input.Length];
    for (
        int i = input.Length - 1, destIndex = 0;
        i >= 0;
        i--, destIndex++
    )
    {
        reversedCharArray[destIndex] = input[i];
    }
    return new string(reversedCharArray);
}

Entretanto, a partir do C# 7.2, com o advento dos tipos System.Span<T> e ReadOnlySpan<T> isso não é mais necessário. No C# 8, o operador pode ser utilizado, inclusive, em expressões aninhadas.

int length = 1000;
Span<byte> buffer = length <= 1024 ? stackalloc byte[length] : new byte[length];

O trecho de código acima mostra o nível de sofisticação atual dessa feature alocando de forma condicional o array na stack, se ele tiver menos do que 1024 posições, ou na heap.

Como stackalloc pode melhorar a performance do código

No passado, demonstramos como implementações ingênuas, que ignoram o impacto do GC, podem ter prejuízos significativos de performance. No código que segue, que valida CPFs, por exemplo, há muitas alocações desnecessárias.

public static bool ValidarCPF(string sourceCPF)
{
    if (String.IsNullOrWhiteSpace(sourceCPF))
        return false;

    string clearCPF;
    clearCPF = sourceCPF.Trim();
    clearCPF = clearCPF.Replace("-", "");
    clearCPF = clearCPF.Replace(".", "");

    if (clearCPF.Length != 11)
    {
        return false;
    }

    int[] cpfArray;
    int totalDigitoI = 0;
    int totalDigitoII = 0;
    int modI;
    int modII;

    if (clearCPF.Equals("00000000000") ||
        clearCPF.Equals("11111111111") ||
        clearCPF.Equals("22222222222") ||
        clearCPF.Equals("33333333333") ||
        clearCPF.Equals("44444444444") ||
        clearCPF.Equals("55555555555") ||
        clearCPF.Equals("66666666666") ||
        clearCPF.Equals("77777777777") ||
        clearCPF.Equals("88888888888") ||
        clearCPF.Equals("99999999999"))
    {
        return false;
    }

    foreach (char c in clearCPF)
    {
        if (!char.IsNumber(c))
        {
            return false;
        }
    }

    cpfArray = new int[11];
    for (int i = 0; i < clearCPF.Length; i++)
    {
        cpfArray[i] = int.Parse(clearCPF[i].ToString());
    }

    for (int posicao = 0; posicao < cpfArray.Length - 2; posicao++)
    {
        totalDigitoI += cpfArray[posicao] * (10 - posicao);
        totalDigitoII += cpfArray[posicao] * (11 - posicao);
    }

    modI = totalDigitoI % 11;
    if (modI < 2) { modI = 0; }
    else { modI = 11 - modI; }

    if (cpfArray[9] != modI)
    {
        return false;
    }

    totalDigitoII += modI * 2;

    modII = totalDigitoII % 11;
    if (modII < 2) { modII = 0; }
    else { modII = 11 - modII; }
    if (cpfArray[10] != modII)
    {
        return false;
    }
    // CPF Válido!
    return true;
}

Essas alocações desnecessárias, em .NET, fatalmente, pressionam o GC fazendo com que a performance fique deteriorada. Na época, mostramos uma revisão desse código que eliminava todas as alocações na heap.

Na ocasião demonstramos, inclusive, como código aparentemente menos eficiente, que realiza muito mais processamento, muitas vezes é compensado pela utilização mais racional da memória – sendo menos ofensivo ao GC.

public struct Cpf
{
    private readonly string _value;

    public readonly bool EhValido;
    private Cpf(string value)
    {
        _value = value;

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

        var posicao = 0;
        var totalDigito1 = 0;
        var totalDigito2 = 0;
        var dv1 = 0;
        var dv2 = 0;

        bool digitosIdenticos = true;
        var ultimoDigito = -1;

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

                ultimoDigito = digito;
                if (posicao < 9)
                {
                    totalDigito1 += digito * (10 - posicao);
                    totalDigito2 += digito * (11 - posicao);
                }
                else if (posicao == 9)
                {
                    dv1 = digito;
                }
                else if (posicao == 10)
                {
                    dv2 = digito;
                }

                posicao++;
            }
        }

        if (posicao > 11)
        {
            EhValido = false;
            return;
        }

        if (digitosIdenticos)
        {
            EhValido = false;
            return;
        }

        var digito1 = totalDigito1 % 11;
        digito1 = digito1 < 2
            ? 0
            : 11 - digito1;

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

        totalDigito2 += digito1 * 2;
        var digito2 = totalDigito2 % 11;
        digito2 = digito2 < 2
            ? 0
            : 11 - digito2;

        EhValido = dv2 == digito2;
    }

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

    public override string ToString() => _value;
}

O problema da implementação que recomendamos, de qualquer forma, é que ela é muito diferente daquela que faz alocações desncessárias. Além disso, sob diversas perspectivas, é também muito mais difícil de entender.

A utilização do operador stackalloc abre a possibilidade para que escrevamos uma versão, também sem alocações, porém muito mais próxima da que tínhamos inicialmente.

public static bool ValidarCPF(string sourceCPF)
{
    static bool VerificaTodosValoresSaoIguais(ref Span<int> input)
    {
        for (var i = 1; i < 11; i++)
        {
            if (input[i] != input[0])
            {
                return false;
            }
        }

        return true;
    }

    if (string.IsNullOrWhiteSpace(sourceCPF))
        return false;

    Span<int> cpfArray = stackalloc int[11];
    var count = 0;
    
    foreach (var c in sourceCPF)
    {
        if (char.IsDigit(c))
        {
            if (count > 10)
            {
                return false;
            }
            cpfArray[count] = c - '0';
            count++;
        }
    }

    if (count != 11) return false;
    if (VerificaTodosValoresSaoIguais(ref cpfArray)) return false;


    var totalDigitoI = 0;
    var totalDigitoII = 0;
    int modI;
    int modII;

    for (var posicao = 0; posicao < cpfArray.Length - 2; posicao++)
    {
        totalDigitoI += cpfArray[posicao] * (10 - posicao);
        totalDigitoII += cpfArray[posicao] * (11 - posicao);
    }

    modI = totalDigitoI % 11;
    if (modI < 2) { modI = 0; }
    else { modI = 11 - modI; }

    if (cpfArray[9] != modI)
    {
        return false;
    }

    totalDigitoII += modI * 2;

    modII = totalDigitoII % 11;
    if (modII < 2) { modII = 0; }
    else { modII = 11 - modII; }
    
    return cpfArray[10] == modII;
}

Aqui, também não ocorrem alocações e, consequentemente, não há pressão sobre o GC.

A nova versão, usando stackalloc, além de mais simples, é mais performática do que aquela com otimizações que apresentamos anteriormente.

Em tempo, as heurísticas de acionamento do GC melhoraram visivelmente nos últimos tempos fazendo com que o tempo de execução da implementação ingênua caísse pela metade (comparação com os resultados obtidos no post onde a experiência foi feita originalmente).

Em resumo

O fato
O Garbage Collector pode impactar negativamente a performance das aplicações. Por isso, é importante que nossos códigos evitem alocações desnecessárias na heap.
A novidade
C# 8 tornou ainda mais fácil a utilização do operador “stackalloc”. Com ele, podemos armazenar arrays na stack (no lugar da heap), diminuindo significativamente a pressão no GC.
Os benefícios
O operador “stackalloc” autoriza a aplicação de otimizações sem que tenhamos que recorrer a mudanças muito profundas na “lógica” do código. Dessa forma, fica mais barato obter e manter código de alta performance.

Compartilhe este insight:

Comentários

Participe deixando seu comentário sobre este artigo a seguir:

Subscribe
Notify of
guest
1 Comentário
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Carlos Eduardo Olivieri
Carlos Eduardo Olivieri
2 anos atrás

Muito obrigado pelo post. Sempre agregando algo de valor.

AUTOR

Elemar Júnior
Fundador e CEO da EximiaCo atua como tech trusted advisor ajudando empresas e profissionais a gerar mais resultados através da tecnologia.

NOVOS HORIZONTES PARA O SEU NEGÓCIO

Nosso time está preparado para superar junto com você grandes desafios tecnológicos.

Entre em contato e vamos juntos utilizar a tecnologia do jeito certo para gerar mais resultados.

Insights EximiaCo

Confira os conteúdos de negócios e tecnologia desenvolvidos pelos nossos consultores:

Arquitetura de Dados

Insights de um DBA na análise de um plano de execução

Especialista em performance de Bancos de Dados de larga escala
Arquitetura de Software

Estratégias para modernização do legado

Desenvolvedor .NET/NodeJs e especialista em Kafka com experiência em startups e grandes empresas
Infraestrutura e Nuvem

Migração para a nuvem, mais do que mudança tecnológica, implica em mudança da cultura organizacional

Engenheiro de nuvem, arquiteto de software e especialista em Containers e Devops

Acesse nossos canais

Simplificamos, potencializamos e aceleramos resultados usando a tecnologia do jeito certo

EximiaCo 2022 – Todos os direitos reservados

1
0
Queremos saber a sua opinião, deixe seu comentáriox
()
x

No C# 8 ficou mais fácil alocar “arrays” na “Stack” e isso pode ter um impacto positivo tremendo na performance

Para se candidatar nesta turma aberta, preencha o formulário a seguir:

Condição especial de pré-venda: R$ 14.000,00 - contratando a mentoria até até 31/01/2023 e R$ 15.000,00 - contratando a mentoria a partir de 01/02/2023, em até 12x com taxas.

Tenho interesse nessa capacitação

Para solicitar mais informações sobre essa capacitação para a sua empresa, preencha o formulário a seguir:

Tenho interesse em conversar

Se você está querendo gerar resultados através da tecnologia, preencha este formulário que um de nossos consultores entrará em contato com você:

O seu insight foi excluído com sucesso!

O seu insight foi excluído e não está mais disponível.

O seu insight foi salvo com sucesso!

Ele está na fila de espera, aguardando ser revisado para ter sua publicação programada.

Tenho interesse em conversar

Se você está querendo gerar resultados através da tecnologia, preencha este formulário que um de nossos consultores entrará em contato com você:

Tenho interesse nessa solução

Se você está procurando este tipo de solução para o seu negócio, preencha este formulário que um de nossos consultores entrará em contato com você:

Tenho interesse neste serviço

Se você está procurando este tipo de solução para o seu negócio, preencha este formulário que um de nossos consultores entrará em contato com você:

× Precisa de ajuda?