web-dev-qa-db-ja.com

C#の制御構造「for」と「foreach」のパフォーマンスの違い

パフォーマンスを向上させるコードスニペットはどれですか?以下のコードセグメントはC#で記述されています。

1。

for(int counter=0; counter<list.Count; counter++)
{
    list[counter].DoSomething();
}

2。

foreach(MyType current in list)
{
    current.DoSomething();
}
103
Kthevar

まあ、それは部分的にlistの正確なタイプに依存します。また、使用している正確なCLRにも依存します。

何らかの方法でsignificantであるかどうかは、ループで実際の作業を行っているかどうかによって決まります。ほぼallの場合、パフォーマンスの違いはそれほど大きくありませんが、読みやすさの違いはforeachループを優先します。

私も個人的にLINQを使用して「if」を回避します。

foreach (var item in list.Where(condition))
{
}

編集:foreachList<T>を反復処理するとforループと同じコードが生成されると主張している人のために、そうではないという証拠があります:

static void IterateOverList(List<object> list)
{
    foreach (object o in list)
    {
        Console.WriteLine(o);
    }
}

次のILを生成します。

.method private hidebysig static void  IterateOverList(class [mscorlib]System.Collections.Generic.List`1<object> list) cil managed
{
  // Code size       49 (0x31)
  .maxstack  1
  .locals init (object V_0,
           valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object> V_1)
  IL_0000:  ldarg.0
  IL_0001:  callvirt   instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<object>::GetEnumerator()
  IL_0006:  stloc.1
  .try
  {
    IL_0007:  br.s       IL_0017
    IL_0009:  ldloca.s   V_1
    IL_000b:  call       instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object>::get_Current()
    IL_0010:  stloc.0
    IL_0011:  ldloc.0
    IL_0012:  call       void [mscorlib]System.Console::WriteLine(object)
    IL_0017:  ldloca.s   V_1
    IL_0019:  call       instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object>::MoveNext()
    IL_001e:  brtrue.s   IL_0009
    IL_0020:  leave.s    IL_0030
  }  // end .try
  finally
  {
    IL_0022:  ldloca.s   V_1
    IL_0024:  constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object>
    IL_002a:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_002f:  endfinally
  }  // end handler
  IL_0030:  ret
} // end of method Test::IterateOverList

コンパイラはarraysを異なる方法で処理し、foreachループを基本的にforループに変換しますが、List<T>は変換しません。配列の同等のコードは次のとおりです。

static void IterateOverArray(object[] array)
{
    foreach (object o in array)
    {
        Console.WriteLine(o);
    }
}

// Compiles into...

.method private hidebysig static void  IterateOverArray(object[] 'array') cil managed
{
  // Code size       27 (0x1b)
  .maxstack  2
  .locals init (object V_0,
           object[] V_1,
           int32 V_2)
  IL_0000:  ldarg.0
  IL_0001:  stloc.1
  IL_0002:  ldc.i4.0
  IL_0003:  stloc.2
  IL_0004:  br.s       IL_0014
  IL_0006:  ldloc.1
  IL_0007:  ldloc.2
  IL_0008:  ldelem.ref
  IL_0009:  stloc.0
  IL_000a:  ldloc.0
  IL_000b:  call       void [mscorlib]System.Console::WriteLine(object)
  IL_0010:  ldloc.2
  IL_0011:  ldc.i4.1
  IL_0012:  add
  IL_0013:  stloc.2
  IL_0014:  ldloc.2
  IL_0015:  ldloc.1
  IL_0016:  ldlen
  IL_0017:  conv.i4
  IL_0018:  blt.s      IL_0006
  IL_001a:  ret
} // end of method Test::IterateOverArray

興味深いことに、C#3仕様にこのドキュメントが記載されていません...

130
Jon Skeet

forループは、これとほぼ同等のコードにコンパイルされます。

int tempCount = 0;
while (tempCount < list.Count)
{
    if (list[tempCount].value == value)
    {
        // Do something
    }
    tempCount++;
}

foreachループが、これとほぼ同等のコードにコンパイルされる場所:

using (IEnumerator<T> e = list.GetEnumerator())
{
    while (e.MoveNext())
    {
        T o = (MyClass)e.Current;
        if (row.value == value)
        {
            // Do something
        }
    }
}

ご覧のとおり、列挙子の実装方法とリストインデクサーの実装方法にすべて依存します。結局のところ、配列に基づく型の列挙子は通常、次のように記述されます。

private static IEnumerable<T> MyEnum(List<T> list)
{
    for (int i = 0; i < list.Count; i++)
    {
        yield return list[i];
    }
}

ご覧のとおり、この例ではそれほど違いはありませんが、リンクリストの列挙子はおそらく次のようになります。

private static IEnumerable<T> MyEnum(LinkedList<T> list)
{
    LinkedListNode<T> current = list.First;
    do
    {
        yield return current.Value;
        current = current.Next;
    }
    while (current != null);
}

。NET では、LinkedList <T>クラスにはインデクサーさえないため、リンクリストでforループを実行することはできません。ただし、可能であれば、インデクサーは次のように記述する必要があります。

public T this[int index]
{
       LinkedListNode<T> current = this.First;
       for (int i = 1; i <= index; i++)
       {
            current = current.Next;
       }
       return current.value;
}

ご覧のとおり、ループ内でこれを複数回呼び出すと、リスト内の場所を記憶できる列挙子を使用するよりもはるかに遅くなります。

14
Martin Brown

半検証する簡単なテスト。ちょっとしたテストをしてみました。コードは次のとおりです。

static void Main(string[] args)
{
    List<int> intList = new List<int>();

    for (int i = 0; i < 10000000; i++)
    {
        intList.Add(i);
    }

    DateTime timeStarted = DateTime.Now;
    for (int i = 0; i < intList.Count; i++)
    {
        int foo = intList[i] * 2;
        if (foo % 2 == 0)
        {
        }
    }

    TimeSpan finished = DateTime.Now - timeStarted;

    Console.WriteLine(finished.TotalMilliseconds.ToString());
    Console.Read();

}

そして、これがforeachセクションです。

foreach (int i in intList)
{
    int foo = i * 2;
    if (foo % 2 == 0)
    {
    }
}

Forをforeachに置き換えると、foreachは20ミリ秒速くなりました-consistently。 forは135-139msで、foreachは113-119msでした。私は何度かやり取りしましたが、それがちょうど始まったプロセスではないことを確認しました。

ただし、fooとifステートメントを削除すると、forは30ミリ秒速くなりました(foreachは88ミリ秒、forは59ミリ秒)。どちらも空のシェルでした。 foreachが実際に変数を渡したのは、forが変数をインクリメントしているだけだと想定しているからです。追加した場合

int foo = intList[i];

その後、forは約30ms遅くなります。これは、fooを作成し、配列内の変数を取得してfooに割り当てることに関係していると思います。 intList [i]にアクセスするだけであれば、そのペナルティはありません。

正直なところ、私はforeachがすべての状況でわずかに遅くなると思っていましたが、ほとんどのアプリケーションでは問題にはなりません。

編集:これはJonsの提案を使用した新しいコードです(134217728は、System.OutOfMemory例外がスローされる前に持つことができる最大のintです):

static void Main(string[] args)
{
    List<int> intList = new List<int>();

    Console.WriteLine("Generating data.");
    for (int i = 0; i < 134217728 ; i++)
    {
        intList.Add(i);
    }

    Console.Write("Calculating for loop:\t\t");

    Stopwatch time = new Stopwatch();
    time.Start();
    for (int i = 0; i < intList.Count; i++)
    {
        int foo = intList[i] * 2;
        if (foo % 2 == 0)
        {
        }
    }

    time.Stop();
    Console.WriteLine(time.ElapsedMilliseconds.ToString() + "ms");
    Console.Write("Calculating foreach loop:\t");
    time.Reset();
    time.Start();

    foreach (int i in intList)
    {
        int foo = i * 2;
        if (foo % 2 == 0)
        {
        }
    }

    time.Stop();

    Console.WriteLine(time.ElapsedMilliseconds.ToString() + "ms");
    Console.Read();
}

結果は次のとおりです。

データを生成します。 forループの計算:2458ms foreachループの計算:2005ms

それらを入れ替えて、物事の順序を処理するかどうかを確認すると、ほぼ同じ結果が得られます。

12
Kenny Mann

注:この答えは、C#がLinkedListsにインデクサーを持たないため、C#に比べてJavaに適用されますが、一般的なポイントはまだを保持します。

使用しているlistLinkedListである場合、インデクサーコード(array-style access)のパフォーマンスは、使用するよりもはるかに悪いです。大きなリストの場合は、IEnumeratorからのforeach

インデクサー構文を使用して、LinkedList内の要素10.000にアクセスする場合:list[10000]、リンクリストはヘッドノードから開始し、Next- pointerを1万回走査して、正しいオブジェクトに到達します。明らかに、これをループで実行すると、次の結果が得られます。

list[0]; // head
list[1]; // head.Next
list[2]; // head.Next.Next
// etc.

GetEnumeratorを(暗黙的にforach- syntaxを使用して)呼び出すと、ヘッドノードへのポインターを持つIEnumeratorオブジェクトを取得します。 MoveNextを呼び出すたびに、そのポインターは次のように次のノードに移動します。

IEnumerator em = list.GetEnumerator();  // Current points at head
em.MoveNext(); // Update Current to .Next
em.MoveNext(); // Update Current to .Next
em.MoveNext(); // Update Current to .Next
// etc.

ご覧のとおり、LinkedListsの場合、ループが長くなるほど、配列インデクサーメソッドはますます遅くなります(同じヘッドポインターを何度も繰り返す必要があります)。 IEnumerableは一定時間で動作します。

もちろん、ジョンが言ったように、これはlistのタイプに本当に依存します。listLinkedListではなく配列である場合、動作は完全に異なります。

9
Tom Lokhorst

他の人が言及したように、パフォーマンスは実際にはそれほど重要ではありませんが、foreachは、ループでのIEnumerable/IEnumeratorの使用のため、常に少し遅くなります。コンパイラーはその構成をそのインターフェース上の呼び出しに変換し、すべてのステップで、関数+プロパティがforeach構成で呼び出されます。

IEnumerator iterator = ((IEnumerable)list).GetEnumerator();
while (iterator.MoveNext()) {
  var item = iterator.Current;
  // do stuff
}

これは、C#の構造の同等の展開です。 MoveNextおよびCurrentの実装に基づいて、パフォーマンスへの影響がどのように変化するか想像できます。一方、配列アクセスでは、その依存関係はありません。

「foreachループは読みやすさのために優先されるべきです」という十分な引数を読んだ後、私の最初の反応は「何」だったと言えますか?一般的に、読みやすさは主観的であり、この特定の例ではさらに読みやすくなります。プログラミング(実際にはJavaより前のすべての言語)のバックグラウンドを持つ人にとって、forループはforeachループよりも読みやすいです。さらに、foreachループの方が読みやすいと主張している同じ人々は、コードの読み取りと保守を困難にするlinqおよびその他の「機能」の支持者でもあり、これは上記のポイントを証明しています。

パフォーマンスへの影響については、 this 質問への回答を参照してください。

編集:C#には、インデクサーを持たないコレクション(HashSetなど)があります。これらのコレクションでは、foreachが反復する唯一の方法であり、それが使用されるべきだと思う唯一のケースですfor.

1
ThunderGr

提供した例では、foreachループの代わりにforループを使用することをお勧めします。

標準のforeachコンストラクトは、ループが展開されていない限り(ステップあたり1.0サイクル)、単純なfor-loop(ステップあたり2サイクル)よりも高速(ステップあたり1.5サイクル)になります。

したがって、日常のコードでは、パフォーマンスは、より複雑なforwhile、またはdo-while構造を使用する理由ではありません。

このリンクを確認してください: http://www.codeproject.com/Articles/146797/Fast-and-Less-Fast-Loops-in-C


╔══════════════════════╦═══════════╦═══════╦════════════════════════╦═════════════════════╗
║        Method        ║ List<int> ║ int[] ║ Ilist<int> onList<Int> ║ Ilist<int> on int[] ║
╠══════════════════════╬═══════════╬═══════╬════════════════════════╬═════════════════════╣
║ Time (ms)            ║ 23,80     ║ 17,56 ║ 92,33                  ║ 86,90               ║
║ Transfer rate (GB/s) ║ 2,82      ║ 3,82  ║ 0,73                   ║ 0,77                ║
║ % Max                ║ 25,2%     ║ 34,1% ║ 6,5%                   ║ 6,9%                ║
║ Cycles / read        ║ 3,97      ║ 2,93  ║ 15,41                  ║ 14,50               ║
║ Reads / iteration    ║ 16        ║ 16    ║ 16                     ║ 16                  ║
║ Cycles / iteration   ║ 63,5      ║ 46,9  ║ 246,5                  ║ 232,0               ║
╚══════════════════════╩═══════════╩═══════╩════════════════════════╩═════════════════════╝

0
rpax

Deep .NET-part 1 Iteration でそれについて読むことができます

.NETソースコードから逆アセンブリまでの結果(最初の初期化なし)をカバーします。

例-foreachループを使用した配列反復: enter image description here

および-foreachループを使用して反復をリストします。 enter image description here

最終結果: enter image description here

enter image description here

0
Or Yaacov

両方のループの速度をテストするときに簡単に見落とされる可能性がある、さらに興味深い事実があります。デバッグモードを使用しても、コンパイラはデフォルト設定を使用してコードを最適化しません。

これにより、foreachがデバッグモードの場合よりも高速であるという興味深い結果に至りました。一方、forはリリースモードのforeachより高速です。明らかに、コンパイラは、いくつかのメソッド呼び出しを危険にさらすforeachループよりも、forループを最適化するためのより良い方法を持っています。 forループは、これがCPU自体によって最適化される可能性があるほど基本的な方法です。

0
Sam