5

我有一个应用程序,我试图在其中创建一个非常大的字节“立方体”。一个 3 维数组 (~1000x1000x500) 保存了我感兴趣的所有值 - 但我遇到了内存不足的问题。虽然这是意料之中的,但我收到的各种 OOM 消息的行为却非常令人困惑。第一的:

Foo[,,] foo1 = new Foo[1000, 1000, 500];

失败并出现 OOM 错误,但这不会:
Foo[,,] foo1 = new Foo[250, 1000, 500];
Foo[,,] foo2 = new Foo[250, 1000, 500];
Foo[,,] foo3 = new Foo[250, 1000, 500];
Foo[,,] foo4 = new Foo[250, 1000, 500];

这两组代码不应该消耗本质上相同的内存量吗?

此外,我最初在消耗了大约 1.5GB 时收到错误消息,但我认为通过将其切换到 64 位应用程序可以让我在失败前使用更多内存。

我是否遇到堆栈空间限制?如果是这样,我怎样才能完全在堆上实例化这个结构而不必在堆栈上存在(作为单个实体)?

在此先感谢-我期待任何人都可以对这种行为有所了解。

4

4 回答 4

4

问题是,当你分配一个数组时,你需要连续的内存。

在 RAM 中找到 10 个大小为 10MB 的连续内存块比找到 1 个 100MB 的巨大块更有可能。

假设您有 100 个字节的 RAM,地址为 0 到 99。例如,如果您在位置 23 分配了一个大小为 1 字节的内存块,尽管您还有 99 个字节的 RAM,但如果您想分配一个内存块大小为 99 字节,您将失败,因为内存必须是连续的。在这种情况下,您可以分配的最大块将是 76 字节长。

于 2013-05-09T15:59:23.713 回答
4

32 位应用程序被限制为 4 GB 的地址空间,所以这是上限。如果进程在 32 位操作系统上运行,则进一步限制为 2 或 3 GB,具体取决于应用程序和操作系统的设置。

在第一种情况下,您要分配一个大数组。在 .NET 中,数组是在堆上分配的,因此这里的堆栈空间不是问题。鉴于问题中的数字,我假设该阵列大约为 1.5 GB。为了处理这个问题,CLR 需要一个连续的内存块。如果您在较小的块中分配相同数量的字节(如第二个示例中所示),则运行时将更有可能根据需要分配内存。

尽管如此,至少有 2 GB 可用,您会认为 1.5 GB 应该不是问题,但事实是进程不会严格从一端到另一端使用地址空间。一旦 DLL 被加载到进程中,地址空间就会被分割。

以我的经验,32 位托管应用程序(在 32 位操作系统上)通常限制为大约 1.5 GB 的堆空间,这可以解释您所看到的 OOM。

在 64 位操作系统上运行相同的应用程序将使应用程序访问整个 4 GB 地址空间,这将影响托管堆的可用空间。

将应用程序转换为 64 位应用程序,将地址空间大小从 4 GB 更改为 8 TB。但是,即使在这种情况下,您也应该记住任何 .NET 对象的默认最大大小为 2 GB。有关详细信息,请参阅此问题

于 2013-05-09T16:40:09.153 回答
3

编辑

我正在考虑我的答案的功能更全面的实现,我想我会追加。我不确定并行化是否会有所帮助,也许它取决于initializer.

using System;
using System.Linq;

public static T[][][] NewJagged<T>(
        int h,
        int w,
        ing d,
        Func<int, int, int, T> initializer = null,
        bool parallelize = true)
{
    if (h < 1)
    {
        throw new ArgumentOutOfRangeException("h", h, "Dimension less than 1.")
    }

    if (w < 1)
    {
        throw new ArgumentOutOfRangeException("w", w, "Dimension less than 1.")
    }

    if (d < 1)
    {
        throw new ArgumentOutOfRangeException("d", d, "Dimension less than 1.")
    }

    if (initializer == null)
    {
        initializer = (i, j, k) => default(T);
    }

    if (parallelize)
    {
        return NewJaggedParalellImpl(h, w, d, initializer);
    } 

    return NewJaggedImpl(h, w, d, initializer);
}

private static T[][][] NewJaggedImpl<T>(
        int h,
        int w,
        int d,
        Func<int, int, int, T> initializer)
{
    var result = new T[h][][];
    for (var i = 0; i < h; i++)
    {
        result[i] = new T[w][];
        for (var j = 0; j < w; j++)
        {
            result[i][j] = new T[d];
            for (var k = 0; k < d; k++)
            {
                result[i][j][k] = initializer(i, j, k);
            }
        }
    }

    return result;
}

private static T[][][] NewJaggedParalellImpl<T>(
        int h,
        int w,
        int d,
        Func<int, int, int, T> initializer)
{
    var result = new T[h][][];
    ParallelEnumerable.Range(0, h).ForAll(i =>
    {
        result[i] = new T[w][];
        ParallelEnumerable.Range(0, w).ForAll(j =>
        {
            result[i][j] = new T[d];
            ParallelEnumerable.Range(0, d).ForAll(k =>
            {
                result[i][j][k] = initializer(i, j, k);
            });
        });
    });

    return result;
}            

这使得函数完全通用,但仍然为您提供简单的语法,

var foo1 = NewJagged<Foo>(1000, 1000, 500);

但是,您可以在初始化时获得幻想并并行填充,

var foo2 = NewJagged<Foo>(
    1000,
    1000,
    5000,
    (i, j, k) =>
        {
            var pos = (i * 1000 * 500) + (j * 500) + k;
            return ((pos % 2) == 0) ? new Foo() : null;
        });

在这种情况下,填充棋盘效果(我认为。);


这最初似乎无法回答您的问题...

如果你有一个功能,像这样

public static T[][][] ThreeDimmer<T>(int h, int w, int d) where T : new()
{
    var result = new T[h][][];
    for (var i = 0; i < h; i++)
    {
        result[i] = new T[w][];
        for (var j = 0; j < w; j++)
        {
            result[i][j] = new T[d];
            for (var k = 0; k < d; k++)
            {
                result[i][j][k] = new T();
            }
        }
    }

    return result;
}

然后,您将封装引用类型的 3 维锯齿状数组的初始化。这将允许你这样做,

Foo[][][] foo1 = ThreeDimmer<Foo>(1000, 1000, 500);

这将避免多维数组的内存碎片问题。它还将避免其他陷阱和限制,从而为您提供更快更灵活的锯齿状数组。

于 2013-05-09T16:38:04.763 回答
3

对我来说,这看起来像是一个内存碎片问题。另外,请注意new使用堆。

在第一个示例中,您需要一个非常大的内存块,并且无论您的系统中有多少 RAM,操作系统都可能无法找到这么大的连续内存块。

较小的分配有效,因为较小的连续内存块总是比较大的更丰富。

于 2013-05-09T15:59:10.663 回答