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

82

Короткий вопрос

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

Использование MyTextBox.Text += someValueприводит к тому, что приложение съедает огромное количество памяти, и у него заканчивается доступная память после нескольких тысяч записей.

Есть ли более эффективный способ добавления текста до TextBox.Text180 000 раз?

Изменить Меня действительно не волнует результат этого конкретного случая, однако я хочу знать, почему это похоже на потребление памяти, и есть ли более эффективный способ добавления текста в TextBox.


Длинный (оригинальный) вопрос

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

В настоящее время я использую приложение для 180000 идентификационных номеров, однако объем памяти, который занимает приложение, со временем растет экспоненциально. Он начинается примерно с 90 КБ, но примерно на 3000 записей он занимает примерно 250 МБ, а на 4000 записей приложение занимает около 500 МБ памяти.

Если я закомментирую обновление текстового поля результатов, память останется относительно постоянной на уровне примерно 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 });
}

Следует также отметить, что приложение является одноразовым, и не имеет значения, что на создание всех отчетов уйдет несколько часов (или дней :)). Меня больше всего беспокоит то, что если он достигнет предела системной памяти, он перестанет работать.

Я согласен оставить строку, обновляющую текстовое поле результатов, закомментированную, чтобы запустить эту вещь, но я хотел бы знать, есть ли более эффективный способ добавления данных в a TextBox.Textдля будущих проектов.

Рэйчел
источник
7
Вы можете попробовать использовать a StringBuilderдля добавления текста, а затем, по завершении, присвоить StringBuilderзначение текстовому полю.
keyboardP
1
Я не знаю, изменит ли это что-нибудь, но что, если бы у вас был StringBuilder, который добавляет новые идентификаторы, и вы использовали бы свойство, которое обновляется с новым значением построителя строк и привязывает его к своему текстовому полю. Текст свойство.
BigL 04
2
Почему вы инициализируете массив объектов при вызове string.Format? Существуют перегрузки, которые принимают 2 параметра, поэтому вы можете избежать создания массива. Кроме того, когда вы используете перегрузку params, массив создается для вас за кулисами.
ChaosPandion 04
1
String concat не обязательно неэффективен. Если вы объединяете строки в нескольких единицах работы и отображаете результаты между каждой единицей работы, это будет более эффективно, чем StringBuilder. StringBuilder действительно более эффективен только тогда, когда вы строите строку через цикл, а затем записываете результат только в конце цикла.
Джеймс Майкл Хэйр
3
Я собирался сказать, что это впечатляющая машина :-)
Джеймс Майкл Хэйр

Ответы:

119

Я подозреваю, что использование памяти настолько велико, потому что текстовые поля поддерживают стек, чтобы пользователь мог отменить / повторить текст. В вашем случае эта функция не требуется, поэтому попробуйте установить IsUndoEnabledзначение false.

клавиатура P
источник
1
Из ссылки MSDN: «Утечка памяти. Если в вашем приложении увеличивается объем памяти, потому что вы очень часто устанавливаете значение из кода, стек отмены текстового блока может быть« утечкой »памяти. Используя это свойство, вы можете отключить это и очистить путь к утечке памяти ".
волна
33
В большинстве случаев пользователи и разработчики ожидают, что текстовое поле будет работать как стандартные текстовые поля (то есть с возможностью отмены / повторения). В крайних случаях, таких как требования OP, это может оказаться помехой. Если большинство людей используют его, то он должен быть установлен по умолчанию. Почему вы ожидаете, что крайний случай заставит стандартные функции стать необязательными?
keyboardP
1
Кроме того, вы также можете установить UndoLimitреалистичное значение. Значение по умолчанию -1 указывает на неограниченный стек. Ноль (0) также отключит отмену.
myermian
14

Используйте TextBox.AppendText(someValue)вместо TextBox.Text += someValue. Его легко пропустить, поскольку он находится в TextBox, а не в TextBox.Text. Как и StringBuilder, это позволит избежать создания копий всего текста каждый раз, когда вы что-то добавляете.

Было бы интересно посмотреть, как это соотносится с IsUndoEnabledфлагом из ответа keyboardP.

Cypher2100
источник
В случае форм Windows это лучшее решение, поскольку формы Windows не имеют TextBox.IsUndoEnabled
BrDaHa
В формах Win у вас есть bool CanUndoсвойство
imlokesh
9

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

Darthg8r
источник
2
Я забыл упомянуть, что цикл выполняется в фоновом потоке, а результаты обновляются в реальном времени
Рэйчел
5

Вместо текстового поля я бы сделал следующее:

  1. Откройте текстовый файл и на всякий случай перенесите ошибки в файл журнала.
  2. Используйте элемент управления в виде списка для представления ошибок, чтобы избежать копирования потенциально массивных строк.
ХаосПандион
источник
4

Лично я всегда использую string.Concat*. Я помню, как много лет назад читал здесь вопрос о переполнении стека, в котором была статистика профилирования, сравнивающая часто используемые методы, и (похоже) вспомнил, что это string.Concatпобедило.

Тем не менее, лучшее, что я могу найти, - это этот справочный вопрос и этот конкретный вопрос String.Formatvs.StringBuilder , в котором упоминается, что внутренне String.Formatиспользуется StringBuilder. Это заставляет меня задуматься, а не где-нибудь твоя память.

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

Jwheron
источник
Я согласен, иногда люди попадают в колею, говоря «всегда используйте X, потому что X лучше», что обычно является чрезмерным упрощением. Между string.Concat (), string.Format () и StringBuilder много тонкости. Мое практическое правило - использовать все, для чего оно предназначено (я знаю, это звучит глупо, но это правда). Я использую concat, когда присоединяюсь к строкам (а затем сразу использую результат), я использую Format, когда выполняю нетривиальное форматирование строк (заполнение, числовые форматы и т. Д.), И StringBuilder для построения строк во время цикла, чтобы использоваться в конце цикла.
Джеймс Майкл Хэйр
@JamesMichaelHare, для меня это имеет смысл; Вы полагаете, что здесь более уместно использование string.Format/ StringBuilder?
jwheron
О нет, я просто согласился с вашим общим мнением о том, что concat обычно лучше всего подходит для простых конкатенаций строк. Проблема с «практическими правилами» заключается в том, что они могут меняться от версии .NET к версии, если BCL изменяется, поэтому использование логически правильной конструкции более удобно в обслуживании и обычно лучше выполняет свои задачи. На самом деле у меня был более старый пост в блоге, где я сравнивал эти три здесь: geekswithblogs.net/BlackRabbitCoder/archive/2010/05/10/…
Джеймс Майкл Хэйр
Принято к сведению - просто хотел убедиться - и ответ отредактирован, чтобы уточнить, что я использую слово «всегда».
jwheron
3

Может быть, текстовое поле пересмотреть? ListBox, содержащий строковые элементы, вероятно, будет работать лучше.

Но основная проблема, похоже, заключается в требованиях. Отображение 180 000 элементов не может быть нацелено на пользователя (человека), равно как и не может быть изменено в «Реальном времени».

Предпочтительным способом было бы показать образец данных или индикатор прогресса.

Если вы хотите сбросить его на бедного пользователя, выполните пакетное обновление строки. Ни один пользователь не мог заметить более 2 или 3 изменений в секунду. Так что если вы производите 100 секунд в секунду, делайте группы по 50 штук.

Хенк Холтерман
источник
Спасибо, Хенк. Это было разовое дело, поэтому писать я ленился. Мне нужен был какой-то визуальный вывод, чтобы знать, каков статус, и мне нужны были возможности выбора текста и полоса прокрутки. Полагаю, я мог бы использовать ScrollViewer / Label, но в TextBoxes встроены ScrollBarrs. Я не ожидал, что это вызовет проблемы :)
Рэйчел
2

В некоторых ответах это упоминалось, но никто не сказал об этом прямо, что удивительно. Строки неизменяемы, что означает, что строка не может быть изменена после ее создания. Следовательно, каждый раз, когда вы присоединяетесь к существующей String, необходимо создавать новый объект String. Очевидно, также необходимо создать память, связанную с этим строковым объектом, что может стать дорогостоящим по мере того, как ваши строки становятся все больше и больше. В колледже я однажды совершил любительскую ошибку, объединив строки в программе Java, которая выполняла сжатие кода Хаффмана. Когда вы объединяете очень большие объемы текста, объединение строк может действительно навредить вам, если вы могли бы просто использовать StringBuilder, как некоторые здесь упоминали.

KyleM
источник
2

Используйте StringBuilder, как было предложено. Попробуйте оценить окончательный размер строки, а затем используйте это число при создании экземпляра StringBuilder. StringBuilder sb = новый StringBuilder (estSize);

При обновлении TextBox просто используйте присваивание, например: textbox.text = sb.ToString ();

Следите за межпотоковыми операциями, как указано выше. Однако используйте BeginInvoke. Нет необходимости блокировать фоновый поток во время обновления пользовательского интерфейса.

Стивен Лихт
источник
1

A) Intro: уже упоминалось, используйте StringBuilder

Б) Пункт: не обновляйте слишком часто, т.е.

DateTime dtLastUpdate = DateTime.MinValue;

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

C) Если это разовая работа, используйте архитектуру 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 (сборщика мусора)!

Также имейте в виду, что предыдущая строка (которая растет) должна быть скопирована в новую строку 179 999 раз. Общее количество скопированных байтов соответствуетn^2 тоже !

Как предлагали другие, используйте вместо этого ListBox. Здесь вы можете добавлять новые строки, не создавая огромных строк. A StringBuildне помогает, поскольку вы также хотите отображать промежуточные результаты.

Оливье Жако-Декомб
источник