6

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

所以我试图谷歌并找到一些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 的对象。

我还需要在解压缩文件后处理数据。我不确定内存流是否是这里最好的方法,但我真的不喜欢写入文件然后再次读取文件。

我的问题

  • 为什么我得到 System.OutMemoryException?
  • 解压缩 gzip 文件并在之后进行一些文本处理的最佳解决方案是什么?
4

4 回答 4

4

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

由于 MemoryStream 的合同是将连续数组作为底层存储,因此它必须经常为大流重新分配数组(通常为 log2(size_of_stream))。这种重新分配的副作用是

  • 重新分配的长时间复制延迟
  • 新数组必须适合已被先前分配严重碎片化的空闲地址空间
  • 新数组将位于有其怪癖的 LOH 堆上(没有压缩,GC2 上的收集)。

因此,通过 MemoryStream 处理大型 (100Mb+) 流可能会在 x86 系统上出现内存不足异常。此外,返回数据的最常见模式是像您一样调用 GetArray,这还需要与用于 MemoryStream 的最后一个数组缓冲区大致相同的空间量。

解决方法:

  • 最便宜的方法是将 MemoryStream 预先增长到您需要的近似大小(最好稍微大一点)。您可以通过读取不存储任何内容的假流来预先计算所需的大小(浪费 CPU 资源,但您将能够读取它)。还考虑返回流而不是字节数组(或返回 MemoryStream 缓冲区的字节数组以及长度)。
  • 如果您需要整个流或字节数组,另一种处理方法是使用临时文件流而不是 MemoryStream 来存储大量数据。
  • 更复杂的方法是实现流,将底层数据分成更小的(即 64K)块,以避免在 LOH 上分配和在流需要增长时复制数据。
于 2012-05-03T02:03:52.690 回答
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 大小约为 256MB”,而在 .net 4 上,“OutOfMemory 大小约为 512MB”。

于 2012-05-03T02:03:30.580 回答
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提供本机包装器。

于 2012-05-03T02:06:41.587 回答
-1

我知道我的对象不能超过 2GB,因为我使用的是 32 位

这是不正确的。您可以根据需要拥有尽可能多的内存。32 位限制意味着您只能拥有 4GB(操作系统占用一半)的虚拟地址空间。虚拟地址空间不是内存。是一个很好的阅读。

为什么我得到 System.OutMemoryException?

因为分配器无法为您的对象找到连续的地址空间,或者它发生得太快并且阻塞了。(很可能是第一个)

解压缩 gzip 文件并在之后进行一些文本处理的最佳解决方案是什么?

编写一个下载文件的脚本,然后使用 gzip 或 7zip 等工具对其进行解压缩,然后进行处理。根据处理类型、文件数量和总大小,您必须在某些时候保存它们以避免此类内存问题。解压后保存,一次处理1MB。

于 2012-05-03T01:16:49.290 回答