Comparando segmentos de strings de forma mais performática, e sem grandes mudanças

A cada nova release do .NET, novos recursos são adicionados e aprimorados, e após a inclusão da estrutura Span<T> e ReadOnlySpan<T>, manipular objetos alocados em memória se tornou mais simples e seguro, alinhando performance e legibilidade.

Utilizar o método Slice da estrutura ReadOnlySpan<T> em operações que envolvam strings pode apresentar resultados de performance significativamente melhores se comparado à utilização do tradicional método Substring. O impacto se torna ainda mais perceptível quando implementado em caminhos críticos de aplicações que demandam alta performance.

Às vezes custa caro seguir pelo caminho mais simples

Imagine um cenário onde é necessário incluir uma regra no código que identifique que uma palavra informada pelo usuário é similar à outra palavra já cadastrada. De forma mais detalhada, a validação deve garantir que a palavra informada não tenha 5 caracteres consecutivos iguais aos da palavra cadastrada.

A implementação mais comum em situações onde é necessário extrair uma parte da string é utilizando o método Substring. Seguindo essa ideia, para atender a regra proposta, é necessário navegar pelas partes da string informada pelo usuário e verificar se a palavra cadastrada possui esses segmentos através do método Contains. Se encontrar o segmento, o método retorna true, indicando que encontrou similaridade entre as palavras.

public bool UsingSubstring(string registeredWord, string inputWord)
{
    var quantityOfCharactersInSequenceAllowed = 5;

    var iterationCount = inputWord.Length - quantityOfCharactersInSequenceAllowed;

    for (int i = 0; i <= iterationCount; i++)
    {
        var innerText = inputWord.Substring(i, quantityOfCharactersInSequenceAllowed);
        if (registeredWord.Contains(innerText, StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }
    }

    return false;
}

Utilizar Substring é simples, porém gera ofensores de performance:

  1. Uma nova string é criada para armazenar a substring retornada pelo método, gerando alocação na memória heap
  2. É realizada uma cópia do conjunto de caracteres da string original para a nova string de acordo com o intervalo informado como parâmetro

Em caminhos críticos, essa abordagem pode se tornar um problema, pois uma nova string é alocada a cada iteração, e com mais alocação de memória, mais vezes o garbage collector será acionado, demandando mais processamento e interrompendo com mais frequência a execução das threads, impactando negativamente a performance.

Quando pequenas mudanças apresentam grandes resultados

A partir do C# 7.2 é possível utilizar a estrutura Span<T>, na qual fornece a representação contígua de uma região de memória de forma segura, permitindo a interação com os elementos diretamente na memória. Sendo assim, é possível acessar e alterar o valor dos elementos sem a necessidade de cópia e alocação em um novo objeto.

No caso da string, por se tratar de um objeto imutável, é necessário utilizar a estrutura ReadOnlySpan<T>,  que fornece somente recursos de acesso aos valores. Aplicando em nossa regra, precisamos realizar dois ajustes se comparado ao método que utiliza Substring

  1. Converter as strings recebidas como parâmetro em ReadOnlySpan<T>
  2. Substituir o método Substring por Slice
public bool UsingSpanSlice(string registeredWord, string inputWord)
{
    var quantityOfCharactersInSequenceAllowed = 5;

    var iterationCount = inputWord.Length - quantityOfCharactersInSequenceAllowed;

    ReadOnlySpan<char> textSpan = registeredWord;
    ReadOnlySpan<char> innerTextSpan = inputWord;
    for (int i = 0; i <= iterationCount; i++)
    {
        var innerTextSlice = innerTextSpan.Slice(i, quantityOfCharactersInSequenceAllowed);
        if (textSpan.Contains(innerTextSlice, StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }
    }

    return false;
}

Ao utilizar o método Slice em conjunto com a estrutura ReadOnlySpan<T>, eliminamos os dois ofensores de performance observados anteriormente:

  1. O retorno do método Slice é outro objeto do tipo ReadOnlySpan<T>, no qual é um ref struct, e objetos desse tipo são alocados na memória stack
  2. De acordo com o intervalo informado como parâmetro, os caracteres retornados pelo método Slice são referências dos mesmos caracteres utilizados na string original, sendo um apontamento para o local de memória onde eles estão armazenados

Aplicar essa técnica permite alcançar melhor performance afetando “pouco” a legibilidade. Se torna ainda mais atrativa em cenários que demandam alta performance, pois por não gerar alocação na memória heap, o garbage collector não é acionado, economizando recursos de processamento e evitando a interrupção da execução de threads.

Medir, medir, medir, sempre medir

Considerando o tempo de execução e a memória consumida, vamos avaliar o impacto de cada implementação através da comparação realizada pela biblioteca BenchmarkDotNet:

[MemoryDiagnoser]
public class SubstringVsSpan
{
    private string _registeredWord;
    private string _inputWord;
    private int _quantityOfCharactersInSequenceAllowed;

    [GlobalSetup]
    public void Setup()
    {
        _registeredWord = "RegisteredStringToBeCompared";
        _inputWord = "InputSendedByUserWithSimilarString";
        _quantityOfCharactersInSequenceAllowed = 5;
    }

    [Benchmark]
    public bool UsingSubstring()
    {
        var iterationCount = _inputWord.Length - _quantityOfCharactersInSequenceAllowed;

        for (int i = 0; i <= iterationCount; i++)
        {
            var innerTextSubstring = _inputWord.Substring(i, _quantityOfCharactersInSequenceAllowed);
            if (_registeredWord.Contains(innerTextSubstring, StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }
        }

        return false;
    }

    [Benchmark]
    public bool UsingSpanSlice()
    {
        var iterationCount = _inputWord.Length - _quantityOfCharactersInSequenceAllowed;

        ReadOnlySpan<char> textSpan = _registeredWord;
        ReadOnlySpan<char> innerTextSpan = _inputWord;
        for (int i = 0; i <= iterationCount; i++)
        {
            var innerTextSlice = innerTextSpan.Slice(i, _quantityOfCharactersInSequenceAllowed);
            if (textSpan.Contains(innerTextSlice, StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }
        }

        return false;
    }
}

Com o exemplo apresentado, utilizar o método Slice da estrutura ReadOnlySpan<T> não só se mostrou mais rápido, como também não gerou qualquer alocação de memória, enquanto o método Substring alocou 928 bytes e levou aproximadamente 15% a mais de tempo para ser executado. 

* A quantidade de memória alocada varia de acordo com o tamanho da string informada pelo usuário

Em um cenário de alta escala, onde essa validação é executada intensivamente, o impacto negativo ao utilizar Substring se torna ainda mais perceptível devido à pressão gerada sobre o garbage collector.

Ponderação sobre performance X legibilidade

Escrever código que performa melhor pode demandar maior complexidade, comprometendo a legibilidade e consequentemente a manutenibilidade da aplicação. Entretanto, em alguns casos é possível alcançar melhor performance, causando pouco ou nenhum impacto à legibilidade. 

No cenário apresentado, bastou realizar pequenas mudanças para se obter um código que performasse melhor, porém, ao utilizar o ReadOnlySpan<T>, é possível que a manutenção do código tenha se tornado mais complexa devido à inclusão de uma estrutura relativamente nova que ainda não é totalmente difundida. Apesar de parecer ser uma boa escolha padrão por conta da aparente simplicidade, cada caso é um caso, sendo necessário avaliar o trade-off e priorizar o que é mais importante para o sistema.

Compartilhe este insight:

Comentários

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

Subscribe
Notify of
guest
0 Comentários
Inline Feedbacks
View all comments

AUTOR

Raphael Castilho
Desenvolvedor especialista em .NET com experiência em aplicações corporativas de larga escala

INSIGHTS EXIMIACO

Desenvolvimento de Software

Gestão de times para acelerar entregas e atuar na resolução de problemas complexos.

Código .NET de Alta Performance

Um guia prático para a escrita de sistemas que executam no melhor tempo consumindo menos recursos.
Tech Trusted Advisor, Fundador e CEO da EximiaCo

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:

Desenvolvimento de Software

Maximizando a Eficiência: O Que Seu Código Está Escondendo?

Arquiteto de Software especializado em desenvolver soluções de alta escalabilidade e performance
Desenvolvimento de Software

A Importância da Proposta de Valor na Modernização de Sistemas Legados

Especialista em Comunicação Digital, UX/UI e Design Thinking
Desenvolvimento de Software

Construindo Produtos Inteligentes, Atraentes e Fáceis de Usar

Especialista em Comunicação Digital, UX/UI e Design Thinking
0
Queremos saber a sua opinião, deixe seu comentáriox

A sua inscrição foi realizada com sucesso!

O link de acesso à live foi enviado para o seu e-mail. Nos vemos no dia da live.

Muito obrigado!

Deu tudo certo com seu envio!
Logo entraremos em contato

Comparando segmentos de strings de forma mais performática, e sem grandes mudanças

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

Comparando segmentos de strings de forma mais performática, e sem grandes mudanças

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?