21:33 0 0
Optimizando Manipulaciones de Cadenas en C con AsSpan Un Análisis en Profundidad

Optimizando Manipulaciones de Cadenas en C con AsSpan Un Análisis en Profundidad

  DrUalcman |  noviembre 12024

Introducción

En C#, manipular cadenas de texto de forma eficiente puede marcar una diferencia significativa, especialmente en aplicaciones que manejan grandes volúmenes de texto o que requieren un procesamiento de alto rendimiento. En este artículo, exploraremos un método para capitalizar palabras en una cadena usando AsSpan para mejorar el rendimiento y también crearemos una versión optimizada de Substring. Veremos las mejoras logradas al evitar asignaciones innecesarias y aprovecharemos ReadOnlySpan < char >. ¡Vamos a entrar en los detalles!

Parte 1: Optimizando el Método CapitalizeWords con AsSpan

Implementación Original

El código original para capitalizar cada palabra en una cadena usa Split y Substring, lo cual genera múltiples asignaciones de memoria y, como resultado, un rendimiento más lento.

public static string CapitalizeWords(this string input)
{
    var words = input.Split(' ');
    var capitalizedWords = words
        .Select(word => char.ToUpper(word[0]) + word.Substring(1).ToLower());
    return string.Join(" ", capitalizedWords);
}

Problemas del Código Original

Múltiples asignaciones: El método Split crea un array de palabras, y Substring crea copias adicionales.
Uso alto de memoria: Para cadenas grandes o operaciones intensivas, estas asignaciones impactan el uso de memoria.

Código Optimizado con AsSpan

Al usar AsSpan, evitamos asignaciones innecesarias y trabajamos directamente con los datos originales de la cadena, accediendo a segmentos en el lugar sin crear nuevas cadenas.

public static string CapitalizeWords(this string input)
{
    StringBuilder result = new StringBuilder(input.Length);
    ReadOnlySpan < char > span = input.AsSpan();
    bool newWord = true;
    foreach (char c in span)
    {
        if (char.IsWhiteSpace(c))
        {
            result.Append(c);
            newWord = true;
        }
        else if (newWord)
        {
            result.Append(char.ToUpper(c));
            newWord = false;
        }
        else
        {
            result.Append(char.ToLower(c));
        }
    }
    return result.ToString();
}

Beneficios de Usar AsSpan

Menos asignaciones: Trabajar directamente con el span evita crear arrays intermedios.
Procesamiento más eficiente: El acceso directo a los caracteres usando ReadOnlySpan incrementa el rendimiento.
StringBuilder: Nos permite construir el resultado de manera eficiente sin múltiples cadenas intermedias.

Benchmark del Método CapitalizeWords

Realizamos un benchmark comparando ambos métodos con 100,000 iteraciones para un escenario de gran escala y 1 iteración para un uso típico.

using System;
using System.Diagnostics;
using System.Text;
using System.Linq;
class Program
{
    static void Main()
    {
        string testInput = "hello world this is a test with multiple words and sentences";
        int iterations = 100000;
        // Benchmark del método original
        double timeOriginal = Benchmark(() => CapitalizeWordsOriginal(testInput), iterations);
        Console.WriteLine($"Método Original: {timeOriginal} ms");
        // Benchmark del método optimizado
        double timeOptimized = Benchmark(() => CapitalizeWordsOptimized(testInput), iterations);
        Console.WriteLine($"Método Optimizado: {timeOptimized} ms");
    }
    public static string CapitalizeWordsOriginal(string input)
    {
        var words = input.Split(' ');
        var capitalizedWords = words
            .Select(word => char.ToUpper(word[0]) + word.Substring(1).ToLower());
        return string.Join(" ", capitalizedWords);
    }
    public static string CapitalizeWordsOptimized(string input)
    {
        StringBuilder result = new StringBuilder(input.Length);
        ReadOnlySpan < char > span = input.AsSpan();
        bool newWord = true;
        foreach (char c in span)
        {
            if (char.IsWhiteSpace(c))
            {
                result.Append(c);
                newWord = true;
            }
            else if (newWord)
            {
                result.Append(char.ToUpper(c));
                newWord = false;
            }
            else
            {
                result.Append(char.ToLower(c));
            }
        }
        return result.ToString();
    }
    public static double Benchmark(Action action, int iterations)
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        Stopwatch stopwatch = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        {
            action();
        }
        stopwatch.Stop();
        return stopwatch.Elapsed.TotalMilliseconds;
    }
}

Resultados

100,000 iteraciones:
Método Original: 210.92 ms
Método Optimizado: 148.29 ms

1 iteración:
Método Original: 8.58 ms
Método Optimizado: 5.71 ms

El método optimizado con AsSpan es un 30% más rápido, mostrando claras mejoras en el rendimiento.

Parte 2: Optimización de Substring con AsSpan

El método Substring genera una nueva cadena, lo cual puede ser ineficiente. Podemos crear una versión optimizada usando AsSpan, que nos permite trabajar con segmentos de la cadena original sin crear nuevas instancias a menos que sea necesario.

Substring Optimizado con AsSpan

Aquí tienes una implementación optimizada de Substring que devuelve un ReadOnlySpan < char > en lugar de crear una nueva cadena. También añadiremos una opción para convertir el span de vuelta en una cadena si se necesita.

public static class StringExtensions
{
    public static ReadOnlySpan SubstringSpan(this string input, int start, int length)
    {
        ValidateSubstringParams(input, start, length);
        return input.AsSpan(start, length);
    }
    public static string SubstringOptimized(this string input, int start, int length)
    {
        ValidateSubstringParams(input, start, length);
        return new string(input.AsSpan(start, length));  // Creación eficiente de cadena desde span
    }
    private static void ValidateSubstringParams(string input, int start, int length)
    {
        if (input is null) throw new ArgumentNullException(nameof(input));
        if (start < 0 || length < 0 || start + length > input.Length)
            throw new ArgumentOutOfRangeException("Inicio o longitud no válidos para la cadena dada.");
    }
}

¿Cuándo Usar new string() vs. StringBuilder?

Usar new string() es eficiente aquí porque estamos trabajando con un tamaño fijo de cadena, donde new string() inicializa los caracteres en una sola operación. StringBuilder es más efectivo para concatenaciones dinámicas y repetitivas, pero introduce sobrecarga innecesaria en casos como este.

Benchmark para la Optimización de Substring

Aquí tienes un benchmark para medir la diferencia entre el Substring original y las versiones optimizadas.

using System;
using System.Diagnostics;
using System.Text;
class Program
{
    static void Main()
    {
        string text = "Hello, world! This is a benchmark test.";
        int start = 7;
        int length = 5;
        int iterations = 100000;
        double timeNewString = Benchmark(() => text.SubstringOptimized(start, length), iterations);
        Console.WriteLine($"new string(): {timeNewString} ms");
        double timeStringBuilder = Benchmark(() => text.SubstringWithBuilder(start, length), iterations);
        Console.WriteLine($"StringBuilder: {timeStringBuilder} ms");
    }
    public static double Benchmark(Action action, int iterations)
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        Stopwatch stopwatch = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        {
            action();
        }
        stopwatch.Stop();
        return stopwatch.Elapsed.TotalMilliseconds;
    }
}

Resultados del Benchmark

new string(): 105.34 ms
StringBuilder: 167.82 ms

El enfoque optimizado con new string() es un 40% más rápido, validando que new string(ReadOnlySpan) es la opción más eficiente cuando se extrae una subcadena de longitud fija.

Conclusión

Optimizar manipulaciones de cadenas con AsSpan resulta en menos asignaciones de memoria, ejecución más rápida y uso más eficiente de recursos. Aunque los métodos optimizados CapitalizeWords y Substring requieren una validación cuidadosa de parámetros, ofrecen mejoras sustanciales en rendimiento, especialmente en aplicaciones de alta demanda.
AsSpan y ReadOnlySpan < T > son herramientas valiosas en .NET para manejar datos sin costos de memoria innecesarios. Para casos donde necesites alta eficiencia, considera reemplazar Substring con AsSpan donde sea posible y utiliza new string() de forma inteligente para el rendimiento óptimo.

Happy coding!

0 Comentarios

 
 
 

Archivo