单个 .net 进程的内存限制大约为 2.5 GB。

12

我正在编写在 Windows Server 2016 上运行的 .NET 应用程序,该应用程序对大文件的许多块进行 http 获取。这样可以大大加快下载过程,因为您可以并行下载它们。不幸的是,一旦它们被下载,将它们全部拼接在一起需要相当长的时间。

需要组合的文件数量在 2 到 4k 之间。这个服务器具有充足的内存,接近800GB。我认为使用MemoryStream来存储已下载的分片,直到它们可以按顺序写入磁盘,是有意义的在我使用约2.5GB的内存后,就会出现System.OutOfMemoryException错误。该服务器有数百 GB 的可用空间,但我无法找出如何使用它们。


1
ConcatenatedStream 可以满足你的需求,只要你不需要随机寻址。它来自于 How do I concatenate two System.Io.Stream instances into one? - dbc
1
请确保您已编译为x64(或任何CPU,但不带“prefer 32位标志”,并在x64上运行)。 - Flydog57
1
它是x64,我使用dumpbin验证了是否支持大地址。 - Josh Dayberry
1
这里有一个简单的程序来重现我的问题。 `static void Main(string[] args) { MemoryStream ms1 = new MemoryStream((int)Math.Pow(1024, 3)); MemoryStream ms2 = new MemoryStream((int)Math.Pow(1024, 3)); MemoryStream ms3 = new MemoryStream((int)(Math.Pow(1024, 3)*.95)); MemoryStream ms4 = new MemoryStream((int)Math.Pow(1024, 3)); //这个会出现内存不足的错误}` - Josh Dayberry
1
第三个内存流很有趣。我将允许的总字节数乘以0.95,它可以工作,如果我增加到0.96或更高,则无法工作。如果第三个内存流不存在,则第四个内存流会导致内存不足错误。 - Josh Dayberry
显示剩余4条评论
4个回答

13

MemoryStreams是基于字节数组构建的。 目前,数组大小不能超过2GB。

System.Array的当前实现对其所有内部计数器等均使用Int32,因此理论上元素的最大数量为Int32.MaxValue

Microsoft CLR还强制规定了每个对象的最大大小为2GB

当你试图将内容放入单个MemoryStream时,底层数组变得太大,因此会出现异常。

尝试分别存储这些部分,并在准备好时直接将它们写入FileStream(或您使用的任何其他方式),而不是先尝试将它们全部连接成一个对象。


