【问题标题】:RegEx, StringBuilder and Large Object Heap FragmentationRegEx、StringBuilder 和大对象堆碎片
【发布时间】:2011-12-22 15:53:33
【问题描述】:

如何在大字符串中运行大量 RegEx(以查找匹配项)而不导致 LOH 碎片?

它是 .NET Framework 4.0,所以我使用的是 StringBuilder,所以它不在 LOH 中,但是一旦我需要在其上运行 RegEx,我必须调用 StringBuilder.ToString(),这意味着它将在 LOH 中.

这个问题有什么解决办法吗?像这样处理大字符串和正则表达式的长时间运行的应用程序几乎是不可能的。

解决这个问题的思路:

在思考这个问题时,我想我找到了一个肮脏的解决方案。

在给定时间我只有 5 个字符串,这 5 个字符串(大于 85KB)将被传递给RegEx.Match

由于新对象不适合 LOH 中的空白空间而发生碎片,这应该可以解决问题:

  1. PadRight 最大字符串。可接受的大小,假设为 1024KB(我可能需要使用 StringBuider 执行此操作)
  2. 通过这样做,所有新字符串都将适合已清空的内存,因为之前的字符串已经超出范围
  3. 不会有任何碎片,因为对象大小始终相同,因此我只会在给定时间分配 1024*5,而 LOH 中的这些空间将在这些字符串之间共享。

我想这个设计的最大问题是如果其他大对象在 LOH 中分配这个位置会导致应用程序分配大量 1024 KB 的字符串可能会产生更严重的碎片。 fixed 语句可能会有所帮助,但是如何在不实际创建不在固定内存地址中的新字符串的情况下将固定字符串发送到 RegEx?

对这个理论有什么想法吗? (不幸的是我不能轻易地重现这个问题,我通常会尝试使用内存分析器来观察变化,并且不确定我可以为此编写什么样的隔离测试用例)

【问题讨论】:

  • 您确定大对象堆正在变得碎片化吗?我对大型(数百千字节)字符串做了很多工作,但从未遇到过 LOH 碎片问题。
  • 是的,我确定。应用程序需要占用大量内存并长时间运行才能看到它的实际影响。如果您确实进行了内存分析,您可能会发现它对您有影响,但不足以让您的应用崩溃。
  • 是的,很简单。一百美元可以给你买一个 64 位的操作系统。没有任何一种编程工作可以与之匹敌。
  • @Hans 我猜你是一个服务器端程序员 :) 这是一个被运送到我无法控制的计算机上的工具。你想让我告诉我的用户他们必须使用 x64 吗?因为不只是100美元?或者可能只是将客户群限制为 x64-Win 7 用户?这更容易,不是吗?
  • 任何软件供应商都会发布先决条件。需要Windows,不能使用Apple,必须能够运行.NET、yada磁盘空间、yada RAM等。这没有什么不同,x64 没有什么特别之处。戴尔过去 3 年的默认选择,无需额外费用。

标签: c# regex memory .net-4.0 large-object-heap


【解决方案1】:

您可以在某个时间点卸载的 AppDomain 中完成您的工作吗?

【讨论】:

  • 问题是您仍然需要共享结果,除非您使用共享存储并直接从内存或文件中以流的形式读取数据,您仍然会遇到同样的问题。因为如果您以任何方式使用远程处理,那么您将再次创建大数组或字符串,这些数组或字符串将转到 LOH,现在在两个应用程序域中。共享内存、内存映射文件等确实是一种解决方案,但在大型应用程序中确实非常复杂,并且性能受到很大影响。
【解决方案2】:

好的,这是我尝试以一种相当通用的方式解决这个问题,但有一些明显的限制。由于我在任何地方都没有看到这个建议,而且每个人都在抱怨 LOH 碎片,所以我想分享代码以确认我的设计和假设是正确的。

理论:

  1. 创建一个共享的海量 StringBuilder(这是为了存储我们从流中读取的大字符串) - new StringBuilder(ChunkSize * 5);
  2. 创建一个大字符串(必须大于最大可接受的大小),应使用空白空间进行初始化。 - 新字符串(' ', ChunkSize * 10);
  3. 将字符串对象固定到内存中,这样 GC 就不会弄乱它。 GCHandle.Alloc(pinnedText, GCHandleType.Pinned)。尽管 LOH 对象通常是固定的,但这似乎可以提高性能。可能是因为unsafe代码
  4. 将流读入共享 StringBuilder,然后使用索引器不安全地将其复制到 pinnedText
  5. 将 pinnedText 传递给 RegEx

有了这个实现,下面的代码就像没有 LOH 分配一样工作。如果我切换到new string(' ') 分配而不是使用静态StringBuilder 或使用StringBuilder.ToString() 代码可以在outofmemory exception 崩溃之前分配300% 更少的内存

我还使用内存分析器确认了结果,即在此实现中没有 LOH 碎片。我仍然不明白为什么 RegEx 不会导致任何意外问题。我还使用不同且昂贵的 RegEx 模式进行了测试,结果是相同的,没有碎片。

代码:

http://pastebin.com/ZuuBUXk3

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;

namespace LOH_RegEx
{
    internal class Program
    {
        private static List<string> storage = new List<string>();
        private const int ChunkSize = 100000;
        private static StringBuilder _sb = new StringBuilder(ChunkSize * 5);


        private static void Main(string[] args)
        {
            var pinnedText = new string(' ', ChunkSize * 10);
            var sourceCodePin = GCHandle.Alloc(pinnedText, GCHandleType.Pinned);

            var rgx = new Regex("A", RegexOptions.CultureInvariant | RegexOptions.Compiled);

            try
            {

                for (var i = 0; i < 30000; i++)
                {                   
                    //Simulate that we read data from stream to SB
                    UpdateSB(i);
                    CopyInto(pinnedText);                   
                    var rgxMatch = rgx.Match(pinnedText);

                    if (!rgxMatch.Success)
                    {
                        Console.WriteLine("RegEx failed!");
                        Console.ReadLine();
                    }

                    //Extra buffer to fragment LoH
                    storage.Add(new string('z', 50000));
                    if ((i%100) == 0)
                    {
                        Console.Write(i + ",");
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                Console.WriteLine("OOM Crash!");
                Console.ReadLine();
            }
        }


        private static unsafe void CopyInto(string text)
        {
            fixed (char* pChar = text)
            {
                int i;
                for (i = 0; i < _sb.Length; i++)
                {
                    pChar[i] = _sb[i];
                }

                pChar[i + 1] = '\0';
            }
        }

        private static void UpdateSB(int extraSize)
        {
            _sb.Remove(0,_sb.Length);

            var rnd = new Random();
            for (var i = 0; i < ChunkSize + extraSize; i++)
            {
                _sb.Append((char)rnd.Next(60, 80));
            }
        }
    }
}

【讨论】:

    【解决方案3】:

    另一种方法是找到某种方法来对基于非数组的数据结构执行正则表达式匹配。不幸的是,在基于流的正则表达式库方面,快速的谷歌并没有带来太多。我猜想 reg-ex 算法需要做很多回溯,而流不支持。

    您绝对需要正则表达式的全部功能吗?您能否实现自己的更简单的搜索函数,这些函数可以处理所有小于 85kb 的字符串链表?

    此外,LOH 碎片只有在您长期持有大对象引用时才会真正引起问题。如果你不断地创造和摧毁它们,那么 LOH 就不应该增长。

    FWIW,我发现 RedGate ANTS memory profiler 非常擅长追踪 LOH 中的对象和碎片级别。

    【讨论】:

    • “LOH 碎片只有在你长时间持有大对象引用时才会真正引起问题”AFAIK 这是不正确的,任何大于 85KB 的东西都将位于 LOH 中,无论它们持有的时间如何。我用的是ANTS Profiler,确实不错。
    • 是的,需要正则表达式的全部功能
    • 抱歉,我的意思是,我只是在长时间保留引用时才发现 LOH 出现问题。你是正确的,任何超过 85k 的东西都会进入 LOH。只是为了让我理解,您的问题是您在 LOH 中的字符串之间分配了其他长期对象,导致您的字符串分配被进一步推到 LOH 上,直到内存不足?
    • 如果是这样,你能阻止这些其他物体进入 LOH 吗? LOH 通常很大,所以如果 only 被分配的东西是你的字符串,并且它们都以相同的速率被取消分配,那么我不会预料到问题。
    • 如果您了解 LOH 碎片,这是一个已知问题。这就是我的问题。
    猜你喜欢
    • 2010-10-15
    • 2012-08-01
    • 1970-01-01
    • 2011-11-14
    • 2011-07-12
    • 1970-01-01
    • 2014-07-14
    • 2018-02-12
    相关资源
    最近更新 更多