Warum beansprucht das Anhängen an TextBox.Text während einer Schleife mit jeder Iteration mehr Speicher?

82

Kurze Frage

Ich habe eine Schleife, die 180.000 Mal läuft. Am Ende jeder Iteration sollen die Ergebnisse an eine TextBox angehängt werden, die in Echtzeit aktualisiert wird.

Verwenden von MyTextBox.Text += someValue führt dazu, dass die Anwendung sehr viel Speicher verbraucht und nach einigen tausend Datensätzen nicht mehr genügend Speicher verfügbar ist.

Gibt es eine effizientere Möglichkeit, Text an TextBox.Text180.000 Mal anzuhängen?

Bearbeiten Das Ergebnis dieses speziellen Falls ist mir wirklich egal, aber ich möchte wissen, warum dies ein Speicherfresser zu sein scheint und ob es eine effizientere Möglichkeit gibt, Text an eine TextBox anzuhängen.


Lange (Original) Frage

Ich habe eine kleine App, die eine Liste von ID-Nummern in einer CSV-Datei liest und für jede einen PDF-Bericht generiert. Nachdem jede PDF-Datei generiert wurde, ResultsTextBox.Textwird die ID-Nummer des Berichts angehängt, der verarbeitet wurde und der erfolgreich verarbeitet wurde. Der Prozess wird in einem Hintergrundthread ausgeführt, sodass die ResultsTextBox in Echtzeit aktualisiert wird, wenn Elemente verarbeitet werden

Ich führe die App derzeit mit 180.000 ID-Nummern aus, aber der Speicher, den die Anwendung belegt, wächst im Laufe der Zeit exponentiell. Es beginnt bei ungefähr 90 KB, aber bei ungefähr 3000 Datensätzen nimmt es ungefähr 250 MB ein und bei 4000 Datensätzen nimmt die Anwendung ungefähr 500 MB Speicher ein.

Wenn ich die Aktualisierung der Ergebnistextbox auskommentiere, bleibt der Speicher mit ungefähr 90 KB relativ stationär, sodass ich davon ausgehen kann, dass geschrieben wird ResultsText.Text += someValue dazu führt, dass der Speicher belegt wird.

Meine Frage ist, warum ist das so? Was ist eine bessere Möglichkeit, Daten an eine TextBox.Text anzuhängen, die keinen Speicher verbraucht?

Mein Code sieht folgendermaßen aus:

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 });
}

Es sollte auch erwähnenswert sein, dass die App eine einmalige Sache ist und es keine Rolle spielt, dass es einige Stunden (oder Tage :) dauern wird, bis alle Berichte erstellt sind. Mein Hauptanliegen ist, dass es nicht mehr läuft, wenn es das Systemspeicherlimit erreicht.

Ich bin in Ordnung, wenn ich die Zeile verlasse, in der die Ergebnisse aktualisiert werden, die TextBox auskommentiert hat, um dieses Ding auszuführen, aber ich würde gerne wissen, ob es eine speichereffizientere Möglichkeit gibt, Daten TextBox.Textfür zukünftige Projekte an a anzuhängen .

Rachel
quelle
7
Sie können versuchen, StringBuilderden Text mit a anzuhängen, und dann nach Abschluss den StringBuilderWert dem Textfeld zuweisen .
TastaturP
1
Ich weiß nicht, ob es etwas ändern würde, aber was wäre, wenn Sie einen StringBuilder hätten, der die neuen IDs anfügt, und Sie würden eine Eigenschaft verwenden, die mit dem neuen Wert des String-Builders aktualisiert wird, und dies an Ihre textbox.text binden Eigentum.
BigL
2
Warum initialisieren Sie ein Objektarray, wenn Sie string.Format aufrufen? Es gibt Überladungen, die zwei Parameter annehmen, sodass Sie das Erstellen eines Arrays vermeiden können. Wenn Sie die Parameterüberladung verwenden, wird das Array hinter den Kulissen für Sie erstellt.
ChaosPandion
1
string concat ist nicht unbedingt ineffizient. Wenn Sie Zeichenfolgen über mehrere Arbeitseinheiten hinweg verketten und die Ergebnisse zwischen den einzelnen Arbeitseinheiten anzeigen, ist dies effizienter als StringBuilder. StringBuilder ist wirklich nur dann effizienter, wenn Sie einen String durch eine Schleife erstellen und dann das Ergebnis erst am Ende der Schleife ausschreiben.
James Michael Hare
3
Ich wollte sagen, das ist ziemlich beeindruckend :-)
James Michael Hare

