为什么在循环期间追加到TextBox.Text每次迭代都占用更多内存?


82

简短问题

我有一个运行18万次的循环。在每次迭代结束时,都应将结果附加到TextBox,并实时更新。

使用MyTextBox.Text += someValue将导致应用程序占用大量内存,并且在记录了数千条之后耗尽了可用内存。

有没有一种更有效的方式将文本附加到TextBox.Text18万次?

编辑我真的不在乎这种特殊情况的结果,但是我想知道为什么这似乎是一个内存消耗,以及是否存在一种将文本追加到TextBox的更有效的方法。


长(原始)问题

我有一个小应用程序,可以读取CSV文件中的ID号列表,并为每个文件生成PDF报告。生成每个pdf文件后,将在其后ResultsTextBox.Text附加已处理且已成功处理的报告的ID号。该进程在后台线程上运行,因此,在处理项目时,ResultsTextBox会实时更新

我目前正在针对180,000个ID号运行该应用程序,但是随着时间的流逝,该应用程序占用的内存呈指数增长。它的开始时间约为90K,但是大约3000条记录占用了大约250MB的内存,而4000条记录则占用了大约500 MB的内存。

如果我注释掉对“结果”文本框的更新,则内存保持相对稳定,大约为90K,因此我可以假定写入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.Text以后项目的更有效内存的方法。


7
您可以尝试使用aStringBuilder附加文本,然后在完成后将StringBuilder值分配给文本框。
keyboardP

1
我不知道它是否会改变任何东西,但是如果您有一个附加新ID-s的StringBuilder并使用一个将其更新为字符串生成器新值并将其绑定到文本框的属性该怎么办?属性。
BigL 2012年

2
为什么在调用string.Format时初始化对象数组?有两个参数的重载,因此您可以避免创建数组。另外,当您使用参数重载时,会在后台为您创建数组。
ChaosPandion 2012年

1
字符串concat不一定效率低下。如果要在多个工作单元之间串联字符串并在每个工作单元之间显示结果,则它将比StringBuilder更有效。实际上,只有在通过循环构建字符串然后仅在循环结束时写出结果时,StringBuilder才真正有效。
James Michael Hare 2012年

3
我要说的是,这真是令人印象深刻的机器:-)
James Michael Hare 2012年

Answers:


119

我怀疑内存使用量如此之大是因为文本框维护一个堆栈,以便用户可以撤消/重做文本。您似乎不需要该功能,因此请尝试将其设置IsUndoEnabled为false。


1
从MSDN链接:“内存泄漏如果由于经常设置代码中的值而导致应用程序中的内存增加,则文本块的撤消堆栈可能是内存的“泄漏”。通过使用此属性,您可以禁用并清除内存泄漏的方式。”

33
在大多数情况下,用户和开发人员都希望文本框的功能类似于标准文本框(即具有撤消/重做功能)。在OP的要求之类的极端情况下,这可能会成为障碍。如果大多数人使用它,那么它应该是默认值。您为什么期望边缘情况迫使标准功能成为自愿加入?
keyboardP

1
另外,您也可以将设置UndoLimit为实际值。默认值-1表示无限制的堆栈。零(0)也将禁用撤消。
myermian 2012年

14

使用TextBox.AppendText(someValue)代替TextBox.Text += someValue。由于它位于TextBox而不是TextBox.Text上,因此很容易错过。与StringBuilder一样,这将避免每次添加内容时都创建整个文本的副本。

看到这与IsUndoEnabledkeyboardP答案中的标志相比如何会很有趣。


对于Windows窗体,这是最好的解决方案,因为Windows窗体没有TextBox.IsUndoEnabled
BrDaHa

在Win表格中,您拥有bool CanUndo财产
imlokesh 2015年

9

不要直接附加到text属性。使用StringBuilder进行附加,然后在完成后,将.text设置为来自stringbuilder的最终字符串


2
我忘了提到循环在后台线程上运行,并且结果实时更新
Rachel

5

代替使用文本框,我将执行以下操作:

  1. 以防万一,打开一个文本文件并将错误流式传输到日志文件。
  2. 使用列表框控件表示错误,以避免复制潜在的大量字符串。

4

我个人总是使用string.Concat*。我记得几年前在Stack Overflow上读过一个问题,该问题具有比较常用方法的性能分析统计数据,并且(似乎)想起了这一点string.Concat

尽管如此,我能找到的最好的是这个参考问题和这个特定String.FormatStringBuilder问题的问题,其中提到String.FormatStringBuilder内部使用。这使我想知道您的记忆猪是否在其他地方。

**基于James的评论,我应该提一下,因为我专注于基于Web的开发,所以我从不进行繁琐的字符串格式化。*


我同意,有时候人们会说“总是使用X cuz X是最好的”,这通常是过分简单化了。string.Concat(),string.Format()和StringBuilder之间有很多微妙之处。我的经验法则是使用每种用途(我知道这听起来很傻,但事实如此)。我在连接字符串时使用concat(然后立即使用结果),在执行非平凡的字符串格式设置(填充,数字格式等)时使用Format,并在循环中使用StringBuilder建立字符串在循环结束时使用。
詹姆斯·迈克尔·黑尔

@JamesMichaelHare,这对我来说很有意义;您是否建议在这里使用string.Format/StringBuilder更合适?
jwheron 2012年

哦,不,我只是同意您的一般观点,即concat通常最适合简单的字符串concat。“经验法则”的问题在于,如果BCL更改,它们可以在.NET版本之间进行更改,因此坚持使用逻辑上正确的结构更可维护,并且通常在执行其任务时表现更好。我实际上有一个较旧的博客文章,我在这里比较了这三个地方:geekswithblogs.net/BlackRabbitCoder/archive/2010/05/10/…–
James Michael Hare

适当地注明了-只是想确定-并编辑了答案以限定我对“始终”一词的使用。
jwheron 2012年

3

也许重新考虑TextBox?包含字符串Items的ListBox可能会表现更好。

但是主要的问题似乎是要求:显示180,000个项目不能针对(人类)用户,也不能在“实时”中进行更改。

最好的方法是显示数据样本或进度指示器。

当您确实想将其转储给不良用户时,将更新批处理字符串。没有用户可以每秒发现超过2或3个更改。因此,如果您每秒产生100个,请每组50个。


谢谢亨克。这是一次性的事情,所以我在写时很懒。我想要某种视觉输出来了解状态,并且我想要文本选择功能和ScrollBar。我想我可以使用ScrollViewer / Label,但是TextBoxes内置了ScrollBarrs。我没想到会引起问题:)
Rachel 2012年

2

有人提到了它,但没有人直接说出来,这令人惊讶。字符串是不可变的,这意味着创建后无法修改字符串。因此,每次连接到现有String时,都需要创建一个新的String Object。显然还需要创建与该String对象关联的内存,当您的String变得越来越大时,该内存可能会变得昂贵。在大学里,我曾经犯过一个业余错误,即在执行霍夫曼编码压缩的Java程序中串联字符串。当您串联大量文本时,如果您仅使用StringBuilder(如本文中的某些内容所述),那么字符串串联确实会伤害您。


2

按照建议使用StringBuilder。尝试估计最终的字符串大小,然后在实例化StringBuilder时使用该数字。StringBuilder sb = new StringBuilder(estSize);

更新TextBox时,只需使用赋值即可,例如:textbox.text = sb.ToString();

注意上面的跨线程操作。但是,请使用BeginInvoke。UI更新时无需阻止后台线程。


1

A)介绍:已经提到,使用 StringBuilder

B)点:不要更新太频繁,即

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架构将其限制在2Gb以内。


1

StringBuilderinViewModel将避免字符串重新绑定混乱,并将其绑定到MyTextBox.Text。这种情况将使性能提高很多倍,并减少内存使用量。


0

尚未提及的是,即使您在后台线程中执行操作,UI元素本身的更新也必须在主线程本身上进行(无论如何在WinForms中)。

更新文本框时,您是否有任何看起来像这样的代码

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

如果是这样,那么您的后台操作肯定会受到UI更新的限制。

我建议您的背景操作如上所述使用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。在这里,您可以追加新字符串而无需创建巨大的字符串。AStringBuild没有帮助,因为您也想显示中间结果。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.