为什么将c#泛型List实现为仅附加数组?

时间:2013-08-30 22:27:12

标签: c# .net list data-structures arraylist

C#通用列表System.Collections.Generic.List<T>是使用可扩展数组作为后备存储实现的,其方式类似于更多基于数组列表的实现。很明显,当对例如随机(索引)访问进行随机(索引)访问时,这提供了巨大的益列表实现为链表。

我想知道为什么不选择将它作为循环数组来实现。这样的实现对于索引随机访问具有相同的O(1)性能并且附加到列表的末尾。但是会提供额外的好处,例如允许O(1)前置操作(即在列表的前面插入新元素),并平均减少随机插入和删除所需的时间。

到目前为止的一些答案摘要

正如@Hachet指出的那样,为了使循环数组实现具有与System.Collections.Generic.List<T>类似的索引性能,需要它始终增长到容量为2的幂。 (这样可以进行廉价的模数运算)。这意味着无法将其大小调整为精确的用户提供的初始容量,因为目前可能正在构建列表实例。所以这是一个明确的权衡问题。

正如一些快速&amp;脏性能测试,对于循环阵列实现,索引可能会慢约2-5倍。

由于索引是一个显而易见的优先级,我可以想象这对于支付来说会有太大的代价,而不是在前置操作上获得更好的性能,而在随机插入/删除时性能会略微提高。

这是一个重复的补充答案信息

这个问题确实与Why typical Array List implementations aren't double-ended?有关,我在发布问题之前没有发现。我没有以完全令人满意的方式回答:

  

我没有做任何基准测试,但在我看来,还有其他瓶颈(例如非本地加载/存储)可能会大大超过这一点。谢谢,如果我听不到更有说服力的话,我可能会接受这个。 - Mehrdad 2011年5月27日4:18

关于这个问题的答案提供了关于如何使循环列表的索引能够很好地执行的附加信息,包括代码示例,以及一些使得权衡决策更加清晰的定量数字。因此,它们提供的信息与另一个问题中存在的信息互为补充。所以我同意这个问题的意图非常相似,我同意它应该被视为重复。如果现在伴随这一信息的新信息将丢失,那将是一种耻辱。

此外,我仍然对实施选择的潜在其他原因感兴趣,这些原因可能尚未出现在任何一个问题的答案中。

2 个答案:

答案 0 :(得分:5)

实际上,可以使用O(1)访问时间来实现循环数组。但是我不相信List<T>索引器的意图是O(1)。 Big O表示法跟踪性能,因为它与其运行的集合的大小有关。 List<T>的实现者,除了想要O(1)之外,还可能会痴迷于其他项目,如指令计数和紧密循环中的性能。它需要在相同的场景中尽可能接近阵列性能,以便通常有用。访问数组的元素是一个非常简单的操作

  • 将索引乘以大小添加到数组开始
  • Deference

在循环数组中索引,同时仍为O(1),涉及至少一个分支操作。它必须根据所需的索引检查数组索引是否需要回绕。这意味着具有已知边界的循环上的紧密数组将在其内部具有分支逻辑。这将是代码路径中的重要下降,在原始阵列上具有快速紧密循环。

编辑

啊,是的,不一定需要分支机构。但是指令计数仍然高于数组的指令数。我认为这仍然会影响作者的担忧。

另外一个问题是prepend是否是优先操作。如果prepend不被视为优先级,那么为什么在一个场景中(索引当然是一个优先事项)会遇到任何性能损失?我的猜测是,索引,枚举和添加到数组末尾的是具有最高优先级的场景。像前置这样的操作可能被认为是罕见的。

答案 1 :(得分:1)

您的问题让我很好奇List&lt;&gt;之间的运行时差异的大小。和一个循环版本。所以我把一个快速的骨架拼成一个强制大小为2的幂(这避免了模运算),即一个最好的情况实现。我没有成长,因为我只想比较索引器属性时差。这不是太糟糕; circularList [x]的速度大约是list [x]的两倍,两者都非常快。我也没有真正调试过,因为我的时间有限。这也可能缺少一些List&lt;&gt;的验证码。正在做,这会使圆形列表相对较慢,如果它也有它。

一般来说,我会说这种行为只会分散List&lt;&gt;的主要目的,这种目的很少实际使用。因此,您在很多用途上都会降低性能,从而使用量极少。我认为他们做出了一个很好的决定,不将它烘焙到List&lt;&gt;。

using System;

public class CircularList<T> {
    private int start, end, count, mask;
    private T[] items;
    public CircularList() : this(8) { }

    public CircularList(int capacity) {
        int size = IsPowerOf2(capacity) ? capacity : PowerOf2Ceiling(capacity);
        this.items = new T[size];
        this.start = this.end = this.count = 0;
        this.mask = size - 1;
    }

    public void Add(T item) {
        if (this.count == 0) {
            this.items[0] = item;
            this.start = this.end = 0;
        } else {
            this.items[++this.end] = item;
        }
        this.count++;
    }

    public void Prepend(T item) {
        if (this.count == 0) {
            this.items[0] = item;
            this.start = this.end = 0;
        } else {
            this.start--;
            if (this.start < 0) this.start = this.items.Length - 1;
            this.items[this.start] = item;
        }
        this.count++;
    }

    public T this[int index] {
        get {
            if ((index < 0) || (index >= this.count)) throw new ArgumentOutOfRangeException();
            return this.items[(index + this.start) & this.mask]; // (index + start) % length
        }
        set {
            if ((index < 0) || (index >= this.count)) throw new ArgumentOutOfRangeException();
            this.items[(index + this.start) & this.mask] = value; // (index + start) % length
        }
    }

    private bool IsPowerOf2(int value) {
        return (value > 0) && ((value & (value - 1)) == 0);
    }
    private int PowerOf2Ceiling(int value) {
        if (value < 0) return 1;
        switch (value) {
            case 0:
            case 1: return 1;
        }
        value--;
        value |= value >> 1;
        value |= value >> 2;
        value |= value >> 4;
        value |= value >> 8;
        return unchecked((value | (value >> 16)) + 1);
    }
}