在.NET中,Stack
,Queue
和List
(以及它们的通用实现)在内部使用数组来保存项目。
添加新元素时,如果内容数组已满,则内部数组的大小会加倍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);
答案 0 :(得分:4)
最有可能效率。
缩小和增长数组涉及在内存中复制数组;你不能简单地将少量的新内存标记到现有的分配上,然后从分配中删除它:分配在操作系统中是固定大小的,所以如果你想增加数组的大小,你实际上有分配新的内存块,将所有内容从源复制到目标,然后删除旧的分配。这也是收缩的类似操作。这可能非常耗时。
现在假设一个数组已经增长到一个给定的大小,它对于.NET(或任何其他框架)来说是一个很好的指标,用于表示该数组的最佳大小。即使从中移除了项目,阵列也有可能再次增长到相同的大小:.NET不是Mystic Meg,所以不会知道你再也不会使用该阵列的块了。因此,不是不断地分配和释放内存块,而是增加分配直到它不需要再次增长,然后只是担心在变量超出范围并成为目标时清理它更有效。 GC
答案 1 :(得分:0)
因为经常是,所以缩小列表时,它只是暂时的,并且您打算稍后再允许它再次增长。因此,删除内存会产生双重成本:首先,您必须释放当前阵列并分配一个新的较小的阵列(并将内容复制到新阵列)。然后,您将不得不再次发布 以分配更大的一个(然后再次复制所有元素)。
同时,如果您知道要永久缩小列表,并且节省的内存量是值得的,那么您有一个方法可以做到这一点:只需分配一个新的列出并复制相关条目。所以当你做想要缩小列表时,你就有办法做到这一点。但与此同时,默认行为试图避免不必要的数据副本。通常没有理由将数据复制到较小的阵列。
答案 2 :(得分:0)
我可以看到两个原因。
List
或Stack
等Capacity
属性增长时,请不要忘记用户可以明确设置容量属性。如果用户明确设置了怎么办?框架不应该否定它不是吗?需要维护更多状态才能实现此功能,以确定用户是设置容量还是List
本身已调整。它会增加不必要的复杂性。Size * 2
,以及它的实现方式。如果你继续在重新分配的热点添加和删除怎么办?分配内存通常会损害性能并增加垃圾,从而将负载放入GC。答案 3 :(得分:0)
在我看来,这种行为反映了这些类的预期用法。特别是Queue
和Stack
旨在作为短期机制来促进在单个线程中进行的计算。它们不用作线程之间的缓冲区。相反,ConcurrentQueue
(source 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();
}