2
有一个应用程序设置,允许创建超过2GB大小的数组。请参见https://learn.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/runtime/gcallowverylargeobjects-element - ckuri
1
我试过了,你是对的。无法使用长度大于2^31来初始化数组。因此,new long[2_000_000_000]成功创建了一个16 GB的数组,但new byte[3_000_000_000]抛出了OverflowException异常。 - ckuri
我正在存储MemoryStream,因为我有大量可用的内存,我无法像下载文件那样快速写入磁盘。我正在运行两个异步进程,一个并行下载块,另一个串行将它们拼接在一起。以前,我只是将块下载为文件,并使用串行后台进程将它们拼接成一个大文件。当我这样做时,下载完成的时间是拼接完成时间的三分之一,这就是为什么我想将一半IO移动到MemoryStreams的原因。 - Josh Dayberry
@MarcellTóth 这里有一个超级简单的例子,似乎重新创建了我的问题。static void Main(string[] args) { MemoryStream ms1 = new MemoryStream((int)Math.Pow(1024, 3)); MemoryStream ms2 = new MemoryStream((int)Math.Pow(1024, 3)); MemoryStream ms3 = new MemoryStream((int)(Math.Pow(1024, 3)*.95)); MemoryStream ms4 = new MemoryStream((int)Math.Pow(1024, 3)); //这个会出现内存不足的错误 } - Josh Dayberry
2
@JoshDayberry 你99%是以x86运行它。我猜测你的应用程序设置为AnyCPU(prefer 32 bit)。这将使您的代码作为32位汇编运行,参见此处:https://dev59.com/f2ct5IYBdhLWcg3wedSP#12066861 将其明确设置为x64(如果在32位系统上直接崩溃,则没有任何CPU的意义),它将正常工作。我重现了你的问题,这样解决了它。 - Marcell Toth
显示剩余3条评论

3
根据MemoryStream类的源代码,您将无法将超过2GB的数据存储到该类的一个实例中。 原因是流的最大长度被设置为Int32.MaxValue,而数组的最大索引被设置为0x0x7FFFFFC7,即2.147.783.591十进制(= 2 GB)。
private const int MemStreamMaxLength = Int32.MaxValue;

片段数组

// We impose limits on maximum array lenght in each dimension to allow efficient 
// implementation of advanced range check elimination in future.
// Keep in sync with vm\gcscan.cpp and HashHelpers.MaxPrimeArrayLength.
// The constants are defined in this method: inline SIZE_T MaxArrayLength(SIZE_T componentSize) from gcscan
// We have different max sizes for arrays with elements of size 1 for backwards compatibility
internal const int MaxArrayLength = 0X7FEFFFFF;
internal const int MaxByteArrayLength = 0x7FFFFFC7;

这篇文章“管理内存超过2GB”早已在微软论坛上讨论,其中提到了一个关于BigArray的博客文章,突破了2GB数组大小限制

更新

我建议使用以下代码,在x64构建中应该能够分配超过4GB,但在x86构建中<4GB将失败。

private static void Main(string[] args)
{
    List<byte[]> data = new List<byte[]>();
    Random random = new Random();

    while (true)
    {
        try
        {
            var tmpArray = new byte[1024 * 1024];
            random.NextBytes(tmpArray);
            data.Add(tmpArray);
            Console.WriteLine($"{data.Count} MB allocated");
        }
        catch
        {
            Console.WriteLine("Further allocation failed.");
        }
    }
}

我没有在一个流中存储超过2GB的数据。但是,我使用了多个流,每个流包含一个块的数据。无论我将块的大小设置为多少,它总是在使用2.5GB内存后出现内存溢出错误。我将许多MemoryStream存储在队列中。 - Josh Dayberry
@JoshDayberry,我明白了...你的页面文件配置是怎样的呢?如果页面文件的大小配置错误,可能会导致内存分配出现问题。我刚刚搜索到了一篇文章并找到了它:Pushing the limits of windows physical memoryPushing the limits of windows virtual memory。最后一篇或许能对你有所帮助... - Markus Safar
服务器内存大约有800GB,但我们卡在了3左右。页面文件利用率稳定在0%。为什么页面文件会相关呢? - Josh Dayberry

1
正如已经指出的那样,这里的主要问题是 MemoryStream 的本质是由一个固定上限大小的 byte[] 所支持。
使用替代的 Stream 实现的选项已经被注意到。另一个选择是查看新的 IO API 中的 "pipelines"。"Pipeline" 基于不连续的内存,这意味着它不需要使用单个连续缓冲区;pipelines 库将根据需要分配多个物理块,您的代码可以处理这些块。我已经详细地写过这个话题;第一部分 在这里。第三部分可能具有最多的代码关注点。

0

确认我理解你的问题:你正在使用多个并行块下载单个非常大的文件,并且你知道最终文件的大小?如果不知道,那么这会变得更加复杂,但仍然可以完成。

最好的选择可能是使用MemoryMappedFile(MMF)。你需要通过MMF创建目标文件。每个线程将为该文件创建一个视图访问器,并以并行方式写入它。最后,关闭MMF。这基本上给了你想要的MemoryStreams的行为,但Windows通过磁盘支持文件。这种方法的好处之一是Windows在后台管理将数据存储到磁盘中(刷新),因此你不必担心,应该会获得出色的性能。


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