Dlaczego dołączanie do TextBox.Text podczas pętli zajmuje więcej pamięci przy każdej iteracji?


82

Krótkie pytanie

Mam pętlę, która działa 180 000 razy. Pod koniec każdej iteracji ma dołączyć wyniki do TextBox, które jest aktualizowane w czasie rzeczywistym.

Używanie MyTextBox.Text += someValuepowoduje, że aplikacja zjada ogromne ilości pamięci i po kilku tysiącach rekordów wyczerpuje się dostępna pamięć.

Czy istnieje skuteczniejszy sposób dołączania tekstu do TextBox.Text180 000 razy?

Edytuj Naprawdę nie obchodzi mnie wynik tego konkretnego przypadku, jednak chcę wiedzieć, dlaczego wydaje się to być świnią pamięci i czy istnieje bardziej wydajny sposób dołączania tekstu do TextBox.


Długie (oryginalne) pytanie

Mam małą aplikację, która odczytuje listę numerów identyfikacyjnych w pliku CSV i generuje raport PDF dla każdego z nich. Po wygenerowaniu każdego pliku PDF ResultsTextBox.Textdo raportu dołączany jest numer identyfikacyjny raportu, który został przetworzony i został pomyślnie przetworzony. Proces działa w wątku w tle, więc ResultsTextBox jest aktualizowany w czasie rzeczywistym, gdy elementy są przetwarzane

Obecnie używam aplikacji dla 180 000 numerów identyfikacyjnych, jednak pamięć, którą aplikacja zajmuje, rośnie wykładniczo w miarę upływu czasu. Zaczyna się od około 90 KB, ale przy około 3000 rekordach zajmuje około 250 MB, a przy 4000 rekordów aplikacja zajmuje około 500 MB pamięci.

Jeśli skomentuję aktualizację w polu tekstowym wyników, pamięć pozostaje względnie stacjonarna przy około 90K, więc mogę założyć, że pisanie ResultsText.Text += someValuepowoduje zjadanie pamięci.

Moje pytanie brzmi: dlaczego tak się dzieje? Jaki jest lepszy sposób dołączania danych do TextBox.Text, który nie zużywa pamięci?

Mój kod wygląda tak:

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

Warto też wspomnieć, że aplikacja jest jednorazowa i nie ma znaczenia, że ​​wygenerowanie wszystkich raportów zajmie kilka godzin (lub dni :)). Moim głównym zmartwieniem jest to, że jeśli osiągnie limit pamięci systemowej, przestanie działać.

Nie przeszkadza mi pozostawienie wiersza aktualizującego pole tekstowe wyników, które zostało zakomentowane, aby uruchomić tę funkcję, ale chciałbym wiedzieć, czy istnieje bardziej wydajny sposób dołączania danych do pliku TextBox.Textdla przyszłych projektów.


7
Możesz spróbować użyć a, StringBuilderaby dołączyć tekst, a następnie, po zakończeniu, przypisz StringBuilderwartość do pola tekstowego.
keyboardP

1
Nie wiem, czy cokolwiek by to zmieniło, ale co by było, gdybyś miał StringBuilder, który dołączałby nowe identyfikatory i używałbyś właściwości, która jest aktualizowana o nową wartość konstruktora ciągów i powiązana z twoim textbox.text własność.
BigL

2
Dlaczego inicjalizujesz tablicę obiektów podczas wywoływania string.Format? Istnieją przeciążenia, które przyjmują 2 parametry, dzięki czemu można uniknąć tworzenia tablicy. Dodatkowo, gdy używasz przeciążenia params, tablica jest tworzona dla Ciebie za kulisami.
ChaosPandion,

1
konkatacja ciągów niekoniecznie jest nieefektywna. Jeśli łączysz ciągi w wielu jednostkach pracy i wyświetlasz wyniki między każdą jednostką pracy, będzie to bardziej wydajne niż StringBuilder. StringBuilder jest bardziej efektywny tylko wtedy, gdy tworzysz ciąg znaków za pomocą pętli, a następnie wypisujesz wynik tylko na końcu pętli.
James Michael Hare

3
Chciałem powiedzieć, że to całkiem imponująca maszyna :-)
James Michael Hare

Odpowiedzi:


119

Podejrzewam, że powodem, dla którego zużycie pamięci jest tak duże, jest to, że pola tekstowe utrzymują stos, aby użytkownik mógł cofnąć / ponowić tekst. Ta funkcja nie wydaje się być wymagana w Twoim przypadku, więc spróbuj ustawić wartość IsUndoEnabledfalse.


1
Z linku MSDN: „Wyciek pamięci Jeśli w aplikacji rośnie ilość pamięci, ponieważ bardzo często ustawiasz wartość z kodu, stos cofania bloku tekstowego może być„ wyciekiem ”pamięci. Używając tej właściwości, możesz wyłączyć i oczyszcza drogę do przecieku pamięci ”.
fala

