为什么 List<T>.Enumerator 比我的实现更快?

16
我发现自己需要编写动态数组的实现,因为这样可以带来很多性能上的优势(在我的情况下)。然而,在为我的版本创建枚举器并将其效率与 List 使用的枚举器进行比较后,我有些困惑;List 的枚举器大约比我的版本快 30-40%,即使它更加复杂。
以下是 List 枚举器实现的重要部分:
public struct Enumerator : IEnumerator<T>, IDisposable, IEnumerator
{
    private List<T> list;
    private int index;
    private int version;
    private T current;
    internal Enumerator(List<T> list)
    {
        this.list = list;
        this.index = 0;
        this.version = list._version;
        this.current = default(T);
        return;
    }

    public bool MoveNext()
    {
        List<T> list;
        list = this.list;
        if (this.version != list._version)
        {
            goto Label_004A;
        }
        if (this.index >= list._size)
        {
            goto Label_004A;
        }
        this.current = list._items[this.index];
        this.index += 1;
        return 1;
        Label_004A:
        return this.MoveNextRare();
    }

    public T Current
    {
        get {  return this.current; }
    }
}

以下是我非常简略的版本:

internal struct DynamicArrayEnumerator<T> : IEnumerator<T> where T : class
{
     private readonly T[] internalArray;
     private readonly int lastIndex;
     private int currentIndex;

     internal DynamicArrayEnumerator(DynamicArray<T> dynamicArray)
     {
          internalArray = dynamicArray.internalArray;
          lastIndex = internalArray.Length - 1;
          currentIndex = -1;
     }

     public T Current
     {
          get { return internalArray[currentIndex]; }
     }

     public bool MoveNext()
     {
          return (++currentIndex <= lastIndex);
     }
}

我知道这是微观优化,但我真的很想了解为什么List枚举器比我的快那么多。有什么想法吗?谢谢!

编辑: 根据要求,这是DynamicArray类(相关部分): 枚举器是其中的一个内部类。

public struct DynamicArray<T> : IEnumerable<T> where T : class
{
    private T[] internalArray;
    private int itemCount;

    internal T[] Data
    {
        get { return internalArray; }
    }

    public int Count
    {
        get { return itemCount; }
    }

    public DynamicArray(int count)
    {
        this.internalArray = new T[count];
        this.itemCount = 0;
    }

    public IEnumerator<T> GetEnumerator()
    {
        return new DynamicArrayEnumerator<T>(this);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }

}

关于我的测试方式:

 List<BaseClass> list = new List<BaseClass>(1000000);
 DynamicArray<BaseClass> dynamicArray = new DynamicArray<BaseClass>(1000000);

// Code for filling with data omitted.

   int numberOfRuns = 0;
   float p1Total = 0;
   float p2Total = 0;
   while (numberOfRuns < 100)
   {
        PerformanceAnalyzer p1 = new PerformanceAnalyzer(() =>
        {
             int u = 0;
             foreach (BaseClass b in list)
             {
                  if (b.B > 100)   // Some trivial task
                      u++;
             }
        });
        p1.ExecuteAndClock();
        p1Total += p1.TotalElapsedTicks;

        PerformanceAnalyzer p2 = new PerformanceAnalyzer(() =>
        {
             int u = 0;
             foreach (BaseClass b in dynamicArray)
             {
                  if (b.B > 100)  // Some trivial task
                       u++;
             }
        });
        p2.ExecuteAndClock();
        p2Total += p2.TotalElapsedTicks;

        numberOfRuns++;
    }

    Console.WriteLine("List enumeration: " + p1Total / totalRuns + "\n");
    Console.WriteLine("Dynamic array enumeration: " + p2Total / totalRuns + "\n");

PerformanceAnalyzer类基本上启动一个计时器,执行提供的Action委托,然后在之后停止计时器。

编辑2(对Ryan Gates的快速答复): 我想自己编写的原因有几个,最重要的是我需要一个非常快速的RemoveAt(int index)方法。

由于在我的特定情况下不必担心列表元素的顺序,因此我可以避免使用.Net内置列表的方法:

public void RemoveAt(int index)
{
    T local;
    if (index < this._size)
    {
        goto Label_000E;
    }
    ThrowHelper.ThrowArgumentOutOfRangeException();
Label_000E:
    this._size -= 1;
    if (index >= this._size)
    {
        goto Label_0042;
    }
    Array.Copy(this._items, index + 1, this._items, index, this._size - index);
Label_0042:
    this._items[this._size] = default(T);
    this._version += 1;
    return;
}

而不是使用以下类似的内容:
public void RemoveAt(int index)
{
     // overwrites the element at the specified index with the last element in the array and decreases the item count.
     internalArray[index] = internalArray[itemCount];  
     itemCount--;
}

在我的情况下,可能会节省大量时间,比如说要通过索引删除长列表中的前1000个元素。


4
你能否发布你的DynamicArray类的基本框架?我有一个想法,但我不想在没有一些验证的情况下就进入它...另外,请展示你的基准测试代码。 - Jon Skeet
5
您的版本需要更多的GoTo语句。 :-) - LarsTech
2
你应该在每个秒表的开始/停止范围内执行多次运行。将100次循环放在该范围内,而不是在外部。 - Servy
为什么你必须自行实现?我无法想象这种情况。如果你一定要这样做,我建议去查看 实际的 .net 源代码,请注意此链接仅在 IE 中有效。 - Ryan Gates
@Dims,这基本上是一种告诉我们在枚举时底层集合是否被修改的方法。 - Servy
显示剩余7条评论
1个回答

16

除了基准测试问题之外,以下是如何使您的DynamicArray类更像List<T>的方法:

public DynamicArrayEnumerator<T> GetEnumerator()
{
    return new DynamicArrayEnumerator<T>(this);
}

IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
    return GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator()
{
    return this.GetEnumerator();
}

现在,知道自己正在使用动态数组的代码可以使用 DynamicArrayEnumerator<T> 进行迭代,无需任何装箱和虚拟调度。这正是 List<T> 所做的。编译器会注意到当一个类型以自定义方式实现模式时,并将使用涉及的类型而不是接口。

使用您当前的代码,创建struct没有任何好处-因为在GetEnumerator()中对其进行了装箱。

尝试上述更改并且修复基准以长时间工作。 我希望看到一个很大的区别。


1
是的,这真的有很大的改善,非常感谢。现在我的程序速度快了3倍。 - H.S.
@H.S.:好的。这就是我一开始的猜测,但如果我错了,解释起来会很混乱... - Jon Skeet
1
@JonSkeet 如果将 DynamicArray 先转换为父类型,例如 IEnumerable<int> myArray = new DynamicArray<int>(10000);,那么这种优化是否会丢失?foreach (x in myArray) {...} - Matthew
2
@Matthew:是的,List<T>也是一样的。 - Jon Skeet

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接