Introducción: Por qué importa el rendimiento de C#
En el mundo moderno del desarrollo de software, la velocidad de la aplicación impacta directamente la experiencia del usuario, los costos de infraestructura y la competitividad del producto. La plataforma .NET y el lenguaje C# brindan herramientas poderosas para crear sistemas de alto rendimiento; sin embargo, el uso incorrecto de las características (por ejemplo, la gestión de memoria no controlada o algoritmos ineficientes) puede provocar retrasos graves y un consumo excesivo de recursos. En este artículo, desglosaremos técnicas clave para optimizar el rendimiento de las aplicaciones C# que te ayudarán a escribir código más rápido y eficiente.
1. Perfilado: Encuentra el cuello de botella antes de optimizar
Antes de hacer cambios, es necesario determinar con precisión dónde se gasta la mayor parte del tiempo. Sin perfilado, corres el riesgo de optimizar lo que ya es rápido, pasando por alto los verdaderos cuellos de botella.
Herramientas de perfilado
- Visual Studio Diagnostic Tools — un perfilador integrado para analizar CPU, memoria y excepciones.
- dotMemory — una herramienta potente para analizar la gestión de memoria y encontrar fugas.
- PerfView — una herramienta gratuita de Microsoft para el análisis profundo del rendimiento de .NET.
- BenchmarkDotNet — una biblioteca para micro-benchmarking de secciones de código individuales.
Ejemplo de uso de BenchmarkDotNet para medir la velocidad de dos métodos:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
public class StringBenchmark
{
private string[] data = Enumerable.Range(0, 1000).Select(i => $"Item {i}").ToArray();
[Benchmark]
public string ConcatenateWithPlus()
{
string result = "";
foreach (var s in data) result += s;
return result;
}
[Benchmark]
public string ConcatenateWithStringBuilder()
{
var sb = new System.Text.StringBuilder();
foreach (var s in data) sb.Append(s);
return sb.ToString();
}
}
// Run: BenchmarkRunner.Run<StringBenchmark>();
Los resultados del benchmark mostrarán claramente que StringBuilder es decenas de veces más rápido que la concatenación usando + en bucles.
2. Gestión de memoria y reducción de la presión del GC
El Recolector de Basura (GC) es un mecanismo potente, pero su operación frecuente puede afectar negativamente el rendimiento, especialmente en sistemas de alta carga. El objetivo principal es minimizar el número de asignaciones y la vida útil de los objetos.
2.1. Usar Structs en lugar de Clases
Si un objeto es pequeño (menos de 16-24 bytes) y tiene una vida útil corta, considera usar un struct. Los structs se almacenan en la pila (o se incrustan en el objeto padre) y no crean presión en el GC.
// Clase — asignación en el heap
public class PointClass
{
public int X { get; set; }
public int Y { get; set; }
}
// Struct — en la pila
public struct PointStruct
{
public int X;
public int Y;
}
2.2. Pooling de objetos
Para objetos creados y destruidos con frecuencia (por ejemplo, conexiones de base de datos, búferes), usa un pool. .NET Core tiene una implementación incorporada ObjectPool<T>.
using Microsoft.Extensions.ObjectPool;
var pool = new DefaultObjectPool<List<int>>(new ListPolicy());
var list = pool.Get();
try
{
// Trabajar con la lista
}
finally
{
pool.Return(list); // Devolver al pool
}
2.3. Span<T> y Memory<T>
Estos tipos permiten trabajar con regiones de memoria contiguas sin asignaciones adicionales. Son ideales para análisis sintáctico, trabajar con arrays y cadenas.
public int SumDigits(ReadOnlySpan<