Escrevendo aplicações mais performáticas
Introdução
Vamos colocar a mão na massa!
Destrutores são um pesadelo para sua aplicação
As seguintes classes serão utilizadas como exemplos:
public class ClasseSemFinalizador
{
public int Id { get; set; }
public string Nome { get; set; }
}
public class ClasseComFinalizador
{
public int Id { get; set; }
public string Nome { get; set; }
~ClasseComFinalizador()
{
// Fazer algo
}
}
[MemoryDiagnoser]
public class PerformanceDestrutor
{
[Params(1_000, 10_000, 100_000)]
public int Size { get; set; }
[Benchmark]
public void ComFinalizador()
{
for (int i = 0; i < Size; i++)
{
var classe = new ClasseComFinalizador
{
Id = i,
Nome = "Teste"
};
}
}
[Benchmark]
public void SemFinalizador()
{
for (int i = 0; i < Size; i++)
{
var classe = new ClasseSemFinalizador
{
Id = i,
Nome = "Teste"
};
}
}
}
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
Intel Core i7-7500U CPU 2.70GHz (Kaby Lake), 1 CPU, 4 logical and 2 physical cores
.NET SDK=6.0.100-preview.6.21355.2
[Host] : .NET 5.0.8 (5.0.821.31504), X64 RyuJIT
DefaultJob : .NET 5.0.8 (5.0.821.31504), X64 RyuJIT
| Method | Size | Mean | Error | Gen 0 | Gen 1 | Allocated |
|--------------- |------- |--------------:|------------:|---------:|---------:|----------:|
| ComFinalizador | 1000 | 133.693 us | 2.6418 us | 3.4180 | 1.7090 | 31 KB |
| SemFinalizador | 1000 | 8.018 us | 0.1612 us | 3.4637 | - | 31 KB |
| ComFinalizador | 10000 | 1,333.029 us | 26.5482 us | 33.2031 | 15.6250 | 312 KB |
| SemFinalizador | 10000 | 76.591 us | 1.5304 us | 34.6680 | - | 312 KB |
| ComFinalizador | 100000 | 13,295.902 us | 262.2954 us | 343.7500 | 171.8750 | 3,125 KB |
| SemFinalizador | 100000 | 779.552 us | 15.4507 us | 347.6563 | - | 3,125 KB |
Concatenar string ou utilizar StringBuilder ?
[MemoryDiagnoser]
public class ManipularString
{
[Params(1, 100, 1000)]
public int Size { get; set; }
[Benchmark]
public string ConcatenacaoString()
{
var json = "[";
for (int i = 0; i <= Size; i++)
{
json += "{";
json += "\"id\"";
json += ":";
json += i;
json += "}";
}
json += "]";
return json;
}
[Benchmark]
public string StringBuilderString()
{
var json = new StringBuilder("[", 100);
for (int i = 0; i <= Size; i++)
{
json.Append("{").Append("\"id\"")
.Append(":").Append(i)
.Append("}");
}
json.Append("]");
return json.ToString();
}
}
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
Intel Core i7-7500U CPU 2.70GHz (Kaby Lake), 1 CPU, 4 logical and 2 physical cores
.NET SDK=6.0.100-preview.6.21355.2
[Host] : .NET 5.0.8 (5.0.821.31504), X64 RyuJIT
DefaultJob : .NET 5.0.8 (5.0.821.31504), X64 RyuJIT
| Method | Size | Mean | Gen 0 | Gen 1 | Allocated |
|-------------------- |----- |---------------:|----------:|---------:|-------------:|
| ConcatenacaoString | 1 | 268.8 ns | 0.0572 | - | 528 B |
| StringBuilderString | 1 | 150.5 ns | 0.0367 | - | 336 B |
| ConcatenacaoString | 100 | 78,626.9 ns | 51.0254 | 0.1221 | 469,184 B |
| StringBuilderString | 100 | 4,270.6 ns | 0.5875 | - | 5,392 B |
| ConcatenacaoString | 1000 | 8,968,043.9 ns | 5562.5000 | 109.3750 | 49,249,192 B |
| StringBuilderString | 1000 | 64,200.5 ns | 5.1880 | 0.2441 | 46,008 B |
Regex e suas armadilhas
O .NET nos oferece dois sabores de Regex, o interpretado e o compilado, vamos testar a performance de ambos, para isso iremos usar o seguinte cenário no qual precisamos saber se uma string contém números e para isso iremos usar o Regex, na imagem a seguir temos dois métodos um que utiliza uma instância do objeto Regex interpretado e outro que utiliza a instância do Regex Compilado os dois utilizam o mesmo pattern que é validar se existe números em uma string.
[MemoryDiagnoser]
public class PerformanceRegex
{
private const string _dados = "lUk*avdr!ZhbbNF^J7yxsGueVAufYC3ixB8vqt";
private const string _pattern = @"[0-9]";
private readonly Regex _regexNaoCompilado = new(_pattern);
private readonly Regex _regexCompilado = new(_pattern, RegexOptions.Compiled);
[Benchmark]
public void RegexNormal()
{
for (int i = 0; i < Size; i++)
{
_ = _regexNaoCompilado.IsMatch(_dados);
}
}
[Benchmark]
public void RegexCompilado()
{
for (int i = 0; i < Size; i++)
{
_ = _regexCompilado.IsMatch(_dados);
}
}
[Params(100, 1_000, 10_000)]
public int Size { get; set; }
}
Depois de executar os testes de performance obtemos o seguinte resultado:
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
Intel Core i7-7500U CPU 2.70GHz (Kaby Lake), 1 CPU, 4 logical and 2 physical cores
.NET SDK=6.0.100-preview.6.21355.2
[Host] : .NET 5.0.8 (5.0.821.31504), X64 RyuJIT
DefaultJob : .NET 5.0.8 (5.0.821.31504), X64 RyuJIT
| Method | Size | Mean | Error | StdDev |
|----------------- |------ |-----------------:|---------------:|---------------:|
| RegexNormal | 100 | 20.858 us | 0.4311 us | 1.2643 us |
| RegexCompilado | 100 | 7.929 us | 0.1889 us | 0.5570 us |
| RegexNormal | 1000 | 206.609 us | 4.1243 us | 8.1409 us |
| RegexCompilado | 1000 | 80.109 us | 1.5968 us | 4.3171 us |
| RegexNormal | 10000 | 2,125.230 us | 68.2312 us | 196.8627 us |
| RegexCompilado | 10000 | 799.817 us | 15.9848 us | 36.7277 us |
[Benchmark]
public void MetodoCustomizado()
{
for (int i = 0; i < Size; i++)
{
_ = ContemNumero(_dados.AsSpan());
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool ContemNumero(ReadOnlySpan<char> span)
{
for (var i = 0; i < span.Length; i++)
{
if (span[i] >= '0' && span[i] <= '9')
{
return true;
}
}
return false;
}
Executando os testes de performance novamente obtivemos o seguinte resultado:
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
Intel Core i7-7500U CPU 2.70GHz (Kaby Lake), 1 CPU, 4 logical and 2 physical cores
.NET SDK=6.0.100-preview.6.21355.2
[Host] : .NET 5.0.8 (5.0.821.31504), X64 RyuJIT
DefaultJob : .NET 5.0.8 (5.0.821.31504), X64 RyuJIT
| Method | Size | Mean | Error | StdDev |
|------------------ |------ |-----------------:|---------------:|---------------:|
| RegexNormal | 100 | 20.858 us | 0.4311 us | 1.2643 us |
| RegexCompilado | 100 | 7.929 us | 0.1889 us | 0.5570 us |
| MetodoCustomizado | 100 | 1.335 us | 0.0265 us | 0.0428 us |
| RegexNormal | 1000 | 206.609 us | 4.1243 us | 8.1409 us |
| RegexCompilado | 1000 | 80.109 us | 1.5968 us | 4.3171 us |
| MetodoCustomizado | 1000 | 16.470 us | 0.3225 us | 0.6137 us |
| RegexNormal | 10000 | 2,125.230 us | 68.2312 us | 196.8627 us |
| RegexCompilado | 10000 | 799.817 us | 15.9848 us | 36.7277 us |
| MetodoCustomizado | 10000 | 162.689 us | 3.1943 us | 4.5811 us |
[Benchmark]
public void RegexNormalInstanciado()
{
for (int i = 0; i < Size; i++)
{
var regex = new Regex(_pattern);
_ = regex.IsMatch(_dados);
}
}
[Benchmark]
public void RegexCompiladoInstanciado()
{
for (int i = 0; i < Size; i++)
{
var regex = new Regex(_pattern, RegexOptions.Compiled);
_ = regex.IsMatch(_dados);
}
}
Novamente depois de executar todos os testes obtivemos o seguinte resultado:
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
Intel Core i7-7500U CPU 2.70GHz (Kaby Lake), 1 CPU, 4 logical and 2 physical cores
.NET SDK=6.0.100-preview.6.21355.2
[Host] : .NET 5.0.8 (5.0.821.31504), X64 RyuJIT
DefaultJob : .NET 5.0.8 (5.0.821.31504), X64 RyuJIT
| Method | Size | Mean | Gen 0 | Gen 1 | Gen 2 | Allocated |
|-------------------------- |------ |-----------------:|----------:|----------:|----------:|-------------:|
| RegexNormal | 100 | 20.858 us | - | - | - | - |
| RegexCompilado | 100 | 7.929 us | - | - | - | - |
| MetodoCustomizado | 100 | 1.335 us | - | - | - | - |
| RegexNormalInstanciado | 100 | 253.201 us | 29.2969 | - | - | 268,000 B |
| RegexCompiladoInstanciado | 100 | 87,663.636 us | - | - | - | 776,800 B |
| RegexNormal | 1000 | 206.609 us | - | - | - | - |
| RegexCompilado | 1000 | 80.109 us | - | - | - | - |
| MetodoCustomizado | 1000 | 16.470 us | - | - | - | - |
| RegexNormalInstanciado | 1000 | 2,536.451 us | - | - |2,680,000 B| - |
| RegexCompiladoInstanciado | 1000 | 861,472.006 us | - | - | - | 7,768,000 B |
| RegexNormal | 10000 | 2,125.230 us | - | - | - | - |
| RegexCompilado | 10000 | 799.817 us | - | - | - | - |
| MetodoCustomizado | 10000 | 162.689 us | - | - | - | - |
| RegexNormalInstanciado | 10000 | 25,384.456 us | 2937.5000 | - | - | 26,800,000 B |
| RegexCompiladoInstanciado | 10000 | 8,872,594.393 us | 9000.0000 | 5000.0000 | 1000.0000 | 78,220,016 B |
Uma outra dica importante ao utilizar o Regex é aplicar Timeout dado que nossas expressões se beneficiam de retrocesso com objetivo de fazer otimização, para mais informações sobre retrocesso basta acessar: Microsoft retrocesso, o timeout garante que a expressão seja validada dentro de uma janela de tempo específica, se não for processada no intervalo especificado será lançada uma exception: RegexMatchTimeoutException.
Considerações
Na continuação deste artigo conheceremos alguns operadores e novas features do .NET que contribuem para alocação mínima de memória.
Contatos
twitter: @ralmsdeveloper
linkedin: @ralmsdeveloper
Deixe um comentário