Introducción: por qué es importante la creación de perfiles
Cada desarrollador de C# se ha enfrentado a una situación en la que la aplicación funciona lentamente, pero no está claro por qué. Las conjeturas intuitivas suelen ser erróneas: el código que parece lento puede ser rápido, y las optimizaciones "obvias" pueden ser inútiles. Aquí es donde la creación de perfiles viene al rescate: un análisis sistemático del rendimiento del código para identificar cuellos de botella.
La creación de perfiles en .NET no es solo medir el tiempo de ejecución. Es todo un conjunto de técnicas: desde un simple Stopwatch hasta herramientas avanzadas como BenchmarkDotNet y PerfView. Sin la creación de perfiles, corres el riesgo de optimizar lo que no es necesario, perdiendo horas en cambios inútiles.
En este artículo, analizaremos métodos prácticos para la creación de perfiles de código C#, aprenderemos a encontrar cuellos de botella reales y entenderemos qué herramientas usar en cada caso concreto.
1. Creación de perfiles básica con Stopwatch
La forma más sencilla de medir el tiempo de ejecución es usar la clase System.Diagnostics.Stopwatch. Es adecuada para comprobaciones rápidas y escenarios simples, pero tiene limitaciones: alta sobrecarga (nanosegundos) y falta de estadísticas.
using System.Diagnostics;
public class SimpleProfiler{ public static void Measure() { var sw = Stopwatch.StartNew(); // Código a probar var list = Enumerable.Range(0, 10_000_000).ToList(); sw.Stop(); Console.WriteLine($"Tiempo: {sw.ElapsedMilliseconds} ms"); }}
Cuándo usarlo: para una estimación aproximada, cuando necesitas una respuesta rápida sin instalar paquetes adicionales. No es adecuado para mediciones precisas (por ejemplo, al comparar algoritmos).
Desventajas: una medición única puede ser aleatoria (GC, procesos en segundo plano). Es necesario ejecutarlo varias veces y promediar; esto ya lo hace BenchmarkDotNet.
2. BenchmarkDotNet: el estándar de oro para benchmarks
BenchmarkDotNet es una biblioteca para crear benchmarks precisos y reproducibles. Calienta automáticamente el JIT, evita la influencia del GC y proporciona estadísticas detalladas.
Instalación y ejemplo básico
// Instalación: dotnet add package BenchmarkDotNetusing BenchmarkDotNet.Attributes;using BenchmarkDotNet.Running;
public class StringBenchmark{ private string _text = "Hello, World!"; [Benchmark] public bool ContainsWithOrdinal() => _text.Contains("World", StringComparison.Ordinal); [Benchmark] public bool ContainsWithInvariant() => _text.Contains("World", StringComparison.InvariantCulture);}
class Program{ static void Main() => BenchmarkRunner.Run<StringBenchmark>();}
El resultado mostrará que Ordinal es 2-3 veces más rápido. Sin el benchmark, no lo habrías notado.
Funcionalidades avanzadas
- Params: prueba con diferentes tamaños de datos.
- MemoryDiagnoser: muestra las asignaciones de memoria.
- Baseline: compara el rendimiento con un punto de referencia.
[MemoryDiagnoser]public class ListBenchmark{ [Params(100, 1000, 10000)] public int N; private List<int> _list; [GlobalSetup] public void Setup() => _list = Enumerable.Range(0, N).ToList(); [Benchmark(Baseline = true)] public int ForLoop() { int sum = 0; for (int i = 0; i < _list.Count; i++) sum += _list[i]; return sum; } [Benchmark] public int ForeachLoop() { int sum = 0; foreach (var item in _list) sum += item; return sum; }}
3. Creación de perfiles de memoria y GC
Las fugas de memoria y las recolecciones de basura frecuentes son una causa común de ralentizaciones en .NET. Para el análisis, utiliza: