私が.NETを学び始めたとき、ある人は、foreachが正当化せずに、foreachの2倍の速度で動作することを教えてくれました。 今、誰かの言葉が私にとって十分でないとき、私はこの記事を書くことにしました。
この記事では、サイクルのパフォーマンスを調べ、いくつかのニュアンスを明確にします。
さあ、行こう!
次のコードを検討してください。
foreach (var item in Enumerable.Range(0, 128)) { Console.WriteLine(item); }
foreachループは構文糖衣であり、この場合、コンパイラーはそれを次のコードにデプロイします。
IEnumerator<int> enumerator = Enumerable.Range(0, 128).GetEnumerator(); try { while (enumerator.MoveNext()) { int item = enumerator.Current; Console.WriteLine(item); } } finally { if (enumerator != null) { enumerator.Dispose(); } }
これを知っていれば、foreachがfor-aの方が遅い理由を簡単に推測できます。 foreachを使用する場合:
- 新しいオブジェクトが作成されます-イテレータ;
- 各反復で、MoveNextメソッドが呼び出されます。
- 各反復で、Currentプロパティにアクセスします。これはメソッド呼び出しに相当します。
以上です! いいえ、すべてがそれほど単純ではありませんが...
もう1つのニュアンスがあります! 幸いなことに、または残念なことに、C#/ CLRには多数の最適化があります。 幸いなことに、開発者はコードを知っている必要があるため、コードが高速であるため、残念ながら(それでも幸運にも、さらにそれを考慮しています)。
たとえば、配列はCLRに高度に統合されたタイプであるため、多数の最適化があります。 それらの1つはforeachループに関するものです。
したがって、反復可能なエンティティは、foreachループのパフォーマンスの重要な側面です。これは、反復可能なエンティティは、それに応じてさまざまな方法で展開できるためです。
この記事では、配列とリストの繰り返しを検討します。 for-aとforeach-aに加えて、静的メソッドArray.ForEachとList.ForEachのインスタンスを使用した反復も検討します。
試験方法
static double ArrayForWithoutOptimization(int[] array) { int sum = 0; var watch = Stopwatch.StartNew(); for (int i = 0; i < array.Length; i++) sum += array[i]; watch.Stop(); return watch.Elapsed.TotalMilliseconds; } static double ArrayForWithOptimization(int[] array) { int length = array.Length; int sum = 0; var watch = Stopwatch.StartNew(); for (int i = 0; i < length; i++) sum += array[i]; watch.Stop(); return watch.Elapsed.TotalMilliseconds; } static double ArrayForeach(int[] array) { int sum = 0; var watch = Stopwatch.StartNew(); foreach (var item in array) sum += item; watch.Stop(); return watch.Elapsed.TotalMilliseconds; } static double ArrayForEach(int[] array) { int sum = 0; var watch = Stopwatch.StartNew(); Array.ForEach(array, i => { sum += i; }); watch.Stop(); return watch.Elapsed.TotalMilliseconds; }
テストは、リリースコードに最適化されたフラグを使用して実行されました。 配列の要素数とリストは100,000,000ですテストマシンのボードには、Intel Core i-5プロセッサと8 GBのRAMが搭載されています。
配列
この図は、for / foreachが配列に対して同じ時間動作することを示しています。 これは、foreachループをforで展開し、配列の長さを最大反復境界として使用する、まさに最適化の作業です。 ちなみに、for-aで反復する場合、長さをキャッシュするかどうかは関係ありません。結果はほぼ同じです。
どんなに奇妙かもしれませんが、配列を使用する場合、長さのキャッシュはマイナスの影響を与える可能性があります。 実際、JITはarray.Lengthをループ内の反復境界として認識すると、ループ内の必要な境界に到達するためのインデックスチェックを実行し、それにより1回だけチェックします。 この最適化は非常に簡単に破棄でき、変数をキャッシュする場合は最適化されなくなりました。
Array.ForEachメソッドは、最悪の結果を示しました。 その実装は非常に簡単に見えます:
public static void ForEach<T>(T[] array, Action<T> action) { for (int index = 0; index < array.Length; ++index) action(array[index]); }
次に、なぜそれがとても遅いのですか? 結局のところ、彼は舞台裏で、いつものように使っています。 アクションデリゲートを呼び出すことがすべてです。 実際、メソッドは各反復で呼び出され、これが余分なオーバーヘッドであることを知っています。 さらに、ご存じのとおり、デリゲートは必要な速さで呼び出されないため、結果が得られます。
配列を使用すると、すべてが明確になりました。 リストに移動します。
リストについては、同様の比較方法を使用します。
リスト
これは完全に異なる結果です!!!
リストを反復処理する場合、for / foreachループは異なる結果をもたらします。 実際のところ、ここでは最適化が行われておらず、foreachはイテレーターを作成し、それをさらに操作することで方向を変えています。
リストの長さをキャッシュするための予想どおりの最良の結果が示されました。 リストのインデックス付けは、JITがインライン化するため、パフォーマンスに影響を与えません(インデックス付けはパラメーターを持つ一般的なプロパティです)。
foreachはforの約2.5倍遅い結果を示しました。 これは、背後で動作する複雑な構造によるものです(MoveNext、Currentを呼び出します)。
List.ForEachとArray.ForEachは、最悪の結果を示しました。 実際、JITはインライン仮想メソッドではなく、ご存知のようにデリゲートは常に仮想的に呼び出されます。
このメソッドの実装は次のようになります。
public void ForEach(Action<T> action) { int num = this._version; for (int index = 0; index < this._size && num == this._version; ++index) action(this._items[index]); if (num == this._version) return; ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion); }
ここでは、各反復で、アクションデリゲートが呼び出されます。 また、反復中にリストが変更されたかどうかも毎回チェックされ、変更された場合は例外がスローされます。
興味深い事実は、配列の場合、ForEachメソッドは静的ですが、リストの場合はインスタンスです。 これは生産性を向上させるために行われると思います。 ご存知のように、リストは通常の配列に基づいているため、ForEachメソッドはインデックスによって配列を参照します。これは、インデクサーを介してインデックスにアクセスするよりも数倍高速です。
配列とリスト
特定の番号
- forループ(長さのキャッシュなし)およびforeachの配列は、長さのキャッシュがあるcの場合よりも少し速く動作します。
- Array.ForEachループは、for / foreachループよりも約6倍遅いです。
- リストの(長さのキャッシュなし)は、配列の約3倍遅くなります。
- (長さのキャッシュを使用した)リストの場合、配列の場合よりも約2倍遅くなります。
- リスト上のforeachは、配列上のforeachよりも約6倍遅くなります。
入賞者
配列の中:
リストの中には:
結論として
あなたにとってどのようにしたらいいのかわかりませんが、私にとってこの記事はとても興味深いものでした。 特にそれを書くプロセス。 配列上のforeachは、長さキャッシングよりも高速であることがわかりました。 最適化のためのJITに感謝します。 リスト上のforeachは、事実よりも遅いです。
今日のコンピューターでは、for-andループよりも遅いため、foreachループを使用しないでください。 結局、それを使用するコードはより宣言的に見えます。 もちろん、コードを非常にうまく最適化する必要がある場合は、おそらくforを優先する方が良いでしょう。
読んでくれてありがとう。