3

I have huge transient arrays created rapidly. Some are kept, some are GC-d. This defragments the heap and the app consumes approx. 2.5x more memory than it would truly need resulting OutOfMemoryException.

As a solution, I would prefer to have one gigantic array (PointF[]) and do the allocation and management of segments by myself. But I wonder how I could make two (or more) arrays share the same memory space.

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

I am thinking of a trick like the winner answer of this SO question. Would that be possible? The problem is that the length and the number of the segment arrays are known only in runtime.

4

2 回答 2

4

假设您确定您的 OutOfMemoryException 可以避免,并且您将其全部存储在内存中的方法不是实际问题(如果内存可用,GC 非常擅长阻止这种情况发生)......

  • 这是你的第一个问题。我不确定 CLR 是否支持任何大于 2 GB 的单个对象
    • Crucial Edit -gcAllowVeryLargeObjects在 64 位系统上更改此设置- 在推出您自己的解决方案之前尝试此操作。
  • 其次,您在谈论“有些被保留,有些被GC'd”。即,一旦完成“子数组”,您希望能够重新分配数组的元素。
  • 第三,我假设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
  }
}

警告 上面的代码有效地引入了childIndex缓冲区溢出漏洞,例如,如果您没有lengthSetChildValue. 在尝试在生产中执行此操作之前,您必须了解并防止它unsafe发生,尤其是在将这些方法与使用.

现在,这可以扩展为返回子数组的伪索引public PointF this[int index]方法、子数组的枚举器等,但正如我所说,这变得越来越复杂,您需要确定它是否真的能解决您的问题。您的大部分时间将花在重用(第一)碎片整理(第二)扩展(第三)throw OutOfMemory(最后)逻辑上。

如果我对 2GB 对象限制的评论是正确的,这种方法还有一个优点,即您可以分配许多 2GB 子数组并将它们用作单个数组。

这假设您不想沿着unsafe路线走并使用指针,但效果是一样的,您只需创建一个包装类来管理固定内存块中的子数组。

另一种方法是使用哈希集/字典方法。分配整个(2GB 大数组)并将其分成块(比如 100 个数组元素)。然后,子数组将分配多个块,并在其最终块中浪费一些空间。这将产生一些整体空间浪费的影响(取决于您的平均“子长度块长度”预测),但是您可以增加和减少子数组的大小以及删除和插入影响较小的子数组的优势关于你的碎片化。


值得注意的参考资料:

将数组作为不同类型的数组或结构访问的其他示例。这些的实现可能会帮助您开发自己的解决方案

数组优化

并行数组和使用unsafe

于 2013-06-14T09:11:21.983 回答
1

您最好的选择可能是ArraySegment<PointF>在同一PointF[]实例上使用多个 all,但偏移量不同,并让您的调用代码记下相对的.Offsetand .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);

作为旁注:另一种方法可能是使用指向数据的指针 - 来自非托管分配,或者来自已固定的托管数组(注意:您应该尽量避免固定),但是:虽然 aPointF*可以传达其自己的偏移量信息,它不能传达长度 - 所以你需要总是通过 aPointF*Length. 当你这样做的时候,你还不如刚刚使用过ArraySegment<T>,它的附带好处是不需要任何unsafe代码。当然,根据具体情况,将巨大的数组视为非托管内存可能(在某些情况下)仍然很诱人。

于 2013-06-14T08:58:11.027 回答