记忆混乱
本文关键字:混乱 记忆 | 更新日期: 2023-09-27 18:12:47
我有一个应用程序,我试图创建一个非常大的"立方体"的字节。一个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位应用程序可以让我在失败之前使用更多的内存。
我是否遇到堆栈空间限制?如果是这样,我怎样才能完全在堆上实例化这个结构,而不必在堆栈上存在(作为单个实体)?
提前感谢-我期待任何人可以对这种行为提出任何启示。
问题是,当你分配一个数组时,你需要连续的内存。
在RAM中找到10个大小为10MB的连续内存块的可能性比找到1个大小为100MB的大块的可能性更大。
假设您有100字节的RAM,地址为0到99。例如,如果您在位置23分配了一个大小为1字节的内存块,尽管您还有99字节的RAM,但如果您想分配一个大小为99字节的内存块,您将失败,因为内存必须是连续的。在这种情况下,您可以分配的最大块长度为76字节。
一个32位的应用程序被限制为4gb的地址空间,所以这是上限。如果进程在32位操作系统上运行,则根据应用程序和操作系统的设置,进一步限制为2或3 GB。
在第一种情况下,你分配一个大数组。在。net中,数组是在堆上分配的,所以这里的堆栈空间不是问题。考虑到问题中的数字,我假设阵列约为1.5 GB。为了处理这个问题,CLR需要一个连续的内存块。如果您在较小的块中分配相同数量的字节(如第二个示例),则运行时将有更好的机会根据需要分配内存。
尽管如此,如果至少有2gb可用,您可能会认为1.5 GB应该不是问题,但事实是进程并不严格地从一端到另一端使用地址空间。一旦dll被加载到进程中,地址空间就会被分割。
根据我的经验,32位管理的应用程序(在32位操作系统上)通常限制在1.5 GB左右的堆空间,这可以解释你所看到的OOM。
在64位操作系统上运行相同的应用程序将使应用程序访问整个4gb地址空间,这将影响可用于托管堆的空间。
将应用程序转换为64位应用程序,将地址空间大小从4gb更改为8tb。然而,即使在这种情况下,您也应该记住,任何. net对象的默认最大大小都是2gb。有关详细信息,请参阅此问题。
EDIT
我在思考一个功能更全面的实现我的答案,我想我应该补充一下。我不确定并行化是否有帮助,也许这取决于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;
}
则封装了引用类型的三维锯齿数组的初始化。这将允许你做,
Foo[][][] foo1 = ThreeDimmer<Foo>(1000, 1000, 500);
这将避免多维数组的内存碎片问题。它还可以避免其他陷阱和限制,为您提供更快更灵活的锯齿数组。
对我来说,这看起来像是内存碎片问题。另外,请注意new
使用堆。
在第一个例子中,你要求一个非常大的内存块,这是有可能的,不管你的系统有多少RAM,操作系统可能无法找到一个连续的内存块那么大。
较小的分配是有效的,因为较小的连续内存块总是比较大的内存块更充裕。