[{"content":"Você cercou o método com um Stopwatch, rodou uma vez e anotou o número. Parabéns: você provavelmente mediu o JIT compilando, o Garbage Collector respirando e o cache da CPU do jeito que ele estava naquele milissegundo. Não o seu código.\nMedir performance em .NET é mais traiçoeiro do que parece, e quase todo benchmark caseiro mede a coisa errada. Veja um exemplo que parece inofensivo:\n1 2 3 4 5 6 7 8 9 10 11 12 var numeros = Enumerable.Range(0, 1_000_000).ToArray(); var sw = Stopwatch.StartNew(); var soma1 = numeros.Sum(); sw.Stop(); Console.WriteLine($\u0026#34;LINQ: {sw.ElapsedMilliseconds}ms\u0026#34;); sw.Restart(); long soma2 = 0; for (int i = 0; i \u0026lt; numeros.Length; i++) soma2 += numeros[i]; sw.Stop(); Console.WriteLine($\u0026#34;for: {sw.ElapsedMilliseconds}ms\u0026#34;); Rode três vezes. Às vezes o LINQ ganha, às vezes o for, às vezes empatam. Qual dos dois é mais rápido? A resposta honesta: você não sabe, e esse código nunca vai te dizer. A pergunta desconfortável que fica é: como você tem tanta certeza de que os seus benchmarks estão certos?\nPor que o Stopwatch mente Quando você roda um trecho de código em C#, várias coisas acontecem nos bastidores que distorcem qualquer medição ingênua. Os quatro principais vilões:\nO JIT compila na primeira execução C# não roda direto na CPU. Seu código é compilado para IL (linguagem intermediária), e só quando um método é chamado pela primeira vez o JIT (Just-In-Time compiler) o traduz para código de máquina. Ou seja: a primeira execução paga o preço da compilação.\nVeja na prática:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 long Soma(int n) { long total = 0; for (int i = 0; i \u0026lt; n; i++) total += i; return total; } var sw = Stopwatch.StartNew(); Soma(1_000_000); sw.Stop(); Console.WriteLine($\u0026#34;1ª execução: {sw.Elapsed.TotalMicroseconds:F0}µs\u0026#34;); sw.Restart(); Soma(1_000_000); sw.Stop(); Console.WriteLine($\u0026#34;2ª execução: {sw.Elapsed.TotalMicroseconds:F0}µs\u0026#34;); Na minha máquina:\n1 2 1ª execução: 1842µs 2ª execução: 312µs Mesmo código, quase 6x de diferença. Se o seu benchmark mede a primeira execução, ele está medindo o JIT, não o seu algoritmo.\nO Garbage Collector aparece sem avisar Se uma coleta de lixo acontecer no meio da sua medição, ela adiciona uma pausa que não tem nada a ver com o código que você está testando. Pior: o GC é disparado pelo estado geral da memória do processo, inclusive pelo lixo que outro trecho do seu programa gerou antes. Duas execuções idênticas podem ter resultados completamente diferentes só por causa disso.\nCache da CPU e estado da máquina A primeira vez que você percorre um array, ele vem da RAM. Na segunda, boa parte já está no cache L1/L2 da CPU, que é ordens de magnitude mais rápido. Some a isso o Windows decidindo indexar arquivos, o Chrome com 40 abas abertas, o antivírus escaneando algo\u0026hellip; uma medição única é praticamente um número aleatório.\nO compilador apaga código \u0026ldquo;inútil\u0026rdquo; Esse é o mais sorrateiro. O JIT é agressivo em otimização, e se ele percebe que o resultado de um cálculo nunca é usado, ele pode simplesmente eliminar o cálculo inteiro (dead code elimination):\n1 2 3 4 5 6 7 var sw = Stopwatch.StartNew(); for (int i = 0; i \u0026lt; 1_000_000; i++) { var x = Math.Sqrt(i); // resultado nunca usado } sw.Stop(); // Parabéns, você mediu um loop vazio. Você acha que mediu um milhão de raízes quadradas. Na prática, pode ter medido nada.\nO jeito certo: BenchmarkDotNet A boa notícia: você não precisa resolver nenhum desses problemas na mão. O BenchmarkDotNet é a biblioteca padrão da comunidade .NET para isso: é a mesma que o time do .NET usa para medir o próprio runtime. Ela faz warm-up (aquece o JIT), roda centenas de iterações, isola o processo, trata estatisticamente os resultados e ainda impede o dead code elimination (desde que você retorne o resultado do método).\nSetup Crie um projeto console e instale o pacote:\n1 2 3 dotnet new console -n MeuBenchmark cd MeuBenchmark dotnet add package BenchmarkDotNet O primeiro benchmark Vamos medir um clássico: concatenar strings com += versus StringBuilder. Todo mundo \u0026ldquo;sabe\u0026rdquo; que o StringBuilder é mais rápido. Mas quanto? E a partir de que tamanho isso importa? Benchmark serve exatamente para trocar achismo por número.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; using System.Text; BenchmarkRunner.Run\u0026lt;ConcatBenchmark\u0026gt;(); [MemoryDiagnoser] public class ConcatBenchmark { [Params(100, 1000)] public int N; [Benchmark(Baseline = true)] public string WithPlus() { var s = \u0026#34;\u0026#34;; for (int i = 0; i \u0026lt; N; i++) s += \u0026#34;x\u0026#34;; return s; } [Benchmark] public string WithStringBuilder() { var sb = new StringBuilder(); for (int i = 0; i \u0026lt; N; i++) sb.Append(\u0026#34;x\u0026#34;); return sb.ToString(); } } Três detalhes importantes nesse código:\n[MemoryDiagnoser] faz a biblioteca medir também as alocações de memória. Sempre use. Você vai ver por quê já já. [Params(100, 1000)] roda cada benchmark com os dois valores de N, mostrando como cada abordagem escala. Os métodos retornam o resultado. Isso impede o JIT de apagar o código que estamos medindo. Rode em modo Release (isso é obrigatório: em Debug o JIT não otimiza e os números não valem nada):\n1 dotnet run -c Release Os resultados Depois de um ou dois minutos rodando, o BenchmarkDotNet imprime primeiro o ambiente da medição:\n1 2 3 4 5 BenchmarkDotNet v0.15.8, macOS Tahoe 26.5.1 (25F80) [Darwin 25.5.0] Apple M4 Pro, 1 CPU, 12 logical and 12 physical cores .NET SDK 10.0.201 [Host] : .NET 10.0.5 (10.0.5, 10.0.526.15411), Arm64 RyuJIT armv8.0-a DefaultJob : .NET 10.0.5 (10.0.5, 10.0.526.15411), Arm64 RyuJIT armv8.0-a \u0026hellip;e a tabela com os resultados:\nMethod N Mean Error StdDev Ratio Gen0 Gen1 Allocated Alloc Ratio WithPlus 100 823.8 ns 14.39 ns 12.75 ns 1.00 1.5030 - 12576 B 1.00 WithStringBuilder 100 111.9 ns 1.49 ns 1.32 ns 0.14 0.0918 0.0001 768 B 0.06 WithPlus 1000 39,210.8 ns 188.15 ns 166.79 ns 1.00 122.6196 0.6104 1025976 B 1.00 WithStringBuilder 1000 741.5 ns 7.04 ns 5.88 ns 0.02 0.5465 0.0048 4576 B 0.004 (Os números exatos variam por máquina e versão do .NET. Rode na sua e compare. Estes são de um Apple M4 Pro, .NET 10.)\nLendo a tabela como gente grande Mean é a média do tempo por operação. É o número principal. Error e StdDev indicam a confiabilidade da medição. Se o StdDev for grande em relação ao Mean, os resultados oscilaram muito. Desconfie (feche o Chrome e rode de novo). Ratio compara com o baseline. WithStringBuilder com N=1000 mostra 0.02: ele leva 2% do tempo do +=, ou seja, é ~53x mais rápido. Allocated é onde mora o insight escondido. Com N=1000, o += alocou ~1 MB (1.025.976 bytes) para construir uma string de mil caracteres. Por quê? Strings em .NET são imutáveis: cada += cria uma string nova e copia todo o conteúdo anterior. São mil strings intermediárias viradas lixo, que o GC vai ter que coletar depois, cobrando esse preço em outro lugar do seu programa. Repare no padrão de escala: multiplicamos N por 10, e o tempo do += multiplicou por ~48, enquanto o do StringBuilder multiplicou por ~6,6. Isso é a diferença entre um algoritmo quadrático e um linear aparecendo nos dados reais, assunto que vamos formalizar no próximo artigo da série.\nE o Stopwatch, joga fora? Não. O Stopwatch continua sendo a ferramenta certa quando:\nA operação demora segundos ou mais (aí o ruído de microssegundos do JIT e do cache vira irrelevante); Você está medindo I/O: chamada de API, query no banco, leitura de arquivo; Você só quer uma noção grosseira durante o desenvolvimento. A regra de bolso: microssegundos ou nanossegundos → BenchmarkDotNet; segundos → Stopwatch está ótimo. O erro é usar Stopwatch para comparar duas implementações que rodam em microssegundos. Nessa escala, ele mede mais ruído do que código.\nRecapitulando Medição ingênua com Stopwatch em código rápido mede JIT, GC e cache, não o seu algoritmo. BenchmarkDotNet resolve isso com warm-up, estatística e isolamento; use [MemoryDiagnoser] sempre e rode em Release. Tempo não é a única métrica: alocação de memória hoje é pausa de GC amanhã. No próximo artigo da série, vamos usar essa ferramenta para tirar o Big O do quadro branco: busca linear vs busca binária, com números reais mostrando exatamente onde O(n) perde de O(log n). E onde, surpreendentemente, ele ganha.\nAté lá, fica a provocação: aquele benchmark que você usou para decidir uma arquitetura semana passada: será que ele estava certo? Rode os deste artigo na sua máquina e compare. Medir é um hábito: quanto antes começar, melhor.\n","date":"2026-07-01T00:00:00-03:00","image":"/posts/benchmark-csharp-benchmarkdotnet/cover.png","permalink":"/posts/benchmark-csharp-benchmarkdotnet/","title":"Seu benchmark em C# está certo? (e como consertar se não estiver)"}]