33
W większości przypadków użytkownicy i programiści oczekują, że pole tekstowe będzie działać jak standardowe pola tekstowe (tj. Z możliwością cofania / ponawiania). W skrajnych przypadkach, takich jak wymagania OP, może to okazać się utrudnieniem. Jeśli większość ludzi go używa, powinno to być ustawienie domyślne. Dlaczego miałbyś oczekiwać, że wyjątkowy przypadek zmusi standardową funkcjonalność do akceptacji?
keyboardP

1
Alternatywnie możesz również ustawić UndoLimitrealistyczną wartość. Wartość domyślna -1 oznacza nieograniczony stos. Zero (0) również wyłącza cofanie.
myermian

14

Użyj TextBox.AppendText(someValue)zamiast TextBox.Text += someValue. Łatwo go przeoczyć, ponieważ znajduje się w TextBox, a nie w TextBox.Text. Podobnie jak StringBuilder, pozwoli to uniknąć tworzenia kopii całego tekstu za każdym razem, gdy coś dodasz.

Byłoby interesujące zobaczyć, jak to się ma do IsUndoEnabledflagi z odpowiedzi keyboardP.


W przypadku formularzy Windows jest to najlepsze rozwiązanie, ponieważ formularze Windows nie mają TextBox.IsUndoEnabled
BrDaHa

W formularzach Win masz bool CanUndonieruchomość
imlokesh

9

Nie dołączaj bezpośrednio do właściwości text. Użyj StringBuilder do dołączania, a po zakończeniu ustaw .text na gotowy ciąg z stringbuildera


2
Zapomniałem wspomnieć, że pętla działa w wątku w tle, a wyniki są aktualizowane w czasie rzeczywistym
Rachel

5

Zamiast używać pola tekstowego, zrobiłbym co następuje:

  1. Otwórz plik tekstowy i na wszelki wypadek przesyłaj strumieniowo błędy do pliku dziennika.
  2. Użyj kontrolki pola listy, aby przedstawić błędy, aby uniknąć kopiowania potencjalnie masowych ciągów.

4

Osobiście zawsze używam string.Concat*. Pamiętam, że przeczytałem tutaj pytanie na temat Stack Overflow lata temu, które zawierało statystyki profilowania porównujące powszechnie używane metody i (wydaje się) przypominać, żestring.Concat wygrało.

Niemniej jednak najlepsze, co mogę znaleźć, to to pytanie referencyjne i to pytanie szczegółowe w String.Formatporównaniu zStringBuilder pytaniem, które wspomina, że String.Formatużywa się StringBuilderwewnętrznie. To sprawia, że ​​zastanawiam się, czy twoja pamięć nie leży gdzie indziej.

** Opierając się na komentarzu Jamesa, powinienem wspomnieć, że nigdy nie robię ciężkiego formatowania ciągów, ponieważ koncentruję się na programowaniu opartym na sieci. *


Zgadzam się, czasami ludzie wpadają w rutynę mówiąc „zawsze używaj X, bo X jest najlepsze”, co zwykle jest zbytnim uproszczeniem. Istnieje wiele subtelności między string.Concat (), string.Format () i StringBuilder. Moja praktyczna zasada brzmi: używaj każdego, do czego jest przeznaczony (wiem, brzmi to głupio, ale to prawda). Używam concat, gdy dołączam ciągi (a następnie używam wyniku natychmiast), używam Format, gdy wykonuję nietrywialne formatowanie ciągów (dopełnienie, formaty liczbowe itp.), A StringBuilder do budowania ciągów podczas pętli do być używane na końcu pętli.
James Michael Hare

@JamesMichaelHare, to ma dla mnie sens; czy sugerujesz, że użycie string.Format/ StringBuilderjest tutaj bardziej odpowiednie?
jwheron,

O nie, właśnie zgadzam się z twoim ogólnym stwierdzeniem, że concat jest zwykle najlepszy do prostych konkatacji ciągów. Problem z "praktycznymi regułami" polega na tym, że mogą one zmieniać się z wersji .NET na wersję, jeśli zmieni się BCL, dzięki czemu trzymanie się logicznie poprawnej konstrukcji jest łatwiejsze w utrzymaniu i zwykle lepiej sprawdza się w zadaniach. Właściwie miałem starszy post na blogu, w którym porównałem te trzy tutaj: geekswithblogs.net/BlackRabbitCoder/archive/2010/05/10/…
James Michael Hare

Należycie zanotowane - po prostu chciałem mieć pewność - i zredagowałem odpowiedź, aby uściślić moje użycie słowa „zawsze”.
jwheron

3

Może zrewidować TextBox? ListBox przechowujący elementy ciągu znaków prawdopodobnie będzie działać lepiej.

