0

Estructuras de Datos en C# 14: Optimización y rendimiento en algoritmos en C#

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ónLista (List<T>)Diccionario (Dictionary<T, T>)Cola (Queue<T>)
BúsquedaO(n)O(1)O(n)
InserciónO(1) al finalO(1)O(1)
EliminaciónO(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#.

Fernando Sonego

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *