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 += someValue
powoduje, ż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.Text
180 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.Text
do 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 += someValue
powoduje 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.Text
dla przyszłych projektów.
StringBuilder
aby dołączyć tekst, a następnie, po zakończeniu, przypiszStringBuilder
wartość do pola tekstowego.Odpowiedzi:
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ść
IsUndoEnabled
false.źródło
UndoLimit
realistyczną wartość. Wartość domyślna -1 oznacza nieograniczony stos. Zero (0) również wyłącza cofanie.Użyj
TextBox.AppendText(someValue)
zamiastTextBox.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
IsUndoEnabled
flagi z odpowiedzi keyboardP.źródło
bool CanUndo
nieruchomość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
źródło
Zamiast używać pola tekstowego, zrobiłbym co następuje:
źródło
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.Format
porównaniu zStringBuilder
pytaniem, które wspomina, żeString.Format
używa sięStringBuilder
wewnę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. *
źródło
string.Format
/StringBuilder
jest tutaj bardziej odpowiednie?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.
źródło
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.
źródło
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.
źródło
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.
źródło
StringBuilder
inViewModel
pozwoli uniknąć bałaganu z rebindowaniami ciągów i powiązać go zMyTextBox.Text
. Ten scenariusz wielokrotnie zwiększy wydajność i zmniejszy użycie pamięci.źródło
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.
źródło
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,000
otrzymujesz całkowitą alokację pamięci dla16,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ów
n^2
!Jak sugerowali inni, zamiast tego użyj ListBox. Tutaj możesz dołączyć nowe ciągi bez tworzenia ogromnego ciągu. A
StringBuild
nie pomaga, ponieważ chcesz również wyświetlić wyniki pośrednie.źródło