Почему Enumerable.Range работает быстрее, чем прямой цикл yield?

В приведенном ниже коде проверяется производительность трех разных способов выполнения одного и того же решения.

    public static void Main(string[] args)
    {
        // for loop
        {
            Stopwatch sw = Stopwatch.StartNew();

            int accumulator = 0;
            for (int i = 1; i <= 100000000; ++i)
            {
                accumulator += i;
            }

            sw.Stop();

            Console.WriteLine("time = {0}; result = {1}", sw.ElapsedMilliseconds, accumulator);
        }

        //Enumerable.Range
        {
            Stopwatch sw = Stopwatch.StartNew();

            var ret = Enumerable.Range(1, 100000000).Aggregate(0, (accumulator, n) => accumulator + n);

            sw.Stop();
            Console.WriteLine("time = {0}; result = {1}", sw.ElapsedMilliseconds, ret);
        }

        //self-made IEnumerable<int>
        {
            Stopwatch sw = Stopwatch.StartNew();

            var ret = GetIntRange(1, 100000000).Aggregate(0, (accumulator, n) => accumulator + n);

            sw.Stop();
            Console.WriteLine("time = {0}; result = {1}", sw.ElapsedMilliseconds, ret);
        }
    }

    private static IEnumerable<int> GetIntRange(int start, int count)
    {
        int end = start + count;

        for (int i = start; i < end; ++i)
        {
            yield return i;
        }
    }
}

Результаты:

time = 306; result = 987459712
time = 1301; result = 987459712
time = 2860; result = 987459712

Неудивительно, что цикл for работает быстрее, чем два других решения, потому что Enumerable.Aggregate требует больше вызовов методов. Однако меня действительно удивляет, что "Enumerable.Range" работает быстрее, чем "самодельный IEnumerable". Я думал, что Enumerable.Range будет иметь больше накладных расходов, чем простой метод GetIntRange.

Каковы возможные причины этого?


person Morgan Cheng    schedule 03.01.2009    source источник


Ответы (4)


Почему Enumerable.Range должен быть медленнее, чем ваш самодельный GetIntRange? На самом деле, если бы Enumerable.Range было определено как

public static class Enumerable {
    public static IEnumerable<int> Range(int start, int count) {
        var end = start + count;
        for(var current = start; current < end; ++current) {
            yield return current;
        }
    }
}

то он должен быть ровно таким же быстрым, как ваш самопальный GetIntRange. Фактически это эталонная реализация для Enumerable.Range, без каких-либо ухищрений со стороны компилятора или программиста.

Вы можете сравнить свои GetIntRange и System.Linq.Enumerable.Range со следующей реализацией (конечно, скомпилируйте в режиме выпуска, как указывает Роб). Эта реализация может быть немного оптимизирована по отношению к тому, что компилятор будет генерировать из блока итератора.

public static class Enumerable {
    public static IEnumerable<int> Range(int start, int count) {
        return new RangeEnumerable(start, count);
    }
    private class RangeEnumerable : IEnumerable<int> {
        private int _Start;
        private int _Count;
        public RangeEnumerable(int start, int count) {
            _Start = start;
            _Count = count;
        }
        public virtual IEnumerator<int> GetEnumerator() {
            return new RangeEnumerator(_Start, _Count);
        }
        IEnumerator IEnumerable.GetEnumerator() {
            return GetEnumerator();
        }
    }
    private class RangeEnumerator : IEnumerator<int> {
        private int _Current;
        private int _End;
        public RangeEnumerator(int start, int count) {
            _Current = start - 1;
            _End = start + count;
        }
        public virtual void Dispose() {
            _Current = _End;
        }
        public virtual void Reset() {
            throw new NotImplementedException();
        }
        public virtual bool MoveNext() {
            ++_Current;
            return _Current < _End;
        }
        public virtual int Current { get { return _Current; } }
        object IEnumerator.Current { get { return Current; } }
    }
}
person yfeldblum    schedule 03.01.2009

Я предполагаю, что вы работаете в отладчике. Вот мои результаты, построенные из командной строки с помощью «/o+ /debug-»

time = 142; result = 987459712
time = 1590; result = 987459712
time = 1792; result = 987459712

Небольшая разница все же есть, но не такая выраженная. Реализации блоков итераторов не так эффективны, как индивидуальные решения, но они довольно хороши.

person Jon Skeet    schedule 03.01.2009
comment
Да, я проводил эксперименты в режиме отладки. Итак, самодельный метод генерирует отладочный код. Релиз сделан намного быстрее. - person Morgan Cheng; 03.01.2009
comment
Здесь есть две вещи: сборка в режиме отладки и запуск в отладчике, а не выполнение без подключенного отладчика. Последнее имеет большее значение. - person Jon Skeet; 03.01.2009

Предполагая, что это работающая сборка выпуска, в противном случае все сравнения отключены, поскольку JIT не будет работать в полную силу.

Вы можете посмотреть на сборку с помощью reflector и посмотреть, что такое оператор 'yield' тоже расширяется. Компилятор создаст класс для инкапсуляции итератора. Возможно, в сгенерированном коде происходит больше уборки, чем реализация Enumerable.Range, которая, вероятно, написана вручную.

person Rob Walker    schedule 03.01.2009

Небольшая разница в выводе Reflector (а также проверка аргументов и дополнительный уровень интернализации здесь определенно неуместны). Основной код больше похож на:

public static IEnumerable<int> Range(int start, int count) {
    for(int current = 0; current < count; ++current) {
        yield return start + current;
    }
}

То есть вместо другой локальной переменной они применяют дополнительную добавку для каждого урожая.

Я попытался сравнить это, но я не могу остановить достаточное количество внешних процессов, чтобы получить понятные результаты. Я также пробовал каждый тест дважды, чтобы игнорировать эффекты JIT-компилятора, но даже это дает «интересные» результаты.

Вот пример моих результатов:

Run 0:
time = 4149; result = 405000000450000000
time = 25645; result = 405000000450000000
time = 39229; result = 405000000450000000
time = 29872; result = 405000000450000000

time = 4277; result = 405000000450000000
time = 26878; result = 405000000450000000
time = 26333; result = 405000000450000000
time = 26684; result = 405000000450000000

Run 1:
time = 4063; result = 405000000450000000
time = 22714; result = 405000000450000000
time = 34744; result = 405000000450000000
time = 26954; result = 405000000450000000

time = 4033; result = 405000000450000000
time = 26657; result = 405000000450000000
time = 25855; result = 405000000450000000
time = 25031; result = 405000000450000000

Run 2:
time = 4021; result = 405000000450000000
time = 21815; result = 405000000450000000
time = 34304; result = 405000000450000000
time = 32040; result = 405000000450000000

time = 3993; result = 405000000450000000
time = 24779; result = 405000000450000000
time = 29275; result = 405000000450000000
time = 32254; result = 405000000450000000

и код

using System;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;

namespace RangeTests
{
  class TestRange
  {
    public static void Main(string[] args)
    {
      for(int l = 1; l <= 2; ++l)
      {
        const int N = 900000000;
        System.GC.Collect(2);
        // for loop
        {
            Stopwatch sw = Stopwatch.StartNew();

            long accumulator = 0;
            for (int i = 1; i <= N; ++i)
            {
                accumulator += i;
            }

            sw.Stop();

            Console.WriteLine("time = {0}; result = {1}", sw.ElapsedMilliseconds, accumulator);
        }
        System.GC.Collect(2);

        //Enumerable.Range
        {
            Stopwatch sw = Stopwatch.StartNew();

            var ret = Enumerable.Range(1, N).Aggregate(0, (long accumulator,int n) => accumulator + n);

            sw.Stop();
            Console.WriteLine("time = {0}; result = {1}", sw.ElapsedMilliseconds, ret);
        }
        System.GC.Collect(2);

        //self-made IEnumerable<int>
        {
            Stopwatch sw = Stopwatch.StartNew();

            var ret = GetIntRange(1, N).Aggregate(0, (long accumulator,int n) => accumulator + n);

            sw.Stop();
            Console.WriteLine("time = {0}; result = {1}", sw.ElapsedMilliseconds, ret);
        }
        System.GC.Collect(2);

        //self-made adjusted IEnumerable<int>
        {
            Stopwatch sw = Stopwatch.StartNew();

            var ret = GetRange(1, N).Aggregate(0, (long accumulator,int n) => accumulator + n);

            sw.Stop();
            Console.WriteLine("time = {0}; result = {1}", sw.ElapsedMilliseconds, ret);
        }
        System.GC.Collect(2);
        Console.WriteLine();
    } }

    private static IEnumerable<int> GetIntRange(int start, int count)
    {
        int end = start + count;

        for (int i = start; i < end; ++i)
        {
            yield return i;
        }
    }

    private static IEnumerable<int> GetRange(int start, int count)
    {
        for (int i = 0; i < count; ++i)
        {
            yield return start + i;
        }
    }
} }

скомпилировано с

csc.exe -optimize+ -debug- RangeTests.cs
person Mark Hurd    schedule 28.05.2010