Чому додавання до TextBox.Text під час циклу займає більше пам'яті з кожною ітерацією?


82

Коротке запитання

У мене є цикл, який запускається 180 000 разів. В кінці кожної ітерації передбачається додавати результати до TextBox, який оновлюється в режимі реального часу.

Використання MyTextBox.Text += someValueзмушує програму з’їдати величезну кількість пам’яті, і вона втрачає доступну пам’ять через кілька тисяч записів.

Чи існує більш ефективний спосіб додавання тексту до TextBox.Text180 000 разів?

Редагувати Мені справді байдуже про результат цього конкретного випадку, однак я хочу знати, чому це, здається, свиня пам’яті, і чи є більш ефективний спосіб додавання тексту до TextBox.


Довге (оригінальне) запитання

У мене є невеликий додаток, який читає список ідентифікаційних номерів у файлі CSV і створює звіт PDF для кожного з них. Після створення кожного pdf-файлу ResultsTextBox.Textдодається додається ідентифікаційний номер звіту, який був оброблений, і що його було успішно оброблено. Процес виконується у фоновому потоці, тому ResultsTextBox оновлюється в режимі реального часу в міру обробки елементів

Зараз я запускаю програму проти 180 000 ідентифікаційних номерів, однак пам’ять, яку забирає програма, з часом зростає в геометричній прогресії. Він починається приблизно з 90 тис., Але приблизно до 3000 записів займає приблизно 250 Мб, а до 4000 записів програма займає близько 500 Мб пам'яті.

Якщо я коментую оновлення результатів TextBox, пам’ять залишається відносно нерухомою і становить приблизно 90 КБ, тож я можу припустити, що саме написання ResultsText.Text += someValueзмушує його з’їдати пам’ять.

Моє запитання: чому це? Який кращий спосіб додавання даних до TextBox.Text, який не з’їдає пам’ять?

Мій код виглядає так:

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

Слід також зазначити, що додаток - це разова справа, і не має значення, що для створення всіх звітів знадобиться кілька годин (або днів :)). Моє головне занепокоєння полягає в тому, що якщо він досягне обмеження системної пам’яті, він перестане працювати.

Я чудово залишаю рядок для оновлення результатів, який TextBox прокоментував для запуску цієї речі, але я хотів би знати, чи існує більш ефективний спосіб додавання даних до даних TextBox.Textдля майбутніх проектів.


7
Ви можете спробувати використовувати a StringBuilderдля додавання тексту, після чого, після завершення, присвоїти StringBuilderзначення текстовому полі.
клавіатура

1
Я не знаю, чи це щось змінить, але що, якби у вас був StringBuilder, який додає нові ідентифікатори, і ви б використовували властивість, яка оновлюється новим значенням конструктора рядків, і прив’язати це до вашого textbox.text майно.
BigL

2
Чому ви ініціалізуєте масив об’єктів під час виклику string.Format? Є перевантаження, які приймають 2 параметри, щоб уникнути створення масиву. Плюс, коли ви використовуєте параметри перевантаження, масив створюється для вас за кадром.
ChaosPandion

1
рядок concat не обов'язково неефективний. Якщо ви об'єднуєте рядки в декілька одиниць роботи та відображаєте результати між кожною одиницею роботи, це буде ефективніше, ніж StringBuilder. StringBuilder насправді ефективніший лише тоді, коли ви будуєте рядок через цикл, а потім виписуєте результат лише в кінці циклу.
James Michael Hare

3
Я збирався сказати, це досить вражаюча машина :-)
Джеймс Майкл Харе

Відповіді:


119

Я підозрюю, що причина використання пам'яті настільки велика, що текстові поля підтримують стек, щоб користувач міг скасувати / повторити текст. Здається, ця функція не потрібна у вашому випадку, тому спробуйте встановити IsUndoEnabledзначення false.


1
З посилання MSDN: "Витік пам'яті. Якщо у вашому додатку збільшується пам'ять, оскільки ви дуже часто встановлюєте значення з коду, стек скасування текстового блоку може бути" витоком "пам'яті. За допомогою цієї властивості ви можете вимкнути це і розчищає шлях до витоку пам'яті ".
хвиля

33
Більшість випадків користувачі та розробники очікували, що текстове поле буде працювати як стандартні текстові поля (тобто з можливістю скасування / повторення). У таких крайніх випадках, як вимоги ОП, це може виявитись перешкодою. Якщо більшість людей користуються ним, тоді це має бути за замовчуванням. Чому ви очікуєте, що крайній випадок змусить стандартну функціональність стати учасником?
клавіатура

1
Крім того, ви також можете встановити UndoLimitреалістичне значення. За замовчуванням -1 вказує необмежений стек. Нуль (0) також вимкне скасування.
myermian

14

Використовуйте TextBox.AppendText(someValue)замість TextBox.Text += someValue. Це легко пропустити, оскільки це на TextBox, а не на TextBox.Text. Як і StringBuilder, це дозволить уникнути створення копій всього тексту кожного разу, коли ви щось додаєте.

Було б цікаво побачити, як це порівнюється із IsUndoEnabledпрапором з відповіді клавіатури P.


У випадку з формами Windows це найкраще рішення, оскільки у вікнах форм немає TextBox.IsUndoEnabled
BrDaHa

У формах Win ви маєте bool CanUndoвластивість
imlokesh

9

Не додавати безпосередньо до властивості text. Використовуйте StringBuilder для додавання, а потім, закінчивши, встановіть .text на готовий рядок з конструктора рядків


2
Я забув згадати про цикл, що виконується у фоновому потоці, і результати оновлюються в режимі реального часу
Рейчел

5

Замість використання текстового поля я б зробив наступне:

  1. Відкрийте текстовий файл і передайте помилки у файл журналу про всяк випадок.
  2. Використовуйте елемент керування списком для представлення помилок, щоб уникнути копіювання потенційно масивних рядків.

4

Особисто я завжди використовую string.Concat*. Я пам’ятаю, як читав тут запитання про Stack Overflow багато років тому, в якому була статистична статистика, що порівнює загальновживані методи, і (мабуть) нагадувати про string.Concatце.

Тим не менше, найкраще, що я можу знайти, - це довідкове запитання та це конкретне String.FormatпротиStringBuilder запитання, де згадується, що String.Formatвикористовується StringBuilderвнутрішньо. Це змушує мене замислюватися, чи не лежить у вас ще одна пам’ять?

** на основі коментаря Джеймса, я повинен згадати, що я ніколи не роблю важкого форматування рядків, оскільки я зосереджуюсь на веб-розробці. *


Я згоден, іноді люди впадають у колію, кажучи "завжди використовувати X, тому що X найкращий", що, як правило, надто спрощує. Між string.Concat (), string.Format () та StringBuilder є багато тонкощів. Моє принципове правило - використовувати кожен, для чого він призначений (я знаю, це звучить безглуздо, але це справедливо). Я використовую concat, коли приєднуюсь до рядків (а потім негайно використовую результат), використовую формат, коли виконую нетривіальне форматування рядків (відступ, числові формати тощо), і StringBuilder для побудови рядків під час циклу до використовувати в кінці циклу.
James Michael Hare

@JamesMichaelHare, це для мене має сенс; Ви припускаєте, що використання string.Format/ StringBuilderдоцільніше тут?
jwheron

О ні, я просто погоджувався з вашим загальним твердженням, що concat, як правило, найкращий для простих рядкових кокатів. Проблема "основних правил" полягає в тому, що вони можуть змінюватися від версії .NET до версії, якщо BCL змінюється, тому дотримання логічно правильної конструкції є більш ремонтопридатним і, як правило, ефективніше виконує свої завдання. Я насправді мав старіший допис у блозі, де я порівняв три тут: geekswithblogs.net/BlackRabbitCoder/archive/2010/05/10/…
Джеймс Майкл Харе

Зазначений належним чином - просто хотів бути впевненим - і відповідь відредаговано, щоб кваліфікувати моє вживання слова "завжди".
jwheron

3

Можливо, переглянути TextBox? Елементи утримуючого рядка ListBox, ймовірно, будуть працювати ефективніше.

Але основною проблемою, здається, є вимоги: показ 180 000 предметів не може бути орієнтований на (людину) користувача, і це не змінює його в "Реальному часі".

Кращим способом буде показати зразок даних або показник прогресу.

