16

I've found myself in a position where I have to roll my own dynamic array implementation, due to various large performance benefits (in my case). However, after creating an enumerator for my version, and comparing the efficiency with the one List uses, I'm a bit bewildered; the List one is aproximately 30-40% faster than my version, even though it's much more complex.

Here's the important part of the List enumerator implementation:

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; }
    }
}

And here's my very barebone version:

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);
     }
}

I know this is micro-optimization, but I'm actually interested in understanding why the List enumerator is so much faster than mine. Any ideas? Thanks!

Edit: As requested; the DynamicArray class (the relevant parts): The enumerator is an inner class in this.

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();
    }

}

As for how I'm testing:

 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");

The PerformanceAnalyzer class basically starts a Stopwatch, execute the supplied Action delegate, and then stop the Stopwatch afterwards.

Edit 2 (Quick answer to Ryan Gates): There's a few reasons why I would want to roll my own, most importantly I need a very fast RemoveAt(int index) method.

Since I don't have to worry about the order of the list elements in my particular case, I can avoid the .Net built-in list's way of doing it:

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;
}

And instead using something along the lines of:

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--;
}

Potencially saving enormous amounts of time in my case, if say the first 1000 elements in a long list have to be removed by index.

H.S.
  • 363
  • 1
  • 10
  • 4
    Can you post the bare bones of your DynamicArray class? I have a thought, but I don't want to go into it without some validation... Also, please show your benchmarking code. – Jon Skeet Dec 06 '12 at 20:29
  • 5
    Your version needs more GoTos. :-) – LarsTech Dec 06 '12 at 20:32
  • I suspect it's because in your `Current` property, you're always accessing the array by index, while in the `List` example the `Current` property value is precomputed by the `MoveNext` call. If `Current` gets called multiple times, your class could have higher overhead. – Matthew Dec 06 '12 at 20:38
  • I tried storing the value in a field as well, but it was "a lot" slower in my case. – H.S. Dec 06 '12 at 20:46
  • @H.S.: As I said, please post your `DynamicArray` implementation. The *exact* way in which you implement `IEnumerable` matters. – Jon Skeet Dec 06 '12 at 20:46
  • 2
    You should really be executing more than 1 run within the scope of each stopwatch start/stop. Have your loop of 100 runs be within that scope, rather than outside of it. – Servy Dec 06 '12 at 20:50
  • Why do you have to roll your own? I can't begin to image a scenario. If you must, I would look at [the actual .net source code](http://referencesource.microsoft.com/netframework.aspx), keep in mind that this link only works in IE. – Ryan Gates Dec 06 '12 at 20:52
  • This might fit better at codereview.stackexchange.com – Erik W Dec 06 '12 at 20:55
  • What is `list._version` and how it can be unequal to `version` if it was set only in constructor? What is `MoveNextRare`? – Dims Dec 06 '12 at 20:56
  • @Dims I's basically a way of telling if the underlying collection was mutated while it was being enumerated. – Servy Dec 06 '12 at 20:58
  • Servy: Yeah, good idea. Still a 20%+ difference in runtime though. Dims: Ask microsoft, that's their list version. – H.S. Dec 06 '12 at 20:58
  • @H.S., I has no `_version` member. I have no `_items` member, I have no `1` automatically converted to `bool`. – Dims Dec 06 '12 at 21:03

1 Answers1

16

Okay, aside from benchmarking problems, here's how you can make your DynamicArray class more like List<T>:

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

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

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

Now, code which knows it's working with a dynamic array can iterate with a DynamicArrayEnumerator<T> without any boxing, and without virtual dispatch. This is exactly what List<T> does. The compiler notices when a type implements the pattern in a custom manner, and will use the types involved instead of the interfaces.

With your current code, you're getting no benefit from creating a struct - because you're boxing it in GetEnumerator().

Try the above change and fix the benchmark to work for longer. I'd expect to see a big difference.

Tim S.
  • 55,448
  • 7
  • 96
  • 122
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 1
    Yeah, that made a totally amazing amount of difference, thanks a ton. Now mine's about 3 times faster. – H.S. Dec 06 '12 at 21:02
  • @H.S.: Goodo. This was my suspicion from the start, but it would have been confusing to explain it if I was wrong... – Jon Skeet Dec 06 '12 at 21:03
  • 1
    @JonSkeet Would this optimization be lost if `DynamicArray` was cast to a parent type first? eg `IEnumerable myArray = new DynamicArray(10000);` `foreach (x in myArray) {...}` ? – Matthew Dec 06 '12 at 21:17
  • 2
    @Matthew: Yup, and the same with `List`. – Jon Skeet Dec 06 '12 at 21:18