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:
- Stopwatchpara medir el tiempo de ejecución.
- BenchmarkDotNetpara 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#.
