Olá!
Este é mais um post da seção Dica Rápida e, desta vez, falaremos sobre uma ferramenta muito útil para avaliar o desempenho de nosso código: o BenchmarkDotNet.
O BenchmarkDotNet é uma biblioteca disponível no NuGet que, uma vez adicionada ao seu projeto, te permite executar testes de desempenho de forma bem semelhante à de testes de unidade.
Vamos fazer um pequeno teste. Para isso, crie um novo projeto do tipo Console
chamado Lab.Benchmark.Linq
, adicione a biblioteca BenchmarkDotNet e, em seguida, crie um arquivo chamado LinqBenchmark.cs, e adicione o seguinte conteúdo:
using BenchmarkDotNet.Attributes;
using System;
using System.Linq;
namespace Lab.Benchmark.Linq
{
public class LinqBenchmark
{
private const int capacity = 1000;
private const int slice = 500;
private int[] numbers = new int[capacity];
[GlobalSetup]
public void Setup()
{
for (int i = 0; i < capacity; i++)
numbers[i] = i;
}
[Benchmark]
public int LinqWhereAndFirst() =>
numbers.Where(x => x > slice).First();
[Benchmark]
public int LinqFirst() =>
numbers.First(x => x > slice);
[Benchmark(Baseline=true)]
public int NoLinq()
{
for (int i = 0; i < numbers.Length; i++)
if (numbers[i] > slice)
return numbers[i];
throw new InvalidOperationException();
}
}
}
Vamos verificar o conteúdo deste arquivo. Repare que temos um atributo chamado GlobalSetup
sobre o método Setup
. Este atributo indica que antes da execução de qualquer benchmark este método será executado.
Em seguida, temos métodos marcados com o atributo Benchmark
, que indica que se trata de um método de medição (da mesma forma que um método de teste no XUnit, por exemplo, teria um atributo Fact
ou Theory
). No último método temos, além do atributo Benchmark
, a propriedade Baseline
com value true
. Isso significa que este será o baseline de nossa medição, ou seja, o ponto de referência para as demais execuções. Este atributo é opcional, e adiciona o número de vezes que o baseline poderia ser executado no tempo de execução dos demais métodos.
Com isto, temos o mínimo necessário para executar nosso benchmark.
Vamos agora ao arquivo Program.cs, e substituir o conteúdo original pelo seguinte:
using BenchmarkDotNet.Running;
namespace Lab.Benchmark.Linq
{
static class Program
{
static void Main(string[] args)
{
BenchmarkRunner.Run<LinqBenchmark>();
}
}
}
Aqui temos apenas uma chamada para o método Run
do executor de benchmarks, especificando o tipo LinqBenchmark
que criamos acima.
Para executar o benchmark, basta configurar o build do projeto para Release
, e executar este programa sem habilitar o debugger (Ctrl +F5).
Se tudo deu certo, o resultado será semalhante ao da imagem abaixo.
E assim temos um teste de desempenho de três implementações distintas de um mesmo algoritmo.
Para maiores informações sobre a biblioteca, recomendo sua documentação no Github (em inglês).
Até a próxima!
Top comments (2)
Muito legal! Agora, fiquei surpreso com o resultado, imaginava que o tempo do First e Where.First seriam próximos. Qual é o motivo da diferença?
Fala, Rafa! Tudo bom?
Gostou da pegadinha. Né? rs
A diferença está na implementação dos métodos
Where
eFirst
(com predicado). O métodoWhere
itera viaforeach
na coleção e faz umyeld return
de cada item que corresponda ao predicado. Essa implementação é rápida porque é traduzida pelo compilador como umfor
, já quenumbers
é um array, e, em seguida, o métodoFirst
(sem predicado) apenas retorna o primeiro item doIEnumerator<T>
obtido junto à coleção.Já no caso do método
First
(com predicado), a implementação é diferente: há uma tentativa de converter a coleção para umIList<T>
e, sendo possível, é retornado o primeiro item da lista. Não sendo possível, ou seja, se a coleção não implementaIList<T>
, é obtido umIEnumerator<T>
da coleção viaGetEnumerator
, e então ocorre a iteração pela coleção viaIEnumerator<T>.MoveNext
até que seja encontrado um item que atenda ao predicado. Essa implementação é menos eficiente que a doWhere
e, por isso, o tempo de resposta é tão discrepante.Maluco. Né?
Abração!