[C# パフォーマンス] C# 言語での配列の反復

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% 高速であること         がわかります foreachfor

LINQ 実装は、        最新の .NET バージョンで大幅に改善されました。.NET 6 ではかなり遅くなりましたが、.NET 7 ではさらに遅くなり、.NET 8 では大規模な配列の場合ははるかに高速になりました。

三つ、foreachfor

        ループよりも高速にするにはどうすればよいでしょうか? foreachforとループはどちらもループの構文上のシュガーです。これらのコードが配列で使用されると、コンパイラは実際に非常によく似たコードを生成します。forforeachwhile

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% 優れています。foreachfor

5.リンク

.NET 8 より前のバージョンの .NET のソース コードを確認すると、ループが使用されている         ことがわかりますでは、 a を使用した方が a よりも速いのに、この場合はなぜそれほど遅いのでしょうか?Sum()System.Linqforeachforeachfor

        この実装は タイプの拡張メソッドです。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>から。次に列挙子を解放する必要があるため、このメソッドはインライン化できません。IDisposabletry/finally
  • ペアを反復処理するには、メソッドとプロパティを呼び出す必要があります。列挙子は参照型であるため、これらの呼び出しは仮想的です。IEnumerable<T>MoveNext()Current

        これらすべてにより、配列の列挙が大幅に遅くなります。

注:これら 2 つのタイプの列挙子のパフォーマンスの違いについては、私の他の投稿「値型列挙子のパフォーマンスと参照型列挙子のパフォーマンス」を参照してください。

.NET 8 のパフォーマンスは、ソースが配列 or の 場合に合計を実行する ため、はるかに優れています。ソースが配列またはor のリストである場合 、 SIMD を使用してさらに最適化が行われ、複数の項目を同時に合計できるようになります。時間。Sum()List<T>intlong

 注: SIMD の仕組みとコードでの使用方法については、私の他の記事「.NET の単一命令複数データ (SIMD)」を参照してください。

6. 結論

        配列の反復は、コンパイラーが実行できるコード最適化の特殊なケースです。を使用すると、これらの最適化に最適な条件が保証されます。foreach

        配列を に変換すると、反復処理が大幅に遅くなります。IEnumerable<T>

        すべての LINQ メソッドが配列の場合に最適化されているわけではありません。.NET 8 より前では、 Sum()メソッド のカスタム実装を使用する方が適切でした 。

おすすめ

転載: blog.csdn.net/gongdiwudu/article/details/131902025