Concatenación de strings y como “matar” un servidor

Uno de los problemas que usualmente uno enfrenta es el alto uso de CPU de un servidor y la “poca” capacidad de procesamiento de éste.


La forma tradicional de analizar estos problemas de alto uso de CPU es tomar dumps de memoria mientras la CPU esta con alto uso y ver que está ejecutando cada thread en el momento de la “foto”.


Para el caso que revisé hace un tiempo, en un sitio de muchas páginas, el dump tomado mostraba que una de ellas estaba consumiendo mucha CPU. Después de analizar con más detalle que actividad estaba realizando ésta, como podrán deducir del título del post, estaba concatenando strings para generar el output.


Los desarrolladores novatos, y los no tanto también aunque en menor medida, tienden a utilizar bastante la concatenación de strings para armar una salida de html o xml. El problema radica en que no conocen o subestiman el “poder de destrucción” de este proceso, en recursos como la CPU y [memoria].


Revisemos este código que utilizaré para la demo de esta oportunidad. En ésta, he sobre-simplificado una página asp y aspx (el problema es el mismo) que concatena string para armar una salida html y escribirla en un label de aspnet, o con response.write.





String sb = string.Empty;
for (int i=1 ; i<=1000;i++)
{
       sb = sb + “dsfih      yhfuwefifiojfdshf dhfdh dhf fhiua hfdoisfog h hiuh asdfgdsfhiufhds<br>”;
}
this.Label1.Text = sb;

El problema de concatenar

El gran costo de la concatenación de string se debe a la inmutabilidad del tipo (¿inmutabilidad?). Bueno, esa es la definición técnica correcta. Pero llevándolo a un lenguaje más simple, el problema radica en que los strings no se pueden alterar y cada vez que se concatena, se deben generar nuevos strings.


¿Aún no está claro?… veamos con un ejemplo.


Supongamos que tenemos este código (como dice [Robbins], un código vale más que mil palabras):





String test = string.Empty;
test = “primera línea”;                        Linea 1
test = test + “<br>”;                          Linea 2
test = test + “segunda línea”;                 Linea 3
test = test + “<br>” + “tercera línea”;        Linea 4
test = test + “<br>cuarta linea”;              Linea 5

¿Cómo afecta la inmutabilidad del string en este proceso?

Al ejecutarse la línea 1, se piden 13 bytes para almacenar el string “primera línea”. Realmente son más que 13 pero esto no influye en lo que se quiere explicar.


Al ejecutarse la línea 2, se calcula el largo de test y el largo del texto a concatenar. En este caso son 13 bytes y 4 bytes, y se piden 17 bytes. Luego, los 13 bytes son copiados desde la primera variable y luego son copiados los 4 bytes de la segunda variable. Una vez copiados todos los bytes, se asigna el string resultante a la variable test.


En este momento hemos comprobado la inmutabilidad del string. No puedo simplemente concatenarle “<br>” a final de test. El CLR (y la máquina virtual de VB 6.0 también) deben crear nuevos strings y copiar todo el contenido.


La línea 3 es similar a la anterior. Los largos actuales son 17 y 13 bytes respectivamente. Se debe pedir 30 bytes, copiar los 17, luego los 13 y asignar el nuevo valor a test.


La línea 4 funciona mejor de lo que esperamos. El funcionamiento está optimizado y en vez de tratarse “<br>”+”tercera línea” como dos strings, para la concatenación se consideran como si fuese uno sólo, requiriendo los 30 bytes de test, más los 17 de “<br>”+”tercera línea”. El proceso continua igual, copiando y asignando.


La línea 5 es equivalente a la 4, salvo que no son 17 bytes sino 16.


Bueno, alguien podría decir que un proceso tan simple como éste no debiera afectar el rendimiento de un servidor con muchas CPUs y memoria. En efecto, en este ejemplo básico, el costo de la concatenación es despreciable. El problema real ocurre cuando esta está dentro de un ciclo y la variable test almacena varios miles de bytes.


Ya no es tan rápido pedirle 35 kilobytes, luego copiar los 35 kilobytes de un string a otro y luego copiar lo que se quiere concatenar.


Personalmente he visto en dumps un string de 450 Kilobytes siendo concatenado. Ni les digo como estaba la CPU de ese servidor.

Solución en .net

Para evitar este problema, en .net existe una clase llamada StringBuilder, disponible en System.Text. StringBuilder. La “gracia” de ésta es que permite concatenar efectivamente strings sin crear nuevos strings por debajo.


Definición y ejemplos de uso, disponibles en español en el siguiente link:


