Introducción a la clase
En esta clase aprenderemos estrategias de optimización para mejorar el rendimiento de los algoritmos en C#. Exploraremos técnicas como profiling, paralelización, estructuras de datos eficientes y reducción de la complejidad algorítmica.
¿Qué obtendrás de esta clase?
- Comprender la importancia de la optimización de algoritmos en sistemas de alto rendimiento.
- Aprender técnicas como profiling, paralelización y optimización de memoria.
- Comparar estructuras de datos según su eficiencia y aplicarlas en problemas reales.
- Implementar ejemplos de optimización en C# con diferentes estrategias.
1. Medición del rendimiento: Profiling en C#
Antes de optimizar un algoritmo, es importante medir su rendimiento. En C#, podemos utilizar:
Stopwatch
para medir el tiempo de ejecución.BenchmarkDotNet
para análisis detallado de rendimiento.
Ejemplo: Medición de tiempo con Stopwatch
using System;
using System.Diagnostics;
class Program
{
static void Main()
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
EjecutarAlgoritmo();
stopwatch.Stop();
Console.WriteLine($"Tiempo de ejecución: {stopwatch.ElapsedMilliseconds} ms");
}
static void EjecutarAlgoritmo()
{
int suma = 0;
for (int i = 0; i < 1000000; i++)
suma += i;
}
}
Esto permite comparar diferentes implementaciones y elegir la más rápida.
2. Selección de estructuras de datos eficientes
Elegir la estructura de datos correcta puede reducir drásticamente la complejidad de un algoritmo.
Operación | Lista (List<T> ) | Diccionario (Dictionary<T, T> ) | Cola (Queue<T> ) |
---|---|---|---|
Búsqueda | O(n) | O(1) | O(n) |
Inserción | O(1) al final | O(1) | O(1) |
Eliminación | O(n) | O(1) | O(1) (FIFO) |
Ejemplo: Reemplazar listas por diccionarios para mejorar la búsqueda
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
Dictionary<int, string> usuarios = new Dictionary<int, string>
{
{ 1, "Carlos" },
{ 2, "Ana" },
{ 3, "Pedro" }
};
int idBuscado = 2;
Console.WriteLine(usuarios.ContainsKey(idBuscado) ? $"Usuario encontrado: {usuarios[idBuscado]}" : "Usuario no encontrado.");
}
}
Aquí reemplazamos una búsqueda O(n) en una lista por O(1) en un diccionario.
3. Optimización con algoritmos eficientes
A veces, cambiar el algoritmo es la mejor optimización.
Ejemplo: Reemplazar Bubble Sort con Quick Sort
Bubble Sort es O(n²), mientras que Quick Sort es O(n log n).
using System;
class Program
{
static void QuickSort(int[] arr, int izquierda, int derecha)
{
if (izquierda < derecha)
{
int pivote = Particionar(arr, izquierda, derecha);
QuickSort(arr, izquierda, pivote - 1);
QuickSort(arr, pivote + 1, derecha);
}
}
static int Particionar(int[] arr, int izquierda, int derecha)
{
int pivote = arr[derecha];
int i = izquierda - 1;
for (int j = izquierda; j < derecha; j++)
{
if (arr[j] < pivote)
{
i++;
(arr[i], arr[j]) = (arr[j], arr[i]);
}
}
(arr[i + 1], arr[derecha]) = (arr[derecha], arr[i + 1]);
return i + 1;
}
static void Main()
{
int[] numeros = { 64, 34, 25, 12, 22, 11, 90 };
QuickSort(numeros, 0, numeros.Length - 1);
Console.WriteLine("Array ordenado: " + string.Join(", ", numeros));
}
}
Quick Sort reduce drásticamente el tiempo de ordenación en grandes volúmenes de datos.
4. Optimización con paralelización (Parallel Processing)
C# proporciona Parallel.For
y Parallel.ForEach
para ejecutar tareas en paralelo y mejorar el rendimiento.
Ejemplo: Uso de Parallel.For para sumar elementos de un array
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
int[] numeros = new int[1000000];
for (int i = 0; i < numeros.Length; i++) numeros[i] = i;
long suma = 0;
Parallel.For(0, numeros.Length, i => {
suma += numeros[i]; // ¡Peligro! Esto causa condiciones de carrera
});
Console.WriteLine($"Suma total: {suma}");
}
}
⚠ Advertencia: suma += numeros[i]
no es seguro en paralelo. Se debe usar Interlocked.Add()
.
Solución segura usando Interlocked.Add
csharpCopiarEditarusing System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main()
{
int[] numeros = new int[1000000];
for (int i = 0; i < numeros.Length; i++) numeros[i] = i;
long suma = 0;
Parallel.For(0, numeros.Length, i => {
Interlocked.Add(ref suma, numeros[i]);
});
Console.WriteLine($"Suma total: {suma}");
}
}
Esto mejora el rendimiento y evita condiciones de carrera en entornos multi-hilo.
5. Optimización de memoria y uso de Span<T>
Span<T>
permite manipular datos sin crear nuevas copias, reduciendo el uso de memoria y la sobrecarga de CPU.
Ejemplo: Uso de Span<T>
para manipular strings sin duplicarlas
using System;
class Program
{
static void Main()
{
string texto = "Hola, bienvenido a la optimización en C#";
ReadOnlySpan<char> span = texto.AsSpan();
ReadOnlySpan<char> subSpan = span.Slice(6, 9);
Console.WriteLine(subSpan.ToString()); // Muestra: "bienvenido"
}
}
Aquí evitamos crear un nuevo string en memoria al extraer una subcadena.
Caso de uso real
Un sistema de procesamiento de pagos debe manejar grandes volúmenes de transacciones. Aplicando:
- Diccionarios para acceder a clientes en O(1).
- Quick Sort para ordenar transacciones de mayor a menor.
- Parallel.For para procesar transacciones en múltiples hilos.
Ejemplo en C#: Procesamiento de pagos en paralelo
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class Program
{
static void Main()
{
List<decimal> pagos = new List<decimal> { 100.50m, 200.75m, 50.25m, 300.00m, 25.00m };
Parallel.ForEach(pagos, pago => {
Console.WriteLine($"Procesando pago de: {pago}");
});
}
}
Este código procesa pagos de forma concurrente, reduciendo el tiempo de ejecución.
Próxima clase: Desafíos Finales y Proyecto Integrador
En la última clase aplicaremos todos los conceptos aprendidos en un proyecto final, resolviendo un problema complejo con estructuras de datos y algoritmos optimizados en C#.