Why is AddRange faster than using a foreach loop?

var fillData = new List<int>();
for (var i = 0; i < 100000; i++)
     fillData.Add(i);

var stopwatch1 = new Stopwatch();
stopwatch1.Start();

var autoFill = new List<int>();
autoFill.AddRange(fillData);
stopwatch1.Stop();

var stopwatch2 = new Stopwatch();
stopwatch2.Start();

var manualFill = new List<int>();

foreach (var i in fillData)
    manualFill.Add(i);
stopwatch2.Stop();

When I take 4 results from stopwach1 and stopwach2, stopwatch1 has always lower value than stopwatch2. That means addrange is always faster than foreach. Does anyone know why?


Solution 1:

Potentially, AddRange can check where the value passed to it implements IList or IList<T>. If it does, it can find out how many values are in the range, and thus how much space it needs to allocate... whereas the foreach loop may need to reallocate several times.

Additionally, even after allocation, List<T> can use IList<T>.CopyTo to perform a bulk copy into the underlying array (for ranges which implement IList<T>, of course.)

I suspect you'll find that if you try your test again but using Enumerable.Range(0, 100000) for fillData instead of a List<T>, the two will take about the same time.

Solution 2:

If you are using Add, it is resizing the inner array gradually as needed (doubling), from the default starting size of 10 (IIRC). If you use:

var manualFill = new List<int>(fillData.Count);

I expect it'll change radically (no more resizes / data copy).

From reflector, AddRange does this internally, rather than growing in doubling:

ICollection<T> is2 = collection as ICollection<T>;
if (is2 != null)
{
    int count = is2.Count;
    if (count > 0)
    {
        this.EnsureCapacity(this._size + count);
        // ^^^ this the key bit, and prevents slow growth when possible ^^^

Solution 3:

Because AddRange checks size of added items and increases size of internal array only once.

Solution 4:

The dissassembly from reflector for the List AddRange method has the following code

ICollection<T> is2 = collection as ICollection<T>;
if (is2 != null)
{
    int count = is2.Count;
    if (count > 0)
    {
        this.EnsureCapacity(this._size + count);
        if (index < this._size)
        {
            Array.Copy(this._items, index, this._items, index + count, this._size - index);
        }
        if (this == is2)
        {
            Array.Copy(this._items, 0, this._items, index, index);
            Array.Copy(this._items, (int) (index + count), this._items, (int) (index * 2), (int) (this._size - index));
        }
        else
        {
            T[] array = new T[count];
            is2.CopyTo(array, 0);
            array.CopyTo(this._items, index);
        }
        this._size += count;
    }
}

As you can see there are some optimizations like EnsureCapacity() call and using Array.Copy().