DEV Community

loading...
Cover image for Dica Rápida: Testando desempenho com BenchmarkDotNet

Dica Rápida: Testando desempenho com BenchmarkDotNet

wsantosdev profile image William Santos ・2 min read

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();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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>();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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!

Discussion (2)

pic
Editor guide
Collapse
rafaelpadovezi profile image
Rafael

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?

Collapse
wsantosdev profile image
William Santos Author • Edited

Fala, Rafa! Tudo bom?
Gostou da pegadinha. Né? rs

A diferença está na implementação dos métodos Where e First (com predicado). O método Where itera via foreach na coleção e faz um yeld return de cada item que corresponda ao predicado. Essa implementação é rápida porque é traduzida pelo compilador como um for, já que numbers é um array, e, em seguida, o método First (sem predicado) apenas retorna o primeiro item do IEnumerator<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 um IList<T> e, sendo possível, é retornado o primeiro item da lista. Não sendo possível, ou seja, se a coleção não implementa IList<T>, é obtido um IEnumerator<T> da coleção via GetEnumerator, e então ocorre a iteração pela coleção via IEnumerator<T>.MoveNext até que seja encontrado um item que atenda ao predicado. Essa implementação é menos eficiente que a do Where e, por isso, o tempo de resposta é tão discrepante.

Maluco. Né?

Abração!