O benchmarking é uma técnica essencial no desenvolvimento de software para medir e comparar o desempenho de diferentes trechos de código. Ele permite identificar gargalos de desempenho e otimizar o código, garantindo que ele execute de maneira eficiente. O C# possui várias ferramentas e bibliotecas para realizar benchmarks de forma fácil e precisa, uma delas é a biblioteca BenchmarkDotNet.
O BenchmarkDotNet é uma poderosa biblioteca de benchmarking para o .NET que oferece recursos avançados, como medição precisa de tempo, configuração automática do ambiente de benchmark e geração de relatórios detalhados.
Exemplo de Utilização
Vamos ver um exemplo prático de como usar o BenchmarkDotNet para medir o desempenho da validação de CPF em C#.
#1 Configuração do Projeto
Crie um novo projeto de Console no Visual Studio ou em sua linha de comando preferida:
dotnet new console -n Benchmark
cd Benchmark
Em seguida, instale o pacote NuGet do BenchmarkDotNet:
dotnet add package BenchmarkDotNet
#2 Implementação da Validação de CPF
Agora, implementaremos a lógica de validação de CPF em duas versões.
Versão 1
Versão facilmente encontrada na internet e implementada em muitos projetos.
namespace Benchmark.Versoes
{
internal class CPFVersao1
{
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;
}
}
}
Versão 2
Está versão foi otimizada para redução do uso do Garbage Collection (GC), tendendo a diminuir o uso de memória e aumento da velocidade de execução.
namespace Benchmark.Versoes
{
internal class CPFVersao2
{
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;
}
public static bool ValidarCPF(Cpf sourceCPF) =>
sourceCPF.EhValido;
}
}
#3 Configuração do Benchmark
Agora, criaremos uma classe chamada ValidarCPFBenchmark
que conterá os métodos de benchmark para validar CPFs válidos e inválidos nas duas implementações. Configuramos para que a versão 1 seja a base de comparação e que os resultados sejam ordenados do mais rápido para o mais lento.
using Benchmark.Versoes;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Mathematics;
using BenchmarkDotNet.Order;
namespace Benchmark
{
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[RankColumn(NumeralSystem.Arabic)]
public class ValidarCPFBenchmark
{
[Params("12345678909", "11111111111")]
public string CPF { get; set; }
[Benchmark(Baseline = true)]
public void ValidarCPFVersao1()
{
CPFVersao1.ValidarCPF(CPF);
}
[Benchmark]
public void ValidarCPFVersao2()
{
CPFVersao2.ValidarCPF(CPF);
}
}
}
#4 Execução do Benchmark
No Program.cs
, executaremos o benchmark usando a classe BenchmarkRunner
do BenchmarkDotNet:
using Benchmark;
using BenchmarkDotNet.Running;
var summary = BenchmarkRunner.Run<ValidarCPFBenchmark>();
#5 Executar o Benchmark
No terminal, execute o o comando abaixo:
dotnet run --configuration Release
#6 Resultado do Benchmark e Significados
Após executar o exemplo, obteremos um resultado semelhante ao seguinte:
BenchmarkDotNet=v0.13.5, OS=Windows 10 (10.0.19045.3086/22H2/2022Update)
Intel Core i7-7700HQ CPU 2.80GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores
.NET SDK=7.0.203
[Host] : .NET 6.0.18 (6.0.1823.26907), X64 RyuJIT AVX2
DefaultJob : .NET 6.0.18 (6.0.1823.26907), X64 RyuJIT AVX2
| Method | CPF | Mean | Error | StdDev | Ratio | RatioSD | Rank |
|------------------ |------------ |----------:|---------:|---------:|------:|--------:|-----:|
| ValidarCPFVersao2 | 11111111111 | 30.83 ns | 0.210 ns | 0.186 ns | 0.93 | 0.02 | 1 |
| ValidarCPFVersao1 | 11111111111 | 33.08 ns | 0.678 ns | 0.807 ns | 1.00 | 0.00 | 2 |
| | | | | | | | |
| ValidarCPFVersao2 | 12345678909 | 32.74 ns | 0.164 ns | 0.153 ns | 0.13 | 0.00 | 1 |
| ValidarCPFVersao1 | 12345678909 | 254.31 ns | 3.838 ns | 3.590 ns | 1.00 | 0.00 | 2 |
Os resultados são apresentados em uma tabela, onde encontramos os seguintes significados:
Method
: Nome do método de benchmark.Mean
: Tempo médio de execução para o método em questão, expresso em unidades de tempo (por exemplo, microssegundos).Error
: Valor do erro absoluto para o tempo médio de execução.StdDev
: Desvio padrão para o tempo médio de execução.Ratio
: Indica a relação entre os tempos de execução e a referência.RatioSD
: Representa o desvio padrão da relação entre os tempos de execução e a referência.Rank
: Ordenação do mas lento para o mais rápido
Analise do Resultado
Podemos verificar que para o CPF Inválido, pela média de tempo, as duas implementações são próximas, mas quando comparamos um CPF válido, a diferença é muito grande, chegando a ser quase 8X mais rápido na média.
Neste exemplo analisamos apenas tempo de execução, mas a biblioteca traz uma infinidade de indicadores que podem ser utilizado na análise.
Lembre-se de que o benchmarking deve ser utilizado como uma ferramenta complementar no processo de otimização do código. É importante considerar fatores adicionais, como legibilidade, manutenibilidade e requisitos específicos do projeto, ao tomar decisões de otimização.
Espero que este exemplo tenha sido útil e que você possa aplicar o benchmarking em seus projetos para melhorar o desempenho do código em C#.