Ale głównym problemem wydają się być wymagania. Pokazanie 180 000 pozycji nie może być skierowane do użytkownika (człowieka), ani też nie zmienia się tego w czasie rzeczywistym.

Preferowanym sposobem byłoby pokazanie próbki danych lub wskaźnika postępu.

Jeśli chcesz zrzucić go do biednego użytkownika, aktualizacje ciągów wsadowych. Żaden użytkownik nie mógł przeanalizować więcej niż 2 lub 3 zmiany na sekundę. Więc jeśli produkujesz 100 / sekundę, twórz grupy po 50.


Dzięki Henk. To była jednorazowa rzecz, więc pisząc to byłem leniwy. Chciałem jakiegoś wizualnego wyniku, aby wiedzieć, jaki jest stan, i chciałem mieć możliwości zaznaczania tekstu i ScrollBar. Przypuszczam, że mogłem użyć ScrollViewer / Label, ale TextBoxes mają wbudowane ScrollBarrs. Nie spodziewałem się, że spowoduje to problemy :)
Rachel

2

Niektóre odpowiedzi nawiązywały do ​​tego, ale nikt nie wyraził tego wprost, co jest zaskakujące. Ciągi znaków są niezmienne, co oznacza, że ​​nie można zmodyfikować ciągu znaków po jego utworzeniu. Dlatego za każdym razem, gdy łączysz się z istniejącym ciągiem, należy utworzyć nowy obiekt ciągu. Pamięć związana z tym obiektem String również oczywiście musi zostać utworzona, co może stać się kosztowne, gdy Twoje ciągi staną się coraz większe. Na studiach popełniłem kiedyś amatorski błąd, łącząc ciągi znaków w programie Java, który wykonywał kompresję kodu Huffmana. Kiedy konkatenujesz bardzo duże ilości tekstu, konkatenacja String może naprawdę zaszkodzić, gdy mogłeś po prostu użyć StringBuilder, jak niektórzy tutaj wspominali.


2

Użyj StringBuilder zgodnie z sugestią. Spróbuj oszacować ostateczny rozmiar ciągu, a następnie użyj tej liczby podczas tworzenia wystąpienia StringBuilder. StringBuilder sb = new StringBuilder (estSize);

Podczas aktualizacji TextBox po prostu użyj przypisania, np .: textbox.text = sb.ToString ();

Uważaj na operacje między wątkami, jak powyżej. Jednak użyj BeginInvoke. Nie ma potrzeby blokowania wątku w tle podczas aktualizacji interfejsu użytkownika.


1

A) Intro: już wspomniane, użyj StringBuilder

B) Punkt: nie aktualizuj zbyt często, tj

DateTime dtLastUpdate = DateTime.MinValue;

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

C) Jeśli jest to jednorazowe zadanie, użyj architektury x64, aby nie przekraczać limitu 2 GB.


1

StringBuilderin ViewModelpozwoli uniknąć bałaganu z rebindowaniami ciągów i powiązać go z MyTextBox.Text. Ten scenariusz wielokrotnie zwiększy wydajność i zmniejszy użycie pamięci.


0

Coś, o czym nie zostało wspomniane, to fakt, że nawet jeśli wykonujesz operację w wątku w tle, aktualizacja samego elementu UI MUSI nastąpić w samym głównym wątku (i tak w WinForms).

Czy podczas aktualizowania pola tekstowego masz jakiś kod, który wygląda jak

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

Jeśli tak, to Twoja operacja w tle jest zdecydowanie ograniczona przez aktualizację interfejsu użytkownika.

Sugerowałbym, aby operacja w tle korzystała z StringBuilder, jak wspomniano powyżej, ale zamiast aktualizować pole tekstowe w każdym cyklu, spróbuj aktualizować je w regularnych odstępach czasu, aby sprawdzić, czy zwiększa to wydajność.

EDYTUJ UWAGA: nie korzystałem z WPF.


0

Mówisz, że pamięć rośnie wykładniczo. Nie, to kwadratowy wzrost , tj. Wzrost wielomianowy, który nie jest tak dramatyczny jak wzrost wykładniczy.

Tworzysz ciągi zawierające następującą liczbę elementów:

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

Dzięki temu n = 180,000otrzymujesz całkowitą alokację pamięci dla 16,200,090,000 items, np16.2 billion items ! Ta pamięć nie zostanie przydzielona od razu, ale wymaga to dużo pracy związanej z porządkowaniem dla GC (garbage collector)!

Pamiętaj też, że poprzedni ciąg (który rośnie) musi zostać skopiowany do nowego łańcucha 179 999 razy. Wraz z całkowitą liczbą skopiowanych bajtówn^2 !

Jak sugerowali inni, zamiast tego użyj ListBox. Tutaj możesz dołączyć nowe ciągi bez tworzenia ogromnego ciągu. A StringBuildnie pomaga, ponieważ chcesz również wyświetlić wyniki pośrednie.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.