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


105

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

1。

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

2。

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

31
それは本当に重要ではないと思います。パフォーマンスの問題が発生している場合は、ほとんどの場合これが原因ではありません。質問しないでください...
darasd

2
あなたのアプリが非常にパフォーマンスクリティカルでない限り、私はこれについて心配しません。クリーンで簡単に理解できるコードを作成することをお勧めします。
Fortyrunner 2009

2
ここでの答えのいくつかは、脳のどこにも反復子の概念がなく、したがって列挙子やポインタの概念がない人々によって投稿されたように見えるのではないかと心配しています。
エドジェームス

3
その2番目のコードはコンパイルされません。System.Objectには 'value'というメンバーはありません(本当に邪悪な場合を除き、それを拡張メソッドとして定義し、デリゲートを比較しています)。foreachを強く入力します。
Trillian、

1
最初のコードもコンパイルされません。ただし、のタイプにlist実際にはのcount代わりにメンバーが含まれている場合を除きCountます。
ジョンスキート、

回答:


130

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

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

私は個人的にLINQを使用して "if"も回避しています:

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

編集:List<T>with を反復するとループforeachと同じコードが生成されると主張している人のために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

コンパイラは配列を異なる方法で処理し、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仕様に記載されている場所はどこにもありません...


興味深いことに、ジョン、上記のList <T>のシナリオ...他のコレクションにも適用されますか?また、これをどのようにして知ったのですか(悪意はまったく意図されていません)...以前のように、以前にこの質問に答えようとしたときに、文字通りこれに遭遇しましたか?それはそうです...ランダム/シークレット:)
Pure.Krome

5
私はしばらくの間、配列の最適化を認識してきました-配列は「コア」な種類のコレクションです。C#コンパイラはすでにそれらを深く認識しているため、それらを異なる方法で処理することは理にかなっています。コンパイラーは、の特別な知識を持っていません(してはいけません)List<T>
ジョンスキート、

乾杯:)そして、ええ...配列は私が何年も前にuniで教えた最初のコレクションコンセプトでした。そのため、コンパイラが(そうでない場合でも)最も原始的なタイプのコレクション。再び乾杯!
Pure.Krome、2009

3
@JonSkeetリスト反復子を最適化すると、反復中にリストが変更されたときの動作が変わります。変更された場合は例外を失います。それでも最適化することは可能ですが、変更が発生しないことを確認する必要があります(他のスレッドも含むと思います)。
Craig Gidney

5
@VeeKeyBee:つまり、2004年のマイクロソフトのことです。a)状況は変化しています。b)の仕事はやってしなければならないであろう小さな有意であることが、このために、各繰り返しで作業量を。とにかくforeach配列と同等であることに注意してくださいfor常に読みやすさを優先してコーディングし、その後、測定可能なパフォーマンス上の利点をもたらすという証拠がある場合にのみ、マイクロ最適化を行います。
Jon Skeet、2012年

15

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>クラスでも、インデクサーを持っていないことがわかります。しかし可能であれば、インデクサーは次のように記述する必要があります。

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

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


12

半検証する簡単なテスト。私はただ見るために小さなテストをしました。これがコードです:

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ミリ秒速くなりました- 一貫して。foreachは113-119msでしたが、forは135-139msでした。私は何度か入れ替えて、起動したばかりのプロセスではないことを確認しました。

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

int foo = intList[i];

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

正直なところ、すべての状況でforeachが少し遅くなることを期待していましたが、ほとんどのアプリケーションでは問題になるほどで​​はありませんでした。

編集:Jonsの提案を使用した新しいコードは次のとおりです(System.OutOfMemory例外がスローされる前に、134217728が最大の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

それらを入れ替えて、それが物事の順序を扱っているかどうかを確認します(ほぼ)同じ結果が得られます。


6
DateTime.Nowよりもストップウォッチを使用することをお勧めします。正直に言うと、それほど高速に実行することはできません。
Jon Skeet、

8
'for'は反復ごとに条件を評価するため、foreachループはより高速に実行されます。あなたの例の場合、これは(list.countを取得するための)1つの追加のメソッド呼び出しを行います。要するに、2つの異なるコードをベンチマークしているため、奇妙な結果になります。'int max = intlist.Count;を試してください。for(int i = 0; i <max; i ++)... 'と' for 'ループは常に期待どおりに高速で実行されます!
AR

1
コンパイル後、forとforeachは、プリミティブを操作するときにまったく同じものに最適化されます。List <T>を紹介するまでは、速度が(大幅に)異なります。
アンソニーラッセル

9

注:C#にはインデクサーがないため、この回答はC#よりもJavaに当てはまりLinkedListsますが、一般的な点はまだ保持されていると思います。

場合listと、あなたしている作業があることを起こるLinkedList、インデクサー・コードの性能は(配列形式のアクセス)を使用して、より多くの悪いですIEnumeratorからのforeach大規模なリストのために、。

LinkedListインデクサー構文を使用して要素10.000にアクセスlist[10000]すると、リンクリストはヘッドノードから始まりNext、正しいオブジェクトに到達するまで-ポインターを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いる場合、listではないですLinkedListが、配列は、動作が完全に異なっています。


4
.NETのLinkedListにはインデクサーがないため、実際にはオプションではありません。
Jon Skeet、

ああ、それで問題が解決します。それから:-)私はLinkedList<T>MSDN のドキュメントを調べているだけで、かなりまともなAPIを持っています。最も重要なのは、get(int index)Javaのようにメソッドがないことです。それでも、特定のよりも遅いインデクサーを公開する他のリストのようなデータ構造については、この点が依然として当てはまると思いIEnumeratorます。
Tom Lokhorst、

2

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

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

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


4
配列アクセスとインデクサーアクセスには違いがあることを忘れないでください。リストがList<T>ここにある場合、インデクサーを呼び出すことのヒット(おそらくインライン化)があります。ベアメタルアレイアクセスのようではありません。
Jon Skeet、

ほんとだ!それはまた別のプロパティの実行であり、実装のなすがままです。
Charles Prakash Dasari、

1

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

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

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


0

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

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


0

あなたが提供した例では、foreachループの代わりにforループを使用する方が間違いなく優れています。

ループが展開されていない限り(ステップあたり1.0サイクル)、標準のforeach構成は単純なもの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               
╚══════════════════════╩═══════════╩═══════╩════════════════════════╩═════════════════════╝


4
リンクしたコードプロジェクトの記事を読み直す可能性があります。興味深い記事ですが、投稿の正反対のことが書かれています。また、再作成したテーブルは、配列およびリストに直接、またはそれらのIListインターフェイスを介してアクセスするパフォーマンスを測定しています。どちらも質問とは何の関係もありません。:)
Paul Walls

0

あなたはそれについてDeep .NETで読むことができます-パート1反復

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

たとえば、foreachループを使用した配列反復: ここに画像の説明を入力してください

および-foreachループを使用して反復をリストします。 ここに画像の説明を入力してください

そして最終結果: ここに画像の説明を入力してください

ここに画像の説明を入力してください

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.