删除项目后,为什么堆栈,队列和列表不收缩内部数组?

时间:2014-04-13 19:39:26

标签: c# .net arrays data-structures collections

在.NET中,StackQueueList(以及它们的通用实现)在内部使用数组来保存项目。

添加新元素时,如果内容数组已满,则内部数组的大小会加倍Push()Enqueue()Add()

问题是,当项目被删除时,为什么这些数据结构减半内部数组?如果数组小于四分之一满,可以将Pop()Dequeue()Remove()上的数组大小减半,以防止浪费内存。

我使用以下代码检查此行为:

var s = new Stack<int>();
for (int i = 0; i <= 512; i++) s.Push(i); // Increases the array capacity to 1024.
for (int i = 0; i <= 512; i++) s.Pop();   // Doesn't decrease array capacity.

// Stack is empty, but internal array capacity is still 1024:
var fieldInfo = typeof(Stack<int>).GetField("_array", BindingFlags.NonPublic | BindingFlags.Instance);
Console.WriteLine((fieldInfo.GetValue(s) as int[]).Length);

4 个答案:

答案 0 :(得分:4)

最有可能效率。

缩小和增长数组涉及在内存中复制数组;你不能简单地将少量的新内存标记到现有的分配上,然后从分配中删除它:分配在操作系统中是固定大小的,所以如果你想增加数组的大小,你实际上有分配新的内存块,将所有内容从源复制到目标,然后删除旧的分配。这也是收缩的类似操作。这可能非常耗时。

现在假设一个数组已经增长到一个给定的大小,它对于.NET(或任何其他框架)来说是一个很好的指标,用于表示该数组的最佳大小。即使从中移除了项目,阵列也有可能再次增长到相同的大小:.NET不是Mystic Meg,所以不会知道你再也不会使用该阵列的块了。因此,不是不断地分配和释放内存块,而是增加分配直到它不需要再次增长,然后只是担心在变量超出范围并成为目标时清理它更有效。 GC

答案 1 :(得分:0)

因为经常是,所以缩小列表时,它只是暂时的,并且您打算稍后再允许它再次增长。因此,删除内存会产生双重成本:首先,您必须释放当前阵列并分配一个新的较小的阵列(并将内容复制到新阵列)。然后,您将不得不再次发布 以分配更大的一个(然后再次复制所有元素)。

同时,如果您知道要永久缩小列表,并且节省的内存量是值得的,那么您有一个方法可以做到这一点:只需分配一个新的列出并复制相关条目。所以当你想要缩小列表时,你就有办法做到这一点。但与此同时,默认行为试图避免不必要的数据副本。通常没有理由将数据复制到较小的阵列。

答案 2 :(得分:0)

我可以看到两个原因。

  1. 当项目添加到ListStackCapacity属性增长时,请不要忘记用户可以明确设置容量属性。如果用户明确设置了怎么办?框架不应该否定它不是吗?需要维护更多状态才能实现此功能,以确定用户是设置容量还是List本身已调整。它会增加不必要的复杂性。
  2. 效率。假设你在那个阶段删除一个元素运行时决定减少数组大小,如果你立即添加元素怎么办?大小将增加Size * 2,以及它的实现方式。如果你继续在重新分配的热点添加和删除怎么办?分配内存通常会损害性能并增加垃圾,从而将负载放入GC。

答案 3 :(得分:0)

在我看来,这种行为反映了这些类的预期用法。特别是QueueStack旨在作为短期机制来促进在单个线程中进行的计算。它们不用作线程之间的缓冲区。相反,ConcurrentQueuesource code)作为小型数组的链接列表更有效地实现,但旨在用作多线程生产者-消费者方案的缓冲区。 ConcurrentQueue可能会生存很长时间,并且在大多数情况下可能几乎是空的。不保留传入数据突发期间获得的最大大小是合理的。


更新:这是Queue的基本实现,它会自动增长和收缩。在内部,它是一个LinkedList小队列,与ConcurrentQueue类非常相似。当然,这不是线程安全的。

public class SegmentedQueue<T> : IEnumerable<T>
{
    private readonly LinkedList<Queue<T>> _list = new LinkedList<Queue<T>>();
    private const int SEGMENT_SIZE = 32;

    public int Count => _list.Sum(q => q.Count);

    public bool IsEmpty => _list.First == null || _list.First.Value.Count == 0;

    public void Enqueue(T item)
    {
        if (_list.Last == null || _list.Last.Value.Count == SEGMENT_SIZE)
        {
            _list.AddLast(new Queue<T>(SEGMENT_SIZE));
        }
        _list.Last.Value.Enqueue(item);
    }

    public T Dequeue()
    {
        if (this.IsEmpty)
            throw new InvalidOperationException("The Queue is empty");
        var item = _list.First.Value.Dequeue();
        if (_list.First.Value.Count == 0)
        {
            _list.RemoveFirst();
        }
        return item;
    }

    public void Clear() => _list.Clear();

    public IEnumerator<T> GetEnumerator() => _list.SelectMany(q => q)
        .GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}