FOREACHとFORの優先順位の違いに関する議論は新しいものではありません。 FOREACHの方が遅いことは誰もが知っていますが、その理由を知っているわけではありません。
私が.NETを学び始めたとき、ある人はFOREACHはFORより2倍遅いと私に言いました。彼は理由もなくこれを言った。当たり前だと思った。
最終的に、FOREACHとFORループのパフォーマンスの違いを調査し、ニュアンスについて説明するためにこの記事を書くことにしました。
次のコードを見てみましょう:
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よりも遅い理由を推測できます:
- 新しいオブジェクトが作成されています。それはクリエーターと呼ばれています。
- MoveNextメソッドは反復ごとに呼び出されます。
- 各反復はCurrentプロパティにアクセスします。
それでおしまい!ただし、思ったほど簡単ではありません。
幸いなことに(または残念なことに)、C#/CLRは実行時に最適化を実行する場合があります。長所は、コードがより高速に動作することです。短所–開発者はこれらの最適化に注意する必要があります。
アレイはCLRに深く統合されたタイプであり、CLRはこのタイプに対して多くの最適化を提供します。 FOREACHループは反復可能なエンティティであり、パフォーマンスの重要な側面です。この記事の後半では、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;
}
テスト条件:
- 「コードの最適化」オプションがオンになっています。
- 要素の数は100000000に等しい(配列とリストの両方)。
- PC仕様:IntelCorei-5および8GBのRAM。
配列
この図は、FORとFOREACHが配列を反復処理するときに同じ時間を費やしていることを示しています。これは、CLR最適化がFOREACHをFORに変換し、配列の長さを最大反復境界として使用するためです。配列の長さがキャッシュされているかどうか(FORを使用している場合)は関係ありません。結果はほぼ同じです。
奇妙に聞こえるかもしれませんが、配列の長さをキャッシュするとパフォーマンスに影響する可能性があります。 配列を使用している間 反復境界としての長さ。JITは、サイクルを超えて右の境界にヒットするようにインデックスをテストします。このチェックは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を使用します。その理由は、ACTIONデリゲートを呼び出すことです。実際、反復ごとにメソッドが呼び出されるため、パフォーマンスが低下します。さらに、デリゲートは私たちが望むほど速く呼び出されません。
リスト
結果は完全に異なります。リストを反復処理すると、FORとFOREACHは異なる結果を示します。最適化はありません。 FOR(リストの長さをキャッシュする)は最良の結果を示しますが、FOREACHは2倍以上遅くなります。これは、MoveNextとCurrentを内部で処理するためです。 List.ForEachとArray.ForEachは、最悪の結果を示しています。代理人は常に仮想的に呼び出されます。このメソッドの実装は次のようになります:
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);
}
各反復はアクションデリゲートを呼び出します。また、リストが変更されているかどうかを確認し、変更されている場合は例外がスローされます。
Listは内部的に配列ベースのモデルを使用し、ForEachメソッドは配列インデックスを使用して反復処理します。これは、インデクサーを使用するよりも大幅に高速です。
特定の番号
- 長さキャッシュのないFORループとFOREACHは、長さキャッシュのあるFORよりも配列でわずかに高速に動作します。
- 配列。Foreach パフォーマンス FOR/FOREACHのパフォーマンスよりも約6倍遅くなります。
- 長さキャッシュのないFORループは、配列と比較して、リストで3倍遅く動作します。
- 長さキャッシュを使用したFORループは、配列と比較して、リストでの動作が2倍遅くなります。
- FOREACHループは、配列と比較して、リストで6倍遅く動作します。
リストのリーダーボードは次のとおりです。
アレイの場合:
結論
私はこの調査、特に執筆プロセスを本当に楽しんだし、あなたもそれを楽しんだことを願っている。結局のところ、FOREACHは、長さを追跡するFORよりも配列の方が高速です。リスト構造では、FOREACHはFORよりも低速です。
FOREACHを使用するとコードの見栄えが良くなり、最新のプロセッサでは使用できます。ただし、コードベースを高度に最適化する必要がある場合は、FORを使用することをお勧めします。
FORまたはFOREACHのどちらのループがより高速に実行されると思いますか?