【问题标题】:Why does appending to TextBox.Text during a loop take up more memory with each iteration?为什么在循环期间附加到 TextBox.Text 每次迭代都会占用更多内存?
【发布时间】:2012-02-02 18:27:12
【问题描述】:

小问题

我有一个运行 180,000 次的循环。在每次迭代结束时,它应该将结果附加到一个实时更新的文本框。

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

有没有更有效的方法将文本附加到TextBox.Text 180,000 次?

编辑我真的不关心这个特定案例的结果,但是我想知道为什么这似乎是一个内存猪,以及是否有更有效的方法来附加文本到一个文本框。


长(原始)问题

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

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

如果我注释掉结果文本框的更新,内存在大约 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 以供将来的项目使用。

【问题讨论】:

  • 您可以尝试使用StringBuilder 附加文本,然后在完成后将StringBuilder 值分配给文本框。
  • 我不知道它是否会改变任何东西,但如果你有一个附加新 Id-s 的 StringBuilder 并且你将使用一个属性,该属性会使用字符串生成器的新值进行更新并将其绑定到您的 textbox.text 属性。
  • 为什么在调用string.Format时要初始化一个对象数组?有些重载需要 2 个参数,因此您可以避免创建数组。另外,当您使用参数重载时,会在幕后为您创建数组。
  • 字符串连接不一定是低效的。如果您要跨多个工作单元连接字符串并在每个工作单元之间显示结果,它将比 StringBuilder 更有效。当您通过循环构建字符串然后只在循环结束时写出结果时,StringBuilder 确实更有效。
  • 我想说的是,这台机器令人印象深刻:-)

标签: c# wpf


【解决方案1】:

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

【讨论】:

  • 来自 MSDN 链接:“内存泄漏如果您的应用程序中的内存增加,因为您经常从代码中设置值,那么文本块的撤消堆栈可能是内存的“泄漏” . 通过使用此属性,您可以禁用它并清除内存泄漏的方式。"
  • 大多数时候,用户和开发人员希望文本框能够像标准文本框一样运行(即具有撤消/重做的能力)。在诸如 OP 要求之类的边缘情况下,它可以证明是一个障碍。如果大多数人使用它,那么它应该是默认的。为什么您会期望边缘案例强制标准功能成为可选功能?
  • 或者,您也可以将UndoLimit 设置为实际值。默认值 -1 表示无限堆栈。零 (0) 也将禁用撤消。
【解决方案2】:

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

看看这与keyboardP的答案中的IsUndoEnabled标志相比会很有趣。

【讨论】:

  • 对于 Windows 窗体,这是最好的解决方案,因为 Windows 窗体没有 TextBox.IsUndoEnabled
  • 在 Win 表单中,您有一个 bool CanUndo 属性
【解决方案3】:

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

【讨论】:

  • 我忘了提到循环在后台线程上运行,结果会实时更新
【解决方案4】:

我会执行以下操作,而不是使用文本框:

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

【讨论】:

    【解决方案5】:

    就我个人而言,我总是使用 string.Concat* 。我记得几年前在 Stack Overflow 上读过一个问题,其中有比较常用方法的分析统计数据,并且(似乎)记得 string.Concat 赢了。

    尽管如此,我能找到的最好的是this reference question 和这个特定的String.Format vs. StringBuilder 问题,其中提到String.Format 在内部使用StringBuilder。这让我想知道你的记忆猪是否在其他地方。

    **根据 James 的评论,我应该提到我从不进行繁重的字符串格式化,因为我专注于基于 Web 的开发。*

    【讨论】:

    • 我同意,有时人们会说“总是使用 X,因为 X 是最好的”,这通常过于简单化了。 string.Concat()、string.Format() 和 StringBuilder 之间有很多微妙之处。我的经验法则是使用每个它的用途(这听起来很傻,我知道,但它是正确的)。我在连接字符串时使用 concat(然后立即使用结果),在执行非平凡字符串格式化(填充、数字格式等)时使用 Format,在循环期间使用 StringBuilder 构建字符串以在循环结束时使用。
    • @JamesMichaelHare,这对我来说很有意义;您是否建议在这里使用string.Format/StringBuilder 更合适?
    • 哦不,我只是同意你的一般观点,即 concat 通常最适合简单的字符串 concat。 “经验法则”的问题在于,如果 BCL 发生更改,它们可以从 .NET 版本更改为另一个版本,因此坚持使用逻辑正确的构造更易于维护,并且通常可以更好地执行其任务。实际上,我有一篇较旧的博客文章,我在这里比较了这三个:geekswithblogs.net/BlackRabbitCoder/archive/2010/05/10/…
    • 适当注明——只是想确定一下——并编辑回答以限定我对“总是”一词的使用。
    【解决方案6】:

    也许重新考虑文本框?保存字符串 Items 的 ListBox 可能会执行得更好。

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

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

    当您确实想将其转储给可怜的用户时,批量字符串更新。没有用户可以每秒进行超过 2 或 3 次更改。因此,如果您每秒生产 100 个,则以 50 个为一组。

    【讨论】:

    • 谢谢亨克。这是一次性的事情,所以我在写它时很懒惰。我想要某种视觉输出来了解状态是什么,并且我想要文本选择功能和一个 ScrollBar。我想我可以使用 ScrollViewer/Label,但是 TextBoxes 内置了 ScrollBarrs。我没想到它会导致问题 :)
    【解决方案7】:

    有些回应暗示了这一点,但没有人直言不讳,这令人惊讶。 字符串是不可变的,这意味着字符串在创建后无法修改。因此,每次连接到现有字符串时,都需要创建一个新的字符串对象。与该字符串对象关联的内存显然也需要创建,随着字符串变得越来越大,这可能会变得昂贵。在大学里,我曾经犯过一个业余错误,即在一个进行 Huffman 编码压缩的 Java 程序中连接字符串。当您连接大量文本时,如果您可以简单地使用 StringBuilder,那么字符串连接真的会伤害您,正如这里的一些人所提到的那样。

    【讨论】:

      【解决方案8】:

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

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

      如上所述注意跨线程操作。但是使用 BeginInvoke。无需阻塞 UI 更新时的后台线程。

      【讨论】:

        【解决方案9】:

        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 限制内。

        【讨论】:

          【解决方案10】:

          ViewModel 中的StringBuilder 将避免字符串重新绑定混乱并将其绑定到MyTextBox.Text。这种方案将性能提高很多倍并减少内存使用量。

          【讨论】:

            【解决方案11】:

            没有提到的是,即使您在后台线程中执行操作,UI 元素本身的更新也必须发生在主线程本身(无论如何在 WinForms 中)。

            更新你的文本框时,你有没有类似的代码

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

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

            我建议您的后台操作使用上面提到的 StringBuilder,但不要在每个周期都更新文本框,而是尝试定期更新它,看看它是否会为您提高性能。

            编辑注意:没有使用过WPF。

            【讨论】:

              【解决方案12】:

              你说内存呈指数增长。不,它是quadratic growth,即多项式增长,不像指数增长那么剧烈。

              您正在创建包含以下项目数量的字符串:

              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。在这里,您可以在不创建巨大字符串的情况下追加新字符串。 StringBuild 无济于事,因为您还想显示中间结果。

              【讨论】:

                猜你喜欢
                • 2016-07-04
                • 2021-10-14
                • 1970-01-01
                • 2015-05-18
                • 1970-01-01
                • 1970-01-01
                • 2011-08-16
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多