我快速创建了巨大的瞬态数组。有些是保留的,有些是GC-d。这将对堆进行碎片整理,应用程序将消耗大约。比实际需要的内存多2.5倍,导致OutOfMemoryException。
作为一种解决方案,我宁愿有一个巨大的数组(PointF[]),并自己做段的分配和管理。但是我想知道如何使两个(或更多)数组共享相同的内存空间。
PointF[] giganticList = new PointF[100];
PointF[] segment = ???;
// I want the segment length to be 20 and starting e.g at position 50
// within the gigantic list
我在想一个技巧,就像这个问题的赢家答案。这可能吗?问题是片段数组的长度和数量只有在运行时才知道。
假设您确定可以避免OutOfMemoryException,并且您将其全部放在内存中的方法并不是实际问题(如果内存可用,GC非常擅长阻止这种情况发生)…
- 这是第一个问题。我不确定CLR是否支持大于2 GB的单个对象。
- 关键编辑-
gcAllowVeryLargeObjects
在64位系统上更改此-在推出自己的解决方案之前先尝试一下。
- 关键编辑-
- 其次,你说的是"有些被保留,有些被GC"。也就是说,一旦你完成了"子数组",你希望能够重新分配数组的元素。 第三,我假设你问题中的
PointF[] giganticList = new PointF[100];
更像PointF[] giganticList = new PointF[1000000];
?还可以考虑使用MemoryFailPoint
,因为它允许您"要求"内存并检查异常,而不是使用OutOfMemoryException崩溃。
编辑也许最重要的是,你现在进入了一个权衡取舍的领域。如果你这样做,你可能会开始失去诸如在循环开始时通过做数组绑定检查来优化for
循环的抖动等优点(for (int i= 0; i < myArray.Length; i++)
得到优化,int length = 5; for (int i= 0; i < length; i++)
没有)。如果您有高计算资源代码,那么这可能会对您造成伤害。您还必须更加努力地并行处理不同的子数组。创建子数组的副本,或它们的部分,甚至其中的项,仍然会分配更多的内存,这些内存将被GC。
这可以通过包装数组,并跟踪哪个部分用于哪个子数组来实现。你实际上是在谈论分配一个巨大的内存块,然后重用其中的一部分,而不把责任放在GC上。您可以利用ArraySegment<T>
,但这也有它自己的潜在问题,比如将原始数组暴露给所有调用者。
这并不简单,但它是可能的。可能不是每次删除子数组时,您都希望通过移动其他子数组来关闭间隙来对主数组进行碎片整理(或者当您用光了连续的段时这样做)。
一个简单的例子看起来像下面的伪代码(未经测试,如果你的电脑离开家,你的猫爆炸了,不要怪我)。还有另外两种方法,我在最后提到了。
public class ArrayCollection {
List<int> startIndexes = new List<int>();
List<int> lengths = new List<int>();
const int 1beeellion = 100;
PointF[] giganticList = new PointF[1beeellion];
public ArraySegment<PointF> this[int childIndex] {
get {
// Care with this method, ArraySegment exposes the original array, which callers could then
// do bad things to
return new ArraySegment<String>(giganticList, startIndexes[childIndex], length[childIndex]);
}}
// returns the index of the child array
public int AddChild(int length) {
// TODO: needs to take account of lists with no entries yet
int startIndex = startIndexes.Last() + lengths.Last();
// TODO: check that startIndex + length is not more than giganticIndex
// If it is then
// find the smallest unused block which is larger than the length requested
// or defrag our unused array sections
// otherwise throw out of memory
startIndexes.Add(startIndex); // will need inserts for defrag operations
lengths.Add(length); // will need inserts for defrag operations
return startIndexes.Count - 1; // inserts will need to return inserted index
}
public ArraySegment<PointF> GetChildAsSegment(int childIndex) {
// Care with this method, ArraySegment exposes the original array, which callers could then
// do bad things to
return new ArraySegment<String>(giganticList, startIndexes[childIndex], length[childIndex]);
}
public void SetChildValue(int childIndex, int elementIndex, PointF value) {
// TODO: needs to take account of lists with no entries yet, or invalid childIndex
// TODO: check and PREVENT buffer overflow (see warning) here and in other methods
// e.g.
if (elementIndex >= lengths[childIndex]) throw new YouAreAnEvilCallerException();
int falseZeroIndex = startIndexes[childIndex];
giganticList[falseZeroIndex + elementIndex];
}
public PointF GetChildValue(int childIndex, int elementIndex) {
// TODO: needs to take account of lists with no entries yet, bad child index, element index
int falseZeroIndex = startIndexes[childIndex];
return giganticList[falseZeroIndex + elementIndex];
}
public void RemoveChildArray(int childIndex) {
startIndexes.RemoveAt(childIndex);
lengths.RemoveAt(childIndex);
// TODO: possibly record the unused segment in another pair of start, length lists
// to allow for defraging in AddChildArray
}
}
警告以上代码有效地引入了缓冲区溢出漏洞例如,如果您没有在SetChildValue
等方法中的子数组中检查所请求的childIndex
和length
。在尝试在生产环境中执行此操作之前,您必须了解这一点并防止它,特别是在将这些方法与unsafe
结合使用时。
现在,这可以扩展为返回子数组的psuedo indexpublic PointF this[int index]
方法,子数组的枚举器等,但正如我所说,这变得越来越复杂,你需要决定它是否真的能解决你的问题。大部分时间将花费在重用(第一个)碎片整理(第二个)扩展(第三个)throw OutOfMemory
(最后一个)逻辑上。
如果我关于2GB对象限制的评论是正确的,那么这种方法还有一个优点,即您可以分配许多2GB的子数组并将它们用作单个数组。
这里假设您不想走unsafe
路线并使用指针,但效果是一样的,您只需创建一个包装器类来管理固定内存块中的子数组。
另一种方法是使用哈希集/字典方法。分配整个数组(巨大的2GB数组)并将其分成块(例如100个数组元素)。然后,子数组将有多个块分配给它,并且在其最终块中浪费一些空间。这将产生一些总体空间浪费的影响(取决于您的平均"子长度vs。块长度"预测"),但优点是您可以增加和减少子数组的大小,以及在对碎片影响较小的情况下删除和插入子数组。
值得注意引用:
- 64位。net 4:
gcAllowVeryLargeObjects
MemoryFailPoint
-允许您"要求"内存并检查异常,而不是在事实后与OutOfMemoryException
崩溃- Large Arrays和LOH Fragmentation。什么是公认的惯例?
- 3GB进程限制在32位,参见:3_GB_barrier, Server Fault/3GB considerations和AWE/PAE
- 缓冲区溢出漏洞,以及为什么你可以在c#中得到这个
作为不同类型的数组或结构访问数组的其他示例。这些实现可以帮助您开发自己的解决方案
<<ul>阵列优化
- Eric Gu -数组的迭代效率?-注意它的年代,但是如何寻找JIT优化的方法仍然与。net 4.0相关(参见CLR中的数组边界检查消除?例如)
- Dave delefs -数组边界检查消除在CLR
- 警告- pdf: 64位架构的隐式数组边界检查
- LinkedList -允许你在一个序列中引用多个不同的数组桶(在块桶方法中将块绑在一起)
并行数组和unsafe
- 并行矩阵乘法使用任务并行库(TPL),特别是
UnsafeSingle
-由单个数组表示的方形或锯齿数组是您试图解决的同类问题。 - 缓冲区溢出漏洞,以及为什么你可以在c#中得到这个(是的,我已经提到过三次了,这很重要)
您最好的选择可能是在同一个PointF[]
实例上使用多个ArraySegment<PointF>
,但在不同的偏移量,并让您的调用代码注意相对的.Offset
和.Count
。请注意,您必须编写自己的代码来分配下一个块,并查找间隙等-本质上是您自己的迷你分配器。
不能直接将段视为PointF[]
。
:
PointF[] giganticList = new PointF[100];
// I want the segment length to be 20 and starting e.g at position 50
// within the gigantic list
var segment = new ArraySegment<PointF>(giganticList, 50, 20);
作为旁注:另一种方法可能是使用指针指向数据-无论是来自非托管分配,还是来自已固定的托管数组(注意:您应该尽量避免固定),但是:虽然PointF*
可以传递其自己的偏移信息,但它不能传递长度-因此您需要始终传递PointF*
和Length
。当您这样做的时候,您可能已经使用了ArraySegment<T>
,它的附带好处是不需要任何unsafe
代码。当然,根据场景的不同,将大数组视为非托管内存可能(在某些场景中)仍然是诱人的。