Recuerdo de mis días de programación en C que cuando se unen dos cadenas, el sistema operativo debe asignar memoria para la cadena unida, luego el programa puede copiar todo el texto de la cadena en la nueva área de la memoria, luego la memoria anterior debe manualmente ser liberado Entonces, si esto se hace varias veces, como en el caso de unirse a una lista, el sistema operativo debe asignar constantemente más y más memoria, solo para liberarla después de la próxima concatenación. Una forma mucho mejor de hacer esto en C sería determinar el tamaño total de las cadenas combinadas y asignar la memoria necesaria para toda la lista de cadenas unidas.
Ahora en los lenguajes de programación modernos (C #, por ejemplo), comúnmente veo el contenido de las colecciones uniéndose iterando a través de la colección y agregando todas las cadenas, una a la vez, a una sola referencia de cadena. ¿No es esto ineficiente, incluso con la potencia informática moderna?
fuente
Respuestas:
Su explicación de por qué es ineficiente es precisa, al menos en los lenguajes con los que estoy familiarizado (C, Java, C #), aunque no estoy de acuerdo con que sea universalmente común realizar cantidades masivas de concatenación de cadenas. En el código C # Yo trabajo en adelante, no es el uso abundante de
StringBuilder
,String.Format
, etc., que son el ahorro de toda la memoria techiniques a evitar el exceso de reasignación.Entonces, para obtener la respuesta a su pregunta, debemos hacer otra pregunta: si nunca es realmente un problema concatenar cadenas, ¿por qué las clases les gustaría
StringBuilder
yStringBuffer
existirían ? ¿Por qué se incluye el uso de tales clases incluso en libros y clases de programación para principiantes? ¿Por qué los consejos de optimización aparentemente maduros serían tan importantes?Si la mayoría de los desarrolladores de concatenación de cadenas basasen su respuesta únicamente en la experiencia, la mayoría diría que nunca hace la diferencia y rechazaría el uso de tales herramientas en favor de las "más legibles"
for (int i=0; i<1000; i++) { strA += strB; }
. Pero nunca lo midieron.La respuesta real a esta pregunta se puede encontrar en esta respuesta SO , que revela que en una instancia, al concatenar 50,000 cadenas (que dependiendo de su aplicación, puede ser una ocurrencia común), incluso las pequeñas, resultaron en un rendimiento de 1000x .
Si el rendimiento literalmente no significa nada en absoluto, se concatena. Pero no estoy de acuerdo con que usar alternativas (StringBuilder) sea difícil o menos legible , y por lo tanto sería una práctica de programación razonable que no debería invocar la defensa de "optimización prematura".
ACTUALIZAR:
Creo que esto se reduce a conocer su plataforma y seguir sus mejores prácticas, que lamentablemente no son universales . Dos ejemplos de dos "idiomas modernos" diferentes:
No es exactamente un pecado capital no conocer todos los matices de cada plataforma de inmediato, pero ignorar problemas importantes de la plataforma como este sería casi como pasar de Java a C ++ y no preocuparse por desasignar la memoria.
fuente
strA + strB
es exactamente lo mismo que usar un StringBuilder. Tiene un éxito de rendimiento 1x. O 0x, dependiendo de cómo estés midiendo. Para más detalles, codinghorror.com/blog/2009/01/…No es eficiente, aproximadamente por las razones que describió. Las cadenas en C # y Java son inmutables. Las operaciones en cadenas devuelven una instancia separada en lugar de modificar la original, a diferencia de lo que sucedía en C. Cuando se concatenan varias cadenas, se crea una instancia separada en cada paso. La asignación y posterior recolección de basura de esas instancias no utilizadas puede causar un impacto en el rendimiento. Solo que esta vez la gestión de memoria la gestiona el recolector de basura.
Tanto C # como Java introducen una clase StringBuilder como una cadena mutable específicamente para este tipo de tareas. Un equivalente en C sería usar una lista vinculada de cadenas concatenadas en lugar de unirlas en una matriz. C # también ofrece un método de unión conveniente en cadenas para unir una colección de cadenas.
fuente
Estrictamente hablando, es un uso menos eficiente de los ciclos de la CPU, por lo que tiene razón. Pero, ¿qué pasa con el tiempo del desarrollador, los costos de mantenimiento, etc.? Si agrega el costo del tiempo a la ecuación, casi siempre es más eficiente hacer lo más fácil, si es necesario, perfilar y optimizar los bits lentos.
"La primera regla de optimización de programas: no lo hagas. La segunda regla de optimización de programas (¡solo para expertos!): No lo hagas todavía".
fuente
Es muy difícil decir algo sobre el rendimiento sin una prueba práctica. Recientemente me sorprendió mucho descubrir que en JavaScript una concatenación de cadenas ingenua generalmente era más rápida que la solución recomendada "hacer una lista y unirse" (prueba aquí , compara t1 con t4). Todavía estoy desconcertado sobre por qué sucede eso.
Algunas preguntas que puede hacer al razonar sobre el rendimiento (especialmente en relación con el uso de memoria) son: 1) ¿qué tan grande es mi entrada? 2) ¿Qué tan inteligente es mi compilador? 3) ¿cómo gestiona la memoria mi tiempo de ejecución? Esto no es exhaustivo, pero es un punto de partida.
¿Qué tan grande es mi entrada?
Una solución compleja a menudo tendrá una sobrecarga fija, tal vez en forma de operaciones adicionales para realizar, o tal vez en la memoria adicional necesaria. Dado que esas soluciones están diseñadas para manejar grandes casos, los implementadores generalmente no tendrán problemas para introducir ese costo adicional, ya que la ganancia neta es más importante que la microoptimización del código. Por lo tanto, si su entrada es lo suficientemente pequeña, una solución ingenua puede tener un mejor rendimiento que la compleja, aunque solo sea para evitar esta sobrecarga. (determinar lo que es "suficientemente pequeño" es la parte difícil)
¿Qué tan inteligente es mi compilador?
Muchos compiladores son lo suficientemente inteligentes como para "optimizar" variables que se escriben, pero nunca se leen. Del mismo modo, un buen compilador también podría convertir una concatenación de cadenas ingenua a un uso de biblioteca (núcleo) y, si muchas de ellas se realizan sin lecturas, no hay necesidad de volver a convertirlas en una cadena entre esas operaciones (incluso si su código fuente parece hacer exactamente eso). No puedo decir si algún compilador lo hace o no, o en qué medida se hace (AFAIK Java al menos reemplaza varios concatos en la misma expresión a una secuencia de operaciones StringBuffer), pero es una posibilidad.
¿Cómo gestiona mi memoria el tiempo de ejecución?
En las CPU modernas, el cuello de botella generalmente no es el procesador, sino el caché; Si su código accede a muchas direcciones de memoria "distantes" en poco tiempo, el tiempo que lleva mover toda esa memoria entre los niveles de caché supera la mayoría de las optimizaciones en las instrucciones utilizadas. Esto es de particular importancia en tiempos de ejecución con recolectores de basura generacionales, ya que las variables creadas más recientemente (dentro del mismo alcance de función, por ejemplo) generalmente estarán en direcciones de memoria contiguas. Esos tiempos de ejecución también mueven rutinariamente la memoria de un lado a otro entre llamadas a métodos.
Una forma en que puede afectar la concatenación de cadenas (descargo de responsabilidad: esta es una suposición descabellada, no estoy lo suficientemente informado para decir con certeza) sería si la memoria para el ingenuo se asignara cerca del resto del código que lo usa (incluso si lo asigna y lo libera varias veces), mientras que la memoria para el objeto de la biblioteca se asignó lejos de él (por lo que muchos contextos cambian mientras su código calcula, la biblioteca consume, su código calcula más, etc. generaría muchos errores de caché). Por supuesto, para entradas grandes OTOH, las fallas de caché sucederán de todos modos, por lo que el problema de las asignaciones múltiples se vuelve más pronunciado.
Dicho esto, no estoy abogando por el uso de este o aquel método, solo que las pruebas y los perfiles y la evaluación comparativa deben preceder a cualquier análisis teórico sobre el rendimiento, ya que la mayoría de los sistemas hoy en día son demasiado complejos para comprenderlos completamente sin una profunda experiencia en el tema.
fuente
StringBuilder
bajo el capó, todo lo que tendría que hacer es no llamartoString
hasta que la variable sea realmente necesaria. Si recuerdo correctamente, lo hace para una sola expresión, mi única duda es si se aplica o no a múltiples declaraciones en el mismo método. No sé nada sobre los componentes internos de .NET, pero creo que el compilador de C # también podría emplear una estrategia similar.Joel escribió un gran artículo sobre este tema hace un tiempo. Como algunos otros han señalado, depende en gran medida del idioma. Debido a la forma en que las cadenas se implementan en C (terminadas en cero, sin campo de longitud), la rutina estándar de la biblioteca strcat es muy ineficiente. Joel presenta una alternativa con solo un cambio menor que es mucho más eficiente.
fuente
No.
¿Has leído "La triste tragedia del teatro de micro-optimización" ?
fuente