Antworten:

119

Ich vermute, der Grund für die so große Speichernutzung liegt darin, dass Textfelder einen Stapel verwalten, damit der Benutzer Text rückgängig machen / wiederholen kann. Diese Funktion scheint in Ihrem Fall nicht erforderlich zu sein. Versuchen Sie daher, sie IsUndoEnabledauf false zu setzen.

TastaturP
quelle
1
Über den MSDN-Link: "Speicherverlust Wenn in Ihrer Anwendung ein Speicherzuwachs auftritt, weil Sie den Wert aus dem Code sehr häufig festlegen, kann der Rückgängig-Stapel des Textblocks ein" Speicherverlust "des Speichers sein. Mit dieser Eigenschaft können Sie ihn deaktivieren es und den Weg frei, um Speicher zu verlieren. "
Welle
33
In den meisten Fällen erwarten Benutzer und Entwickler, dass das Textfeld wie Standardtextfelder funktioniert (dh mit der Möglichkeit zum Rückgängigmachen / Wiederherstellen). In Randfällen wie den Anforderungen von OP kann sich dies als Hindernis erweisen. Wenn die Mehrheit der Benutzer es verwendet, sollte es die Standardeinstellung sein. Warum sollte ein Edge-Case die Opt-In-Funktion für Standardfunktionen erzwingen?
TastaturP
1
Alternativ können Sie auch UndoLimiteinen realistischen Wert festlegen . Der Standardwert -1 gibt einen unbegrenzten Stapel an. Null (0) deaktiviert auch das Rückgängigmachen.
Myermian
14

Verwenden Sie TextBox.AppendText(someValue)anstelle vonTextBox.Text += someValue . Es ist leicht zu übersehen, da es sich um TextBox handelt, nicht um TextBox.Text. Wie bei StringBuilder wird dadurch vermieden, dass jedes Mal, wenn Sie etwas hinzufügen, Kopien des gesamten Textes erstellt werden.

Es wäre interessant zu sehen, wie dies mit dem IsUndoEnabledFlag aus der Antwort von keyboardP verglichen wird .

Cypher2100
quelle
Im Fall von Windows-Formularen ist dies die beste Lösung, da Windows-Formulare keine TextBox haben. IsUndoEnabled
BrDaHa
In Win-Formularen haben Sie eine bool CanUndoEigenschaft
imlokesh
9

Hängen Sie nicht direkt an die Texteigenschaft an. Verwenden Sie einen StringBuilder für das Anhängen. Wenn dies erledigt ist, setzen Sie den Text aus dem Stringbuilder auf den fertigen String

Darthg8r
quelle
2
Ich habe vergessen zu erwähnen, dass die Schleife in einem Hintergrund-Thread ausgeführt wird und die Ergebnisse in Echtzeit aktualisiert werden
Rachel
5

Anstatt ein Textfeld zu verwenden, würde ich Folgendes tun:

  1. Öffnen Sie eine Textdatei und streamen Sie die Fehler für alle Fälle in eine Protokolldatei.
  2. Verwenden Sie ein Listenfeld-Steuerelement, um die Fehler darzustellen und das Kopieren potenziell massiver Zeichenfolgen zu vermeiden.
ChaosPandion
quelle
4

Persönlich benutze ich immer string.Concat*. Ich erinnere mich, dass ich hier vor Jahren eine Frage zu Stack Overflow gelesen habe, in der Profilstatistiken zum Vergleich der häufig verwendeten Methoden verwendet wurden, und mich daran zu erinnern scheintstring.Concat sie sich durchgesetzt haben.

Das Beste, was ich finden kann, ist jedoch diese Referenzfrage und diese spezifische String.Formatvs.StringBuilder Frage, in der erwähnt wird, dass String.Formatein StringBuilderintern verwendet wird. Ich frage mich, ob Ihr Erinnerungsschwein woanders liegt.

** Basierend auf James 'Kommentar sollte ich erwähnen, dass ich niemals schwere Zeichenfolgen formatiere, da ich mich auf die webbasierte Entwicklung konzentriere. *

jwheron
quelle
Ich stimme zu, manchmal geraten die Leute in die Brunft zu sagen "benutze immer X, weil X am besten ist", was normalerweise eine übermäßige Vereinfachung ist. Es gibt viel Subtilität zwischen string.Concat (), string.Format () und StringBuilder. Meine Faustregel ist, jedes zu verwenden, wofür es bestimmt ist (es klingt albern, ich weiß, aber es gilt). Ich verwende concat, wenn ich Zeichenfolgen verbinde (und dann das Ergebnis sofort verwende), ich verwende Format, wenn ich nicht triviale Zeichenfolgenformatierungen (Auffüllen, numerische Formate usw.) durchführe, und StringBuilder, um Zeichenfolgen während einer Schleife zu erstellen am Ende der Schleife verwendet werden.
James Michael Hare
@ JamesMichaelHare, das macht für mich Sinn; Schlagen Sie vor, dass die Verwendung von string.Format/ StringBuilderhier angemessener ist?
Jwheron
Oh nein, ich stimmte nur Ihrem allgemeinen Standpunkt zu, dass Concat normalerweise am besten für einfache String-Concats geeignet ist. Das Problem mit "Faustregeln" besteht darin, dass sie sich von .NET-Version zu Version ändern können, wenn sich die BCL ändert. Daher ist das Festhalten am logisch korrekten Konstrukt wartbarer und für seine Aufgaben in der Regel besser. Ich hatte tatsächlich einen älteren Blog-Beitrag, in dem ich die drei hier verglichen habe: geekswithblogs.net/BlackRabbitCoder/archive/2010/05/10/…
James Michael Hare
Ordnungsgemäß notiert - wollte nur sicher sein - und die Antwort bearbeitet, um meine Verwendung des Wortes "immer" zu qualifizieren.
Jwheron
3

Vielleicht die TextBox überdenken? Eine ListBox mit String-Elementen wird wahrscheinlich eine bessere Leistung erzielen.

Das Hauptproblem scheinen jedoch die Anforderungen zu sein: Das Anzeigen von 180.000 Elementen kann nicht an einen (menschlichen) Benutzer gerichtet werden und wird auch nicht in "Echtzeit" geändert.

Der bevorzugte Weg wäre, eine Stichprobe der Daten oder eine Fortschrittsanzeige anzuzeigen.

Wenn Sie es auf dem armen Benutzer sichern möchten, werden die Stapelzeichenfolgen aktualisiert. Kein Benutzer konnte mehr als 2 oder 3 Änderungen pro Sekunde feststellen. Wenn Sie also 100 / Sekunde produzieren, bilden Sie Gruppen von 50.

Henk Holterman
quelle
Danke Henk. Dies war eine einmalige Sache, also war ich faul beim Schreiben. Ich wollte eine Art visuelle Ausgabe, um den Status zu kennen, und ich wollte Textauswahlfunktionen und eine ScrollBar. Ich nehme an, ich hätte einen ScrollViewer / Label verwenden können, aber in TextBoxen sind ScrollBarrs eingebaut. Ich hatte nicht erwartet, dass dies Probleme verursachen würde :)
Rachel
2

Einige Antworten haben darauf angespielt, aber niemand hat es direkt gesagt, was überraschend ist. Strings sind unveränderlich, was bedeutet, dass ein String nach seiner Erstellung nicht mehr geändert werden kann. Daher muss jedes Mal, wenn Sie mit einem vorhandenen String verketten, ein neues String-Objekt erstellt werden. Der mit diesem String-Objekt verknüpfte Speicher muss natürlich auch erstellt werden, was teuer werden kann, wenn Ihre Strings immer größer werden. Im College habe ich einmal den Amateurfehler gemacht, Strings in einem Java-Programm zu verketten, das die Huffman-Codierungskomprimierung durchgeführt hat. Wenn Sie extrem große Textmengen verketten, kann die Verkettung von Zeichenfolgen Sie wirklich verletzen, wenn Sie einfach StringBuilder hätten verwenden können, wie einige hier erwähnt haben.

KyleM
quelle
2

Verwenden Sie den StringBuilder wie vorgeschlagen. Versuchen Sie, die endgültige Zeichenfolgengröße zu schätzen, und verwenden Sie diese Zahl, wenn Sie den StringBuilder instanziieren. StringBuilder sb = neuer StringBuilder (estSize);

Verwenden Sie beim Aktualisieren der TextBox einfach die Zuweisung, z. B.: Textbox.text = sb.ToString ();

Achten Sie auf Cross-Thread-Operationen wie oben. Verwenden Sie jedoch BeginInvoke. Der Hintergrund-Thread muss nicht blockiert werden, während die Benutzeroberfläche aktualisiert wird.

Steven Licht
quelle
1

A) Intro: bereits erwähnt, verwenden StringBuilder

B) Punkt: Nicht zu häufig aktualisieren, dh

DateTime dtLastUpdate = DateTime.MinValue;

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

C) Wenn dies ein einmaliger Auftrag ist, verwenden Sie die x64-Architektur, um innerhalb des 2-GB-Grenzwerts zu bleiben.

bohdan_trotsenko
quelle
1

StringBuilderIn ViewModelvermeidet das Durcheinander von String-Bindungen und bindet es an MyTextBox.Text. Dieses Szenario erhöht die Leistung um ein Vielfaches und verringert die Speichernutzung.

Anatolii Gabuza
quelle
0

Was nicht erwähnt wurde, ist, dass selbst wenn Sie den Vorgang im Hintergrundthread ausführen, die Aktualisierung des UI-Elements selbst auf dem Hauptthread selbst erfolgen muss (in WinForms sowieso).

Haben Sie beim Aktualisieren Ihres Textfelds Code, der aussieht?

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

Wenn ja, dann wird Ihre Hintergrundoperation durch das UI-Update definitiv zum Engpass.

Ich würde vorschlagen, dass Ihr Hintergrundbetrieb StringBuilder wie oben erwähnt verwendet, aber anstatt das Textfeld in jedem Zyklus zu aktualisieren, versuchen Sie, es in regelmäßigen Abständen zu aktualisieren, um festzustellen, ob es die Leistung für Sie erhöht.

BEARBEITEN HINWEIS: WPF nicht verwendet.

Daryl Teo
quelle
0

Sie sagen, das Gedächtnis wächst exponentiell. Nein, es ist ein quadratisches Wachstum , dh ein Polynomwachstum, das nicht so dramatisch ist wie ein exponentielles Wachstum.

Sie erstellen Zeichenfolgen mit der folgenden Anzahl von Elementen:

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

Mit erhalten n = 180,000Sie die Gesamtspeicherzuordnung für 16,200,090,000 items, dh16.2 billion items ! Dieser Speicher wird nicht sofort zugewiesen, aber es ist eine Menge Aufräumarbeiten für den GC (Garbage Collector)!

Beachten Sie außerdem, dass die vorherige Zeichenfolge (die wächst) 179.999 Mal in die neue Zeichenfolge kopiert werden muss. Die Gesamtzahl der kopierten Bytes geht mitn^2 !

Verwenden Sie stattdessen eine ListBox, wie andere vorgeschlagen haben. Hier können Sie neue Zeichenfolgen anhängen, ohne eine große Zeichenfolge zu erstellen. A StringBuildhilft nicht, da Sie auch die Zwischenergebnisse anzeigen möchten.

Olivier Jacot-Descombes
quelle