Коли ви хочете скинути його на бідного користувача, пакетне оновлення рядків. Жоден користувач не міг визначити більше 2 або 3 змін в секунду. Тож якщо ви виробляєте 100 на секунду, зробіть групи по 50.


Дякую Хенк. Це було одноразово, тому я лінувався, коли писав це. Я хотів отримати якийсь візуальний вивід, щоб знати, який статус, і я хотів можливості вибору тексту та смугу прокрутки. Припускаю, що я міг використовувати ScrollViewer / Label, але в TextBoxes є вбудовані ScrollBarrs. Я не сподівався, що це спричинить проблеми :)
Рейчел

2

Деякі відповіді на це натякають, але ніхто прямо не заявив, що дивно. Рядки незмінні, що означає, що рядок не може бути змінений після його створення. Отже, кожного разу, коли ви об’єднуєтесь із існуючим рядком, потрібно створювати новий об’єкт рядка. Очевидно також потрібно створити пам'ять, пов'язану з цим рядковим об'єктом, яка може стати дорогою, оскільки ваші рядки стають все більшими та більшими. У коледжі я якось припустився аматорської помилки, об'єднавши рядки в програмі Java, яка зробила стиснення кодування Хаффмана. Коли ви конкатенуєте надзвичайно великі обсяги тексту, конкатенація рядків може по-справжньому нашкодити вам, коли ви могли б просто використовувати StringBuilder, як вже згадували деякі тут.


2

Використовуйте StringBuilder, як запропоновано. Спробуйте оцінити кінцевий розмір рядка, а потім використовуйте це число під час створення екземпляра StringBuilder. StringBuilder sb = новий StringBuilder (estSize);

Під час оновлення TextBox просто використовуйте призначення, наприклад: textbox.text = sb.ToString ();

Слідкуйте за поперечними потоковими операціями, як зазначено вище. Однак використовуйте BeginInvoke. Не потрібно блокувати фоновий потік під час оновлення інтерфейсу.


1

А) Вступ: вже згадано, використовуйте StringBuilder

Б) Суть: не оновлювати занадто часто, тобто

DateTime dtLastUpdate = DateTime.MinValue;

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

В) Якщо це одноразова робота, використовуйте архітектуру x64, щоб не перевищувати 2 Гб.


1

StringBuilderin ViewModelдозволить уникнути переплутань рядків, які заплутають і прив’яжуть його до MyTextBox.Text. Цей сценарій багаторазово збільшить продуктивність та зменшить використання пам'яті.


0

Щось, про що не згадувалося, так це те, що навіть якщо ви виконуєте операцію у фоновому потоці, оновлення самого елемента інтерфейсу має відбуватися в самому основному потоці (у будь-якому випадку в WinForms).

Чи оновлюєте ви своє текстове поле під час оновлення текстового поля?

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

Якщо так, то ваш фоновий досвід, безумовно, є вузьким місцем через оновлення інтерфейсу користувача.

Я б запропонував, щоб ваш фоновий оператор використовував StringBuilder, як зазначено вище, але замість того, щоб оновлювати текстове поле кожного циклу, спробуйте оновлювати його через рівні проміжки часу, щоб перевірити, чи підвищує це для вас продуктивність.

ПРИМІТКА ДЛЯ РЕДАКТУВАННЯ: не використовували WPF.


0

Кажете, пам’ять зростає в геометричній прогресії. Ні, це квадратичне зростання , тобто поліноміальне зростання, яке не таке різке, як експоненціальне зростання.

Ви створюєте рядки, що містять таку кількість елементів:

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

Отримавши n = 180,000загальний обсяг пам'яті для 16,200,090,000 items, тобто16.2 billion items ! Ця пам'ять не буде виділена відразу, але це велика робота по очищенню для GC (збирача сміття)!

Також майте на увазі, що попередній рядок (який зростає) потрібно скопіювати в новий рядок 179999 разів. Загальна кількість скопійованих байтів відповідаєn^2 також відповідає!

Як пропонували інші, замість цього використовуйте ListBox. Тут ви можете додавати нові рядки, не створюючи величезного рядка. A StringBuildне допомагає, оскільки ви хочете також відображати проміжні результати.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.