¿Por qué agregar a TextBox.Text durante un bucle consume más memoria con cada iteración?

82

Pregunta corta

Tengo un bucle que se ejecuta 180.000 veces. Al final de cada iteración, se supone que debe agregar los resultados a un TextBox, que se actualiza en tiempo real.

El uso MyTextBox.Text += someValuehace que la aplicación consuma grandes cantidades de memoria y se quede sin memoria disponible después de unos pocos miles de registros.

¿Existe una forma más eficiente de agregar texto a TextBox.Text180.000 veces?

Editar Realmente no me importa el resultado de este caso específico, sin embargo, quiero saber por qué esto parece ser una pérdida de memoria y si hay una forma más eficiente de agregar texto a un TextBox.


Pregunta larga (original)

Tengo una pequeña aplicación que lee una lista de números de identificación en un archivo CSV y genera un informe en PDF para cada uno. Después de que se genera cada archivo pdf, ResultsTextBox.Textse agrega el número de identificación del informe que se procesó y que se procesó correctamente. El proceso se ejecuta en un hilo de fondo, por lo que ResultsTextBox se actualiza en tiempo real a medida que se procesan los elementos

Actualmente estoy ejecutando la aplicación con 180.000 números de identificación, sin embargo, la memoria que ocupa la aplicación está creciendo exponencialmente a medida que pasa el tiempo. Comienza por alrededor de 90K, pero por alrededor de 3000 registros ocupa aproximadamente 250 MB y por 4000 registros la aplicación está ocupando alrededor de 500 MB de memoria.

Si comento la actualización del Results TextBox, la memoria permanece relativamente estacionaria en aproximadamente 90K, por lo que puedo asumir que la escritura ResultsText.Text += someValuees lo que está causando que se coma la memoria.

Mi pregunta es, ¿por qué es esto? ¿Cuál es una mejor manera de agregar datos a un TextBox.Text que no consume memoria?

Mi código se ve así:

try
{
    report.SetParameterValue("Id", id);

    report.ExportToDisk(ExportFormatType.PortableDocFormat,
        string.Format(@"{0}\{1}.pdf", new object[] { outputLocation, id}));

    // ResultsText.Text += string.Format("Exported {0}\r\n", id);
}
catch (Exception ex)
{
    ErrorsText.Text += string.Format("Failed to export {0}: {1}\r\n", 
        new object[] { id, ex.Message });
}

También vale la pena mencionar que la aplicación es algo que se usa una sola vez y no importa que se necesiten algunas horas (o días :)) para generar todos los informes. Mi principal preocupación es que si alcanza el límite de memoria del sistema, dejará de funcionar.

Estoy bien con dejar la línea actualizando el Results TextBox comentado para ejecutar esto, pero me gustaría saber si hay una forma más eficiente de agregar datos a la memoria TextBox.Textpara proyectos futuros.

Raquel
fuente
7
Puede intentar usar a StringBuilderpara agregar el texto y luego, una vez completado, asignar el StringBuildervalor al cuadro de texto.
keyboardP
1
No sé si cambiaría algo, pero ¿qué pasaría si tuviera un StringBuilder que agregue los nuevos Id-s y use una propiedad que se actualice con el nuevo valor del generador de cadenas y vincule esto a su textbox.text propiedad.
BigL
2
¿Por qué está inicializando una matriz de objetos al llamar a string.Format? Hay sobrecargas que toman 2 parámetros para que pueda evitar crear una matriz. Además, cuando usa la sobrecarga de parámetros, la matriz se crea para usted detrás de escena.
ChaosPandion
1
string concat no es necesariamente ineficiente. Si está concatenando cadenas en varias unidades de trabajo y mostrando los resultados entre cada unidad de trabajo, será más eficiente que StringBuilder. StringBuilder es realmente solo más eficiente cuando está construyendo una cadena a través de un bucle y luego solo escribe el resultado al final del bucle.
James Michael Hare
3
Iba a decir, es una máquina bastante impresionante :-)
James Michael Hare

Respuestas:

119

Sospecho que la razón por la que el uso de memoria es tan grande es porque los cuadros de texto mantienen una pila para que el usuario pueda deshacer / rehacer el texto. Esa característica no parece ser necesaria en su caso, así que intente configurarla IsUndoEnableden falso.

keyboardP
fuente
1
Desde el enlace de MSDN: "Pérdida de memoria Si tiene un aumento de memoria en su aplicación porque establece un valor del código muy a menudo, la pila de deshacer del bloque de texto puede ser una" pérdida "de la memoria. Al usar esta propiedad, puede deshabilitar y despejar el camino a la pérdida de memoria ".
ola
33
La mayoría de las veces, los usuarios y desarrolladores esperarían que el cuadro de texto funcione como cuadros de texto estándar (es decir, con la capacidad de deshacer / rehacer). En casos extremos, como los requisitos de OP, puede resultar un obstáculo. Si la mayoría de la gente lo usa, entonces debería ser el predeterminado. ¿Por qué esperaría que un caso de borde obligue a que la funcionalidad estándar se convierta en opt-in?
keyboardP
1
Alternativamente, también puede establecer UndoLimitun valor realista. El valor predeterminado de -1 indica una pila ilimitada. Cero (0) también deshabilitará deshacer.
myermian
14

Utilizar en TextBox.AppendText(someValue)lugar de TextBox.Text += someValue. Es fácil pasarlo por alto ya que está en TextBox, no en TextBox.Text. Al igual que StringBuilder, esto evitará crear copias de todo el texto cada vez que agregue algo.

Sería interesante ver cómo se compara esto con la IsUndoEnabledbandera de la respuesta de keyboardP.

Cypher2100
fuente
En el caso de las formas de Windows, esta es la mejor solución, como formas de las ventanas no tienen TextBox.IsUndoEnabled
BrDaHa
En formularios Win, tiene una bool CanUndopropiedad
imlokesh
9

No agregue directamente a la propiedad de texto. Use un StringBuilder para agregar, luego, cuando haya terminado, establezca el .text en la cadena terminada del stringbuilder

Darthg8r
fuente
2
Olvidé mencionar que el bucle se ejecuta en un hilo de fondo y los resultados se actualizan en tiempo real
Rachel
5

En lugar de usar un cuadro de texto, haría lo siguiente:

  1. Abra un archivo de texto y transmita los errores a un archivo de registro por si acaso.
  2. Utilice un control de cuadro de lista para representar los errores y evitar copiar cadenas potencialmente masivas.
ChaosPandion
fuente
4

Personalmente, siempre uso string.Concat*. Recuerdo haber leído una pregunta aquí en Stack Overflow hace años que tenía estadísticas de perfiles comparando los métodos de uso común, y (parece) recordar que string.Concatganó.

No obstante, lo mejor que puedo encontrar es esta pregunta de referencia y esta pregunta específica String.Formatvs.StringBuilder , que menciona que String.Formatusa StringBuilderinternamente. Esto me hace preguntarme si tu memoria está en otra parte.

** basado en el comentario de James, debo mencionar que nunca hago un gran formato de cadenas, ya que me enfoco en el desarrollo basado en la web. *

jwheron
fuente
Estoy de acuerdo, a veces la gente se mete en la rutina de decir "siempre use X porque X es lo mejor", lo cual suele ser una simplificación excesiva. Hay mucha sutileza entre string.Concat (), string.Format () y StringBuilder. Mi regla general es usar cada uno para lo que está destinado (suena tonto, lo sé, pero es cierto). Utilizo concat cuando estoy uniendo cadenas (y luego uso el resultado inmediatamente), uso Format cuando estoy realizando formateo de cadenas no trivial (relleno, formatos numéricos, etc.) y StringBuilder para construir cadenas durante un bucle para utilizarse al final del ciclo.
James Michael Hare
@JamesMichaelHare, eso tiene sentido para mí; ¿Está sugiriendo que el uso de string.Format/ StringBuilderes más apropiado aquí?
jwheron
Oh no, solo estaba de acuerdo con su punto general de que concat es generalmente mejor para concats de cadena simple. El problema con las "reglas generales" es que pueden cambiar de una versión .NET a otra si el BCL cambia, por lo que mantener la construcción lógicamente correcta es más fácil de mantener y, por lo general, funciona mejor para sus tareas. De hecho, tuve una publicación de blog anterior en la que comparé
James Michael Hare
Debidamente anotado - sólo quería estar seguro - y respuesta editada para calificar mi uso de la palabra "siempre".
jwheron
3

¿Quizás reconsiderar el TextBox? Un ListBox con elementos de cadena probablemente funcionará mejor.

Pero el problema principal parecen ser los requisitos. Mostrar 180.000 elementos no puede estar dirigido a un usuario (humano), ni cambiarlo en "Tiempo real".

La forma preferible sería mostrar una muestra de los datos o un indicador de progreso.

Cuando desee volcarlo en el usuario pobre, actualice la cadena por lotes. Ningún usuario pudo distinguir más de 2 o 3 cambios por segundo. Entonces, si produce 100 / segundo, haga grupos de 50.

