Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

19 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 14 de abril de 2026
Desarrolladores manipulando cadenas

En .NET, todos hemos usado string.Format() cientos de veces. Se trata de una de esas herramientas que llevamos desde siempre en nuestro cinturón y usamos sin pensar demasiado en las implicaciones de rendimiento que puede tener su uso intensivo en escenarios críticos.

Sin embargo, cuando lo utilizamos en aplicaciones de alto rendimiento, en bucles con muchas iteraciones o en sistemas con limitaciones de recursos, es importante pararse un poco a pensar sobre cómo optimizar su uso.

En este artículo exploraremos cómo mejorar el rendimiento de string.Format() utilizando la clase CompositeFormat en .NET, que nos permite evitar el coste de analizar la cadena de formato cada vez que se llama al método.

El problema con string.Format()

Observemos un ejemplo típico de uso de string.Format():

for(var i = 0; i < 100_000; i++)
{
    var str = string.Format("Loop {0} started at {1}", i, DateTime.Now);
    Console.WriteLine(str);
}

Como podemos ver, en cada iteración del bucle se llama a string.Format() para obtener el mensaje que justo después es mostrado en la consola. El problema aquí es que cada llamada a string.Format() es independiente de las anteriores, lo que implica que, en cada vuelta, la cadena de formato "Loop {0} started at {1}" debe ser parseada para detectar los marcadores de posición, y luego reemplazarlos con los valores correspondientes.

De forma intuitiva, podemos ver claro que se trata de dos tareas diferentes: primero se analiza la cadena de formato y luego se realiza el reemplazo de valores. La segunda de ellas es inevitable, pero, ¿podríamos evitar la primera reutilizando el resultado del parseado de la cadena de formato entre las distintas llamadas?

Introducing CompositeFormat

La clase CompositeFormat, introducida en .NET 8, nos permite hacer precisamente eso. En lugar de llamar a string.Format() directamente, podemos crear una instancia de CompositeFormat con la cadena de formato parseada, y luego reutilizar esa instancia para formatear múltiples mensajes.

Aquí tenéis un ejemplo de cómo hacerlo:

var format = CompositeFormat.Parse("Loop {0} started at {1}");
for (int i = 0; i < 100_000; i++)
{
    var str = string.Format(null, format, i, DateTime.Now);
}

Como podéis ver, la cadena de formato se analiza una sola vez al crear la instancia de CompositeFormat, y luego se reutiliza en cada iteración del bucle. Esto reduce significativamente el coste de procesamiento, especialmente cuando el número de iteraciones es alto. Incluso podría mejorarse ligeramente si el objeto CompositeFormat se crea una única vez, como variable estática, y se reutiliza en todas las llamadas.

Si utilizamos BenchmarkDotNet para medir el rendimiento de estos enfoques, obtenemos los siguientes resultados:

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7462/25H2/2025Update/HudsonValley2)
Intel Core i9-9900K CPU 3.60GHz (Coffee Lake), 1 CPU, 16 logical and 8 physical cores
.NET SDK 10.0.101
  [Host]     : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3
  DefaultJob : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3

| Method                    | Mean     | Error    | StdDev   | Gen0      | Allocated |
|-------------------------- |---------:|---------:|---------:|----------:|----------:|
| UsingStandardStringFormat | 20.24 ms | 0.103 ms | 0.091 ms | 1812.5000 |   14.5 MB |
| UsingCompositeFormat | 17.97 ms | 0.096 ms | 0.089 ms | 1218.7500 |   9.92 MB |

El uso de la CPU se ha reducido en un 11,2%, y la cantidad de memoria asignada se redujo en un 31,6%, unos números nada despreciables.

Es importante tener en cuenta que el beneficio podría variar dependiendo de la complejidad de la cadena de formato. Por ejemplo, consideremos una cadena de formato más rebuscada como la siguiente, donde mostramos los mismos dos parámetros varias veces, pero con diferentes formatos:

ID:{0:D5} | Hex:0x{0:X8} | Padded:[{0,8}] | Num:{0:N0} | 
Time:{1:yyyy-MM-dd HH:mm:ss.fff} | Short:{1:dd/MM/yy} | ISO:{1:O}

Al ejecutar las mismas pruebas con esta cadena de formato más compleja, obtenemos los siguientes resultados:

| Method                    | Mean     | Error    | StdDev   | Gen0      | Allocated |
|-------------------------- |---------:|---------:|---------:|----------:|----------:|
| UsingStandardStringFormat | 59.31 ms | 0.292 ms | 0.259 ms | 4666.6667 |  38.06 MB |
| UsingCompositeFormat      | 47.32 ms | 0.300 ms | 0.281 ms | 3727.2727 |  30.44 MB |

En este caso, observamos una reducción del 20% en el uso de CPU, que demuestra que el proceso de parseado de la cadena es más costoso cuando no se reutiliza. El uso de la memoria usando CompositeFormat también mejora un 20% respecto a string.Format(), aunque en menor medida que antes debido a que el tamaño de las cadenas resultantes es mayor en ambos casos, y esto hace que la diferencia porcentualmente sea menor.

Punto extra: ¿por qué las cadenas interpoladas son mejores que ambas opciones?

Aunque CompositeFormat pueda parecer la panacea para mejorar el rendimiento de string.Format(), en realidad, las cadenas interpoladas ($"...") son una opción aún mejor en términos de rendimiento y legibilidad.

De hecho, en general, deberíamos utilizar cadenas interpoladas ($"...") en lugar de string.Format() o CompositeFormat siempre que sea posible. Las cadenas interpoladas son más legibles y se resuelven en tiempo de compilación, por lo que el compilador puede optimizarlas mejor y generar un código más eficiente. Podemos verlo en el siguiente benchmark comparativo:

| Method                  | Mean     | Error    | StdDev   | Gen0      | Allocated |
|------------------------ |---------:|---------:|---------:|----------:|----------:|
| StandardStringFormat    | 60.85 ms | 0.412 ms | 0.385 ms | 4666.6667 |  38.06 MB |
| UsingCompositeFormat    | 47.47 ms | 0.382 ms | 0.357 ms | 3727.2727 |  30.44 MB |
| UsingInterpolatedString | 37.34 ms | 0.236 ms | 0.197 ms | 3785.7143 |  30.44 MB |

Según los resultados, las cadenas interpoladas son un 38,7% más rápidas que string.Format() y un 21,3% más rápidas que CompositeFormat. Además, el uso de memoria es similar al de CompositeFormat, lo que las convierte en la opción preferida cuando la cadena de formato es conocida de antemano.

Para el resto de los casos, siempre y cuando el formateo de la cadena se encuentre en bucles o hot paths, CompositeFormat será la opción más eficiente. Pero ojo, ¡no hay que volverse locos con las micro-optimizaciones!

Publicado en: www.variablenotfound.com.

Aún no hay comentarios, ¡sé el primero!