http://msdn2.microsoft.com/es-es/library/system.text.stringbuilder(VS.80).aspx

Bueno, mucha habladuría y poca demostración. Vamos a ver cómo se comporta mi demo.

He creado tres páginas aspx, con el mismo texto en page_load, salvo que en una de ellas, el desarrollador ha decidido utilizar concatenación de strings en vez de seguir el ejemplo de los otros desarrolladores que habían utilizado correctamente StringBuilder.


Implementación correcta, en páginas webform1.aspx y webform2.aspx





private void Page_Load(object sender, System.EventArgs e)
{
       System.Text.StringBuilder sb = new  System.Text.StringBuilder();
       for (int i=1 ; i<=1000;i++)
       {
             sb.Append(“dsfih      yhfuwefifiojfdshf dhfdh dhf fhiua hfdoisfog h hiuh asdfgdsfhiufhds<br>”);
       }
       this.Label1.Text = sb.ToString();
}
 

Implementación Incorrecta, en página webform3.aspx





private void Page_Load(object sender, System.EventArgs e)
{
       String sb = string.Empty;
       for (int i=1 ; i<=1000;i++)
       {
             sb = sb + “dsfih      yhfuwefifiojfdshf dhfdh dhf fhiua hfdoisfog h hiuh asdfgdsfhiufhds<br>”;
      
}
       this.Label1.Text = sb;

}

 

Adicionalmente creé una página llamada default.aspx que me permitía llegar a estas tres páginas.





<TABLE id=”Table1″ cellSpacing=”0″ cellPadding=”0″ width=”300″ border=”0″>
       <TR><TD><a href=”webform1.aspx”>Pagina 1</a></TD></TR>
       <TR><TD><a href=”webform2.aspx”>Pagina 2</a></TD></TR>
       <TR><TD><a href=”webform3.aspx”>Pagina 3</a></TD></TR>
</TABLE>


Luego grabé con ACT un script para una prueba de carga con la ejecución de las páginas. Para demostrar con mayor fuerza el impacto de la concatenación, en  el script decidí incluir cinco ejecuciones de la página webform1.aspx (correcta), cuatro de la página webform2.aspx (correcta) y una sola de la página webform3.aspx (incorrecta).


Mi interés no es ensañarme con la página webform3.aspx sino que demostrar que con el 10% de las páginas concatenado string, el impacto es severo.

Prueba de carga

La prueba de carga la realicé durante un minuto y con 5 usuarios concurrentes, lo suficiente para entretener al servidor. Posteriormente corregí el código reemplazando la concatenación de string por un stringbuilder y ejecuté el mismo script grabado.


Estos son los resultados obtenidos.


La ejecución con concatenación de strings corresponde a la con el número (1) de color celeste. La con stringbuilder corresponde a la con número (2) de color café oscuro.


Reporte

Conclusión

El gráfico y las tablas son totalmente concluyentes. Concatenando strings logramos ejecutar sólo 4.901 requerimientos de páginas, pero con la versión con stringbuilder, se llega hasta 20.903 requerimientos. La versión corregida permite ejecutar 4,26 veces la cantidad de requerimientos o dicho de otra forma, sólo podemos obtener el 23,4% de rendimiento desde nuestro servidor con una página concatenando string. Por supuesto, estos valores son relativos al sistema operativo, versión de IIS, cantidad de CPUs, memoria, etc. Esto es sólo un ejemplo ejecutado en un computador de escritorio.


Por otra parte, es necesario explicar que en la prueba donde se concatena strings, el resultado se ve afectado por dos motivos. Uno de ellos ya ha sido mencionado y corresponde al hecho de pedir memoria y copiar el contenido. El otro motivo es el alto tiempo que el garbage collector (GC) pasa trabajando limpiando los strings que ya no se usan. El uso (y abuso) del GC lo veremos en otra oportunidad.


Saludos,
Patrick

3 Replies to “Concatenación de strings y como “matar” un servidor”

  1. Excelente, hace un tiempo me paso eso, pero usando javascript, que lamentablemente no tiene no stringbuilder. Excelente el blog. hoy lo descubri, y aprendi mucho. Muchas Gracias!

  2. Muy buena información. Voy a cambiar algunos de mis códigos donde uso muchas concatenaciones. Gracias!!

  3. My friend Brian Miehe, a guy who drives a LOT of traffic to various offers through his large website network, has just released a traffic service with a free trial so webmasters can “try before they buy”. If you are interested in driving lots of traffic to your website then don’t miss this offer: http://gmbal.com/189u

Leave a Reply to Miguel Cancel reply

Your email address will not be published. Required fields are marked *