Henk Holterman
fuente
Gracias Henk. Esto fue algo de una sola vez, así que estaba siendo perezoso al escribirlo. Quería algún tipo de salida visual para saber cuál era el estado, y quería capacidades de selección de texto y una barra de desplazamiento. Supongo que podría haber usado un ScrollViewer / Label, pero los TextBoxes tienen ScrollBarrs integrados. No esperaba que causara problemas :)
Rachel
2

Algunas respuestas lo han aludido, pero nadie lo ha declarado abiertamente, lo cual es sorprendente. Las cadenas son inmutables, lo que significa que una cadena no se puede modificar después de su creación. Por lo tanto, cada vez que concatena a una cadena existente, es necesario crear un nuevo objeto de cadena. Obviamente, la memoria asociada con ese objeto String también debe crearse, lo que puede resultar costoso a medida que sus cadenas se hacen cada vez más grandes. En la universidad, una vez cometí el error de aficionado de concatenar cadenas en un programa Java que hacía compresión de codificación Huffman. Cuando está concatenando cantidades extremadamente grandes de texto, la concatenación de cadenas puede realmente perjudicarlo cuando podría haber usado simplemente StringBuilder, como algunos han mencionado aquí.

KyleM
fuente
2

Utilice StringBuilder como se sugiere. Intente estimar el tamaño final de la cadena y luego use ese número al crear una instancia de StringBuilder. StringBuilder sb = nuevo StringBuilder (estSize);

Cuando actualice el TextBox, simplemente use la asignación, por ejemplo: textbox.text = sb.ToString ();

Esté atento a las operaciones de subprocesos cruzados como arriba. Sin embargo, use BeginInvoke. No es necesario bloquear el hilo de fondo mientras se actualiza la interfaz de usuario.

Steven Licht
fuente
1

A) Intro: ya mencionado, use StringBuilder

B) Punto: no actualice con demasiada frecuencia, es decir

DateTime dtLastUpdate = DateTime.MinValue;

while (condition)
{
    DoSomeWork();
    if (DateTime.Now - dtLastUpdate > TimeSpan.FromSeconds(2))
    {
        _form.Invoke(() => {textBox.Text = myStringBuilder.ToString()});
        dtLastUpdate = DateTime.Now;
    }
}

C) Si es un trabajo único, use la arquitectura x64 para mantenerse dentro del límite de 2 Gb.

bohdan_trotsenko
fuente
1

StringBuilderin ViewModelevitará el desorden de las reencuadernaciones de cuerdas y las unirá MyTextBox.Text. Este escenario aumentará el rendimiento muchas veces y disminuirá el uso de memoria.

Anatolii Gabuza
fuente
0

Algo que no se ha mencionado es que incluso si está realizando la operación en el subproceso en segundo plano, la actualización del elemento de la interfaz de usuario TIENE que suceder en el subproceso principal (en WinForms de todos modos).

Al actualizar su cuadro de texto, ¿tiene algún código que se parezca a

if(textbox.dispatcher.checkAccess()){
    textbox.text += "whatever";
}else{
    textbox.dispatcher.invoke(...);
}

Si es así, la actualización de la interfaz de usuario definitivamente está obstruyendo su operación en segundo plano.

Sugeriría que su operación en segundo plano use StringBuilder como se indicó anteriormente, pero en lugar de actualizar el cuadro de texto en cada ciclo, intente actualizarlo a intervalos regulares para ver si aumenta el rendimiento para usted.

EDITAR NOTA: no he usado WPF.

Daryl Teo
fuente
0

Dices que la memoria crece exponencialmente. No, es un crecimiento cuadrático , es decir, un crecimiento polinomial, que no es tan dramático como un crecimiento exponencial.

Está creando cadenas con el siguiente número de elementos:

1 + 2 + 3 + 4 + 5 ... + n = (n^2 + n) /2.

Con n = 180,000usted obtiene la asignación total de memoria para 16,200,090,000 items, es decir 16.2 billion items! Esta memoria no se asignará a la vez, ¡pero es mucho trabajo de limpieza para el GC (recolector de basura)!

Además, tenga en cuenta que la cadena anterior (que está creciendo) debe copiarse en la nueva cadena 179,999 veces. ¡El número total de bytes copiados también va con él n^2!

Como han sugerido otros, use un ListBox en su lugar. Aquí puede agregar nuevas cadenas sin crear una cadena enorme. A StringBuildno ayuda, ya que también desea mostrar los resultados intermedios.

Olivier Jacot-Descombes
fuente