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!