11

以下のコードは、同じソリューションを実行する 3 つの異なる方法のパフォーマンスをチェックしています。

    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

Enumerable.Aggregate はより多くのメソッド呼び出しを行うため、「for ループ」が他の 2 つのソリューションよりも高速であることは驚くべきことではありません。しかし、「Enumerable.Range」が「自作の IEnumerable」よりも高速であることに本当に驚きました。Enumerable.Range は単純な GetIntRange メソッドよりも多くのオーバーヘッドがあると思いました。

これにはどのような理由が考えられますか?

4

4 に答える 4

12

なぜ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(もちろん、Robが指摘しているように、リリースモードでコンパイルします)。この実装は、コンパイラがイテレータブロックから生成するものに関してわずかに最適化される場合があります。

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; } }
    }
}
于 2009-01-03T02:19:35.527 に答える
5

私の推測では、デバッガーで実行していると思います。「/o+ /debug-」を使用してコマンドラインからビルドした結果は次のとおりです。

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

まだわずかな違いがありますが、それほど顕著ではありません。Iterator ブロックの実装は、カスタム ソリューションほど効率的ではありませんが、かなり優れています。

于 2009-01-03T08:47:44.480 に答える
4

これが実行中のリリースビルドであると仮定すると、JITが完全に機能しないため、すべての比較がオフになります。

リフレクターを使用してアセンブリを確認し、「yield」ステートメントが拡張されていることを確認できます。コンパイラーは、イテレーターをカプセル化するクラスを作成します。おそらく、手作業でコーディングされたEnumerable.Rangeの実装よりも、生成されたコードでより多くのハウスキーピングが行われている可能性があります。

于 2009-01-03T02:16:35.673 に答える
2

リフレクター出力のわずかな違い(および引数チェックと追加レベルの内部化は、ここでは明らかに関係ありません)。基本的なコードは次のようなものです。

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

つまり、別のローカル変数の代わりに、すべての利回りに追加の加算を適用します。

これをベンチマークしようとしましたが、理解できる結果を得るのに十分な外部プロセスを停止できません。また、JITコンパイラの影響を無視するために、各テストを2回試しましたが、それでも「興味深い」結果が得られます。

これが私の結果のサンプルです:

実行0:
時間=4149; 結果=405000000450000000
時間=25645; 結果=405000000450000000
時間=39229; 結果=405000000450000000
時間=29872; 結果=405000000450000000

時間=4277; 結果=405000000450000000
時間=26878; 結果=405000000450000000
時間=26333; 結果=405000000450000000
時間=26684; 結果=405000000450000000

実行1:
時間=4063; 結果=405000000450000000
時間=22714; 結果=405000000450000000
時間=34744; 結果=405000000450000000
時間=26954; 結果=405000000450000000

時間=4033; 結果=405000000450000000
時間=26657; 結果=405000000450000000
時間=25855; 結果=405000000450000000
時間=25031; 結果=405000000450000000

実行2:
時間=4021; 結果=405000000450000000
時間=21815; 結果=405000000450000000
時間=34304; 結果=405000000450000000
時間=32040; 結果=405000000450000000

時間=3993; 結果=405000000450000000
時間=24779; 結果=405000000450000000
時間=29275; 結果=405000000450000000
時間=32254; 結果=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
于 2010-05-28T22:48:04.937 に答える