GZIP解压缩C# OutOfMemory

7

我有许多大的gzip文件(大约10MB到200MB),是我从ftp下载的需要解压缩的文件。

于是,我试图在谷歌上找到一些gzip解压缩的解决方案。

    static byte[] Decompress(byte[] gzip)
    {
        using (GZipStream stream = new GZipStream(new MemoryStream(gzip), CompressionMode.Decompress))
        {
            const int size = 4096;
            byte[] buffer = new byte[size];
            using (MemoryStream memory = new MemoryStream())
            {
                int count = 0;
                do
                {
                    count = stream.Read(buffer, 0, size);
                    if (count > 0)
                    {
                        memory.Write(buffer, 0, count);
                    }
                }
                while (count > 0);
                return memory.ToArray();
            }
        }
    }

对于小于50MB的任何文件,它都可以正常运行,但是一旦输入的文件超过50MB,我就会遇到系统内存异常。异常发生前的最后一个位置和内存长度为134217728。我不认为这与我的物理内存有关,我知道由于使用32位,我不能拥有超过2GB的对象。

我还需要在解压缩文件后处理数据。我不确定MemoryStream是否是最好的方法,但我不喜欢先将文件写入磁盘再重新读取文件。

我的问题:

  • 为什么会出现System.OutMemoryException?
  • 最佳的解压gzip文件并进行文本处理的方法是什么?

你正在将流的整个内容加载到内存中,并将其作为字节数组返回。除了内存不足异常之外,你还能期望什么呢?你不应该像这样全部加载它到内存中--你最终打算用数组做什么?将其写入文件?无论你打算做什么,都应该基于流而不是基于数组。 - Kirk Woll
嗯...异常发生在memory.write上,卡在了134217728。我对内存管理不熟悉,请多包涵。稍后我会将所有处理过的文件保存到数据库中,gzipped文件中的文件是csv文件。 - William Calvin
3
可以,但如果你在解压缩的同时处理它,那么你的设计会更好。这样做就不需要分配大块内存来处理它了(例如,将gzip流直接放入“StreamReader”中)。 - Kirk Woll
3
也许错误最容易在你的函数原型中被发现:“static byte[] Decompress(byte[] gzip)”。你想要把一个“流”作为参数,而不是一个数组。 - sarnold
谢谢建议。我会尝试使用流。 - William Calvin
有没有带完整源代码示例的最终解决方案? - Kiquenet
4个回答

4

MemoryStream的内存分配策略对于大数据量并不友好。

由于MemoryStream的约定是拥有连续的数组作为基础存储,因此对于大流来说它必须经常重新分配数组(通常为log2(size_of_stream)次)。这样重新分配的副作用包括:

  • 在重新分配时会出现长时间的复制延迟
  • 新的数组必须适应之前已经被先前分配的空闲地址空间严重碎片化的情况
  • 新的数组将位于LOH堆上,而这个堆还有其自身的怪癖(没有压缩、GC2下进行收集等)。

因此,通过MemoryStream处理大型(100Mb+)流很可能在x86系统上导致内存不足异常。此外,最常见的返回数据的模式是调用GetArray,这需要与MemoryStream上一次使用的缓冲区大小相同的空间。

解决方法:

  • 最便宜的方法是将MemoryStream预先增长到大概所需的大小(最好略微大一些)。可以通过读取不存储任何内容的虚拟流来预先计算所需的大小(这会浪费CPU资源,但你将能够读取它)。另外考虑返回流而不是字节数组(或者返回MemoryStream缓冲区的字节数组和长度)。
  • 如果您需要整个流或字节数组,则处理它的另一种选择是使用临时文件流而不是MemoryStream来存储大量数据。
  • 更复杂的方法是实现一个将基础数据分块为较小(例如64K)块的流,以避免在LOH上分配和复制数据,当流需要增长时。

是的,谢谢你向我澄清这个问题。我现在有点明白了,在这种情况下,内存流对我来说并不是一个好朋友。我以为它可以帮助提高性能,但实际上它给我带来了更多的头痛。谢谢。 - William Calvin

1
您可以尝试以下类似的测试,以了解在出现 OutOfMemoryException 之前可以向 MemoryStream 中写入多少内容:
        const int bufferSize = 4096;
        byte[] buffer = new byte[bufferSize];

        int fileSize = 1000 * 1024 * 1024;

        int total = 0;

        try
        {
            using (MemoryStream memory = new MemoryStream())
            {
                while (total < fileSize)
                {
                    memory.Write(buffer, 0, bufferSize);
                    total += bufferSize;
                }

            }

            MessageBox.Show("No errors"); 

        }
        catch (OutOfMemoryException)
        {
            MessageBox.Show("OutOfMemory around size : " + (total / (1024m * 1024.0m)) + "MB" ); 
        }

您可能需要先解压到一个临时物理文件中,然后分块重新读取并在处理时逐步进行。

另外一点:有趣的是,在Windows XP PC上,当代码针对.net 2.0时,以上代码会出现“OutOfMemory around size 256MB”的错误,而在.net 4上则会出现“OutOfMemory around size 512MB”的错误。


1
我已经在上面指定了。它大约在128MB左右的134217728处卡住了。我不确定为什么会这么早发生,但我猜测选择内存流可能是我的第一个错误。谢谢你的回答。 - William Calvin
可以确认我也碰到了完全相同的限制。 - Kris

1

你是否在使用多线程处理文件?这会占用大量的地址空间。OutOfMemory 错误通常与物理内存无关,因此 MemoryStream 可以比预期更早地耗尽。请参考此讨论http://social.msdn.microsoft.com/Forums/en-AU/csharpgeneral/thread/1af59645-cdef-46a9-9eb1-616661babf90。如果您切换到 64 位进程,您处理的文件大小可能会更好。

在当前情况下,您可以使用内存映射文件来避免任何地址大小限制。如果您正在使用 .NET 4.0,则提供了一个 Windows 函数的本地包装器 http://msdn.microsoft.com/en-us/library/dd267535.aspx


是的,我在提问之前看到了那个链接。我只是想知道我还有哪些其他选项。谢谢你的回答。 - William Calvin

-1
我知道由于使用32位系统,我不能拥有超过2GB的对象。
那是不正确的。您可以拥有任意数量的内存。32位限制意味着您只能拥有4GB(操作系统占用其中一半)的虚拟地址空间。虚拟地址空间并不是内存。这里是一个不错的阅读材料。
为什么我会得到System.OutMemoryException?
因为分配器找不到连续的地址空间来存储您的对象,或者它发生得太快而导致堵塞。(最可能是前者)
如何最好地解压gzip文件并进行文本处理?
编写一个脚本下载文件,然后使用gzip或7zip等工具解压缩文件,然后进行处理。根据处理类型、文件数量和总大小,您将不得不在某个时候保存它们以避免出现内存问题。解压缩后保存它们,并每次处理1MB。

6
关于CLR 4.0仍然限制单个对象大小在2GB的问题,提问者是正确的。另外,我认为建议使用7-zip等外部工具完全忽略了这个问题的本质。 - Kirk Woll

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接