1. 説明
反復可能性は配列などの演算の基礎です。C++ プログラム開発のプロセスでは、反復可能演算は非常に一般的かつ広範に行われます。ただし、この演算についてどれだけ知っているか、どれだけ知らないかは、開発の柔軟性に影響します。開発の様子。したがって、この記事では、使用する際の参考として、そのようなアプリケーションをすべて体系的にリストするだけです。
2. 簡単な例から始めます
配列内の項目の合計を実装するのは非常に簡単です。ほとんどの開発者は次のように実装していると思います。
static int Sum(int[] array)
{
var sum = 0;
for (var index = 0; index < array.Length; index++)
sum += array[index];
return sum;
}
実際には、C# にはもっと簡単な代替手段があります。
static int Sum(int[] array)
{
var sum = 0;
foreach (var item in array)
sum += item;
return sum;
}
もう 1 つの方法は、 LINQ によって提供される操作 を使用することです 。配列を含むあらゆる列挙型に適用できます。Sum()
では、これら 3 つはパフォーマンスの点でどのように均衡するのでしょうか?
このベンチマークは、.NET 10、1、および 000 上の配列サイズ 6 と 7.8 のパフォーマンスを比較します。int
ループを使用する方が、ループを使用するよりも約 30% 高速であること がわかります。 foreach
for
LINQ 実装は、 最新の .NET バージョンで大幅に改善されました。.NET 6 ではかなり遅くなりましたが、.NET 7 ではさらに遅くなり、.NET 8 では大規模な配列の場合ははるかに高速になりました。
三つ、foreach
for
ループよりも高速にするにはどうすればよいでしょうか? foreachfor
とループはどちらもループの構文上のシュガーです。これらのコードが配列で使用されると、コンパイラは実際に非常によく似たコードを生成します。for
foreach
while
SharpLabで次のコードを確認 できます。
var array = new[] {0, 1, 2, 3, 4, 5 };
Console.WriteLine(Sum_For());
Console.WriteLine(Sum_ForEach());
int Sum_For()
{
var sum = 0;
for (var index = 0; index < array.Length; index++)
sum += array[index];
return sum;
}
int Sum_ForEach()
{
var sum = 0;
foreach (var item in array)
sum += item;
return sum;
}
コンパイラは次のものを生成します。
[CompilerGenerated]
private static int <<Main>$>g__Sum_For|0_0(ref <>c__DisplayClass0_0 P_0)
{
int num = 0;
int num2 = 0;
while (num2 < P_0.array.Length)
{
num += P_0.array[num2];
num2++;
}
return num;
}
[CompilerGenerated]
private static int <<Main>$>g__Sum_ForEach|0_1(ref <>c__DisplayClass0_0 P_0)
{
int num = 0;
int[] array = P_0.array; // copy array reference
int num2 = 0;
while (num2 < array.Length)
{
int num3 = array[num2];
num += num3;
num2++;
}
return num;
}
コードは非常に似ていますが、配列への参照がローカル変数として追加されていることに注意してください。これにより、JIT コンパイラーが境界チェックを削除できるようになり、反復が高速化されます。生成されたアセンブリの違いを確認します: foreach
Program.<<Main>$>g__Sum_For|0_0(<>c__DisplayClass0_0 ByRef)
L0000: sub rsp, 0x28
L0004: xor eax, eax
L0006: xor edx, edx
L0008: mov rcx, [rcx]
L000b: cmp dword ptr [rcx+8], 0
L000f: jle short L0038
L0011: nop [rax]
L0018: nop [rax+rax]
L0020: mov r8, rcx
L0023: cmp edx, [r8+8]
L0027: jae short L003d
L0029: mov r9d, edx
L002c: add eax, [r8+r9*4+0x10]
L0031: inc edx
L0033: cmp [rcx+8], edx
L0036: jg short L0020
L0038: add rsp, 0x28
L003c: ret
L003d: call 0x000002e975d100fc
L0042: int3
Program.<<Main>$>g__Sum_ForEach|0_1(<>c__DisplayClass0_0 ByRef)
L0000: xor eax, eax
L0002: mov rdx, [rcx]
L0005: xor ecx, ecx
L0007: mov r8d, [rdx+8]
L000b: test r8d, r8d
L000e: jle short L001f
L0010: mov r9d, ecx
L0013: add eax, [rdx+r9*4+0x10]
L0018: inc ecx
L001a: cmp r8d, ecx
L001d: jg short L0010
L001f: ret
これにより、ベンチマークのパフォーマンスが向上します。
SharpLab では、 配列はすでにローカル変数であり、コピーは作成されないことに注意してください。この場合のパフォーマンスは同等です。
4 番目に、配列をスライスします
場合によっては、配列の一部のみを反復処理したい場合があります。繰り返しになりますが、ほとんどの開発者は以下を実装すると思います。
static int Sum(int[] source, int start, int length)
{
var sum = 0;
for (var index = start; index < start + length; index++)
sum += source[index];
return sum;
}
これは、 Span.Slice() メソッド を使用して 簡単にforeachに変換できます。
static int Sum(int[] source, int start, int length)
=> Sum(source.AsSpan().Slice(start, length));
static int Sum(ReadOnlySpan<int> source)
{
var sum = 0;
foreach (var item in source)
sum += item;
return sum;
}
それで、これらのショーのパフォーマンスはどうですか?
また、配列の一部に対してループを使用するよりも約 20% 優れています。foreach
for
5.リンク
.NET 8 より前のバージョンの .NET のソース コードを確認すると、ループが使用されている ことがわかります。では、 a を使用した方が a よりも速いのに、この場合はなぜそれほど遅いのでしょうか?Sum()
System.Linq
foreach
foreach
for
この実装は タイプの拡張メソッドです。AND 演算とは異なり、ソースが配列内にある場合に特別なケースはありません。コンパイラはこの実装を次のように変換します。Sum()
IEnumerable<int>
Count()
Where()
Sum()
static int Sum(this IEnumerable<int> source)
{
var sum = 0;
IEnumerator<int> enumerator = source.GetEnumerator();
try
{
while(enumerator.MoveNext())
sum += enumerator.Current;
}
finally
{
enumerator?.Dispose()
}
return sum;
}
このコードにはパフォーマンス上の問題がいくつかあります。
GetEnumerator()
戻る。これは、列挙子が参照型であることを意味します。つまり、列挙子をヒープ上に割り当てる必要があり、ガベージ コレクターの負荷が増加します。IEnumerator<T>
IEnumerator<T>
から。次に列挙子を解放する必要があるため、このメソッドはインライン化できません。IDisposable
try/finally
- ペアを反復処理するには、メソッドとプロパティを呼び出す必要があります。列挙子は参照型であるため、これらの呼び出しは仮想的です。
IEnumerable<T>
MoveNext()
Current
これらすべてにより、配列の列挙が大幅に遅くなります。
注:これら 2 つのタイプの列挙子のパフォーマンスの違いについては、私の他の投稿「値型列挙子のパフォーマンスと参照型列挙子のパフォーマンス」を参照してください。
.NET 8 のパフォーマンスは、ソースが配列 or の 場合に合計を実行する ため、はるかに優れています。ソースが配列またはor のリストである場合 、 SIMD を使用してさらに最適化が行われ、複数の項目を同時に合計できるようになります。時間。Sum()
List<T>
int
long
注: SIMD の仕組みとコードでの使用方法については、私の他の記事「.NET の単一命令複数データ (SIMD)」を参照してください。
6. 結論
配列の反復は、コンパイラーが実行できるコード最適化の特殊なケースです。を使用すると、これらの最適化に最適な条件が保証されます。foreach
配列を に変換すると、反復処理が大幅に遅くなります。IEnumerable<T>
すべての LINQ メソッドが配列の場合に最適化されているわけではありません。.NET 8 より前では、 Sum()メソッド のカスタム実装を使用する方が適切でした 。