Será que o Regex é rápido?

19 minuto(s) de leitura - June 13, 2023

01

Fala pessoal, tudo bem?!

Curiosidade leva nos sempre a pensar fora da caixa!!!

No artigo SNAKE CASE eu usei um REGEX para aplicar a nomenclatura snake-case em uma string, mas hoje domingão (madrugada ainda rss), fiquei pensando de quanto performático era esse método.


Certo, e?

Pois bem, fiquei inquieto e comecei a escrever alguns bits na tentativa de descobrir o que seria melhor em um ambiente onde eu precisaria processar milhares ou milhões de dados, então cheguei a construir alguns métodos, para garantir a performance em um ambiente crítico, onde pode acontecer milhares ou milhões de interações por segundos.

Eu sou fã do projeto Newtonsoft por muito tempo ele entregou com maestria o que prometia, então fui estudar um pouco os fontes dele e me deparei com isso AQUI, então percebi que ele teve uma estratégia para aumentar a performance na serialização, eu poderia copiar esse método dele e fazer os testes que eu queria, mas, a minha experiência não seria a mesma, isso me motivou a criar alguns métodos que estão aqui neste pequeno artigo.


Vamos começar?!
Veja os métodos que foram utilizados nos testes que executei.

Método usando Regex

Esse foi o método usado no artigo citado acima. Simples e objetivo usando todo poder do Regex!

public string ToSnakeCaseUsingRegex()
{
    return Regex
        .Replace(
            _frase, 
            @"([a-z0-9])([A-Z])", 
            "$1_$2", 
            RegexOptions.Compiled)
        .ToLower();
}

Método usando LINQ

Não poderia passar por aqui sem falar da importancia do LINQ dentro do ecosistema .NET, sem sombra de dúvidas é uma das melhores implementações dentro da plataforma. O LINQ é simplesmente fantástico, pode ser usado praticamente pra manipular qualquer informação, eu te amo LINQ.
public string ToSnakeCaseUsingLinq()
{
    return string
        .Concat(_frase
            .Select((c, i) =>
                i > 0 && char.IsUpper(c) 
                    ? "_" + c 
                    : c.ToString()))
                .ToLower();
}

Método usando StringBuilder e Span

Aqui vamos começar a brincadeira com essa nova implementação do .NET, o SPAN, quando se fala em gerenciamento de memória, lembre-se desse cara, nesse exemplo iremos fazer uma pequena mesclagem, com SPAN e o StringBuilder para empilhar temporariamente nossos caracteres.
public string ToSnakeCaseUsingStringBuildAndSpan()
{
    ReadOnlySpan<char> frase = _frase;

    var stringBuilder = new StringBuilder();

    for (var i = 0; i < frase.Length; i++)
    {
        if (char.IsUpper(frase[i]) && frase[0] != frase[i])
        {
            stringBuilder.Append('_');
            stringBuilder.Append(frase[i]);
        }
        else
        {
            stringBuilder.Append(frase[i]);
        }
    }

    return stringBuilder
        .ToString()
        .ToLower();
}

Método usando somente Span

Aqui vamos usar o SPAN, só que a única diferença é que mudei a estratégia e estou usando um buffer para mover a posição dos caracteres.
 public string ToSnakeCaseUsingSpanOnBuffer()
{
    var undescores = 0;

    for (var i = 0; i < _frase.Length; i++)
    {
        if (char.IsUpper(_frase[i]))
        {
            undescores++;
        }
    }

    var length = (undescores + _frase.Length) - 1;
    Span<char> buffer = new char[length];
    var possitionOfBuffer = 0;
    var letterPosition = 0;

    while (possitionOfBuffer < buffer.Length)
    {
        if (letterPosition > 0 && char.IsUpper(_frase[letterPosition]))
        {
            buffer[possitionOfBuffer] = '_';
            buffer[possitionOfBuffer + 1] = _frase[letterPosition];
            possitionOfBuffer += 2;
            letterPosition++;
            continue;
        }

        buffer[possitionOfBuffer] = _frase[letterPosition];

        possitionOfBuffer++;
        letterPosition++;
    }

    return buffer
        .ToString()
        .ToLower();
}

Resultado dos testes

Os testes foram realizando com 10, 100.000 e 1.000.000 de interações.

-----------------------------------------------------------------
UsingStringBuilderAndSpan 10              Tempo: 00:00:00.0025257
                          100_000         Tempo: 00:00:00.1162287
                          1_000_000       Tempo: 00:00:02.0734867
-----------------------------------------------------------------
UsingSpanOnBuffer         10              Tempo: 00:00:00.0004815
                          100_000         Tempo: 00:00:00.1087459
                          1_000_000       Tempo: 00:00:01.0008935
-----------------------------------------------------------------
UsingRegex                10              Tempo: 00:00:00.0606576
                          100_000         Tempo: 00:00:00.5761851
                          1_000_000       Tempo: 00:00:06.2264039
-----------------------------------------------------------------
UsingLinq                 10              Tempo: 00:00:00.0089215
                          100_000         Tempo: 00:00:00.2520451
                          1_000_000       Tempo: 00:00:02.7254307
-----------------------------------------------------------------
Podemos observar que o REGEX teve a pior performance aqui, na verdade quando o assunto foi processar muita informação, ele foi péssimo, nossa mais de 6 segundos, em um ambiente de produção e crítico escorre até lágrimas dos olhos, já o LINQ me surpreendeu novamente mostrando que ainda é muito eficiente em cenários críticos, os métodos implementados usando SPAN tiveram a melhor performance.


Benchmark

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
Intel Core i7-7500U CPU 2.70GHz (Kaby Lake), 1 CPU, 4 logical and 2 physical cores
.NET Core SDK=3.1.100
  [Host]     : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT  [AttachedDebugger]
  DefaultJob : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT


|                             Method |       Mean |     Error |    StdDev |     Median | Rank |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|----------------------------------- |-----------:|----------:|----------:|-----------:|-----:|-------:|------:|------:|----------:|
|       ToSnakeCaseUsingSpanOnBuffer |   387.2 ns |   6.99 ns |   6.54 ns |   387.0 ns |    1 | 0.1831 |     - |     - |     384 B |
| ToSnakeCaseUsingStringBuildAndSpan |   473.0 ns |   9.42 ns |  16.00 ns |   469.5 ns |    2 | 0.3328 |     - |     - |     696 B |
|               ToSnakeCaseUsingLinq | 1,763.3 ns |  69.64 ns | 198.68 ns | 1,680.1 ns |    3 | 0.7839 |     - |     - |    1640 B |
|              ToSnakeCaseUsingRegex | 5,438.9 ns | 193.14 ns | 531.95 ns | 5,292.9 ns |    4 | 1.1520 |     - |     - |    2416 B |
Podemos observar que o REGEX novamente teve a pior performance aqui, chegando a alocar mais de 2K na memória, enquanto o LINQ alocou apenas sua metade, e os métodos que usamos SPAN teve o melhor comportamento, alocando muito menos memória.


Um pouco sobre Span

O Span é uma struct, uma nova feature do .NET, o objetivo principal do team do .NET ter escrito é, diminuir o impacto na memória gerenciada, a heap, e, para eu não ser muito redundante tem um artigo muito legal do Stephen Toub falando mais sobre o Span, é basicamente o Deep-Dive dentro do Span.
Fica dica de leitura: SPAN by Stephen Toub.


O que aprendemos com isso?

Aprendemos que mesmo que o .NET já nos forneça uma pilha de bibliotecas, com métodos quase prontos, não se acomode, em vez disso teste e analise seu cenário, quanto mais crítico ele for, mais a necessidade de performance você terá, pense fora da caixa, muitas das vezes você escrever seus próprios métodos pode ser a melhor opção.



Fico por aqui e um forte abraço! 😄

#mvpbuzz #mvpbr #mvp #developerssergipe #share #vscode #performance #efcore7 #netcore7 #span

Deixe um comentário