9

次の短いが完全なサンプル プログラム

const long iterations = 1000000000;

T[] array = new T[1 << 20];
for (int i = 0; i < array.Length; i++)
{
    array[i] = new T();
}

Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < iterations; i++)
{
    array[i % array.Length].Value0 = i;
}

Console.WriteLine("{0,-15}  {1}   {2:n0} iterations/s",
    typeof(T).Name, sw.Elapsed, iterations * 1000d / sw.ElapsedMilliseconds);

次のタイプにT置き換えられます

class SimpleClass                   struct SimpleStruct
{                                   {
    public int Value0;                  public int Value0;
}                                   }

class ComplexClass                  struct ComplexStruct
{                                   {
    public int Value0;                  public int Value0;
    public int Value1;                  public int Value1;
    public int Value2;                  public int Value2;
    public int Value3;                  public int Value3;
    public int Value4;                  public int Value4;
    public int Value5;                  public int Value5;
    public int Value6;                  public int Value6;
    public int Value7;                  public int Value7;
    public int Value8;                  public int Value8;
    public int Value9;                  public int Value9;
    public int Value10;                 public int Value10;
    public int Value11;                 public int Value11;
}                                   }

私のマシン(Windows 7 .NET 4.5 32ビット)で次の興味深い結果が得られます

SimpleClass 00:00:10.4471717 95,721,260 回/秒
ComplexClass 00:00:37.8199150 26,441,736 回/秒
SimpleStruct 00:00:12.3075100 81,254,571 回/秒
ComplexStruct 00:00:32.6140182 30,661,679 回/秒

質問 1:ComplexClassがよりもずっと遅いのはなぜSimpleClassですか? 経過時間は、クラス内のフィールド数に比例して増加するようです。多くのフィールドを持つクラスの最初のフィールドへの書き込みは、フィールドが 1 つしかないクラスの最初のフィールドへの書き込みとあまり変わらないはずですよね?

質問 2:ComplexStructがよりも遅いのはなぜSimpleStructですか? IL コードを見ると、それが配列にコピーされるiローカル インスタンスではなく、配列に直接書き込まれていることがわかります。ComplexStructそのため、より多くのフィールドをコピーすることによるオーバーヘッドは発生しません。

おまけの質問:ComplexStructがより速いのはなぜComplexClassですか?


編集:より小さな配列でテスト結果を更新しましたT[] array = new T[1 << 8];:

SimpleClass 00:00:13.5091446 74,024,724 回/秒
ComplexClass 00:00:13.2505217 75,471,698 回/秒
SimpleStruct 00:00:14.8397693 67,389,986 回/秒
ComplexStruct 00:00:13.4821834 74,172,971 回/秒

したがって、 と の間に実質的に違いはなく、 と の間SimpleClassComplexClassはわずかな違いしかありSimpleStructませんComplexStruct。ただし、 と のパフォーマンスは大幅に低下しましSimpleClassSimpleStruct


編集:そして今T[] array = new T[1 << 16];

SimpleClass 00:00:09.7477715 102,595,670 回/秒
ComplexClass 00:00:10.1279081 98,745,927 回/秒
SimpleStruct 00:00:12.1539631 82,284,210 回/秒
ComplexStruct 00:00:10.5914174 94,419,790 回/秒

の結果1<<15は のよう1<<8になり、 の結果1<<17は のようになり1<<20ます。

4

4 に答える 4

7

質問 1 に対する考えられる答え:

CPU は一度に 1 ページずつメモリをキャッシュに読み込みます。

データ型が大きいほど、各キャッシュ ページに収まるオブジェクトの数が少なくなります。32 ビット値を 1 つしか書き込んでいない場合でも、CPU キャッシュにそのページが必要です。オブジェクトが小さいほど、次にメイン メモリから読み取る必要がある前に、より多くのループを通過できます。

于 2012-12-14T23:32:12.570 に答える
2

回答 1 : CPU のキャッシュが固定サイズであるため、一度にキャッシュに収まるオブジェクトが少なくなるため、ComplexClass速度が遅くなります。基本的に、メモリからのフェッチに必要な時間が増加しているためです。キャッシュに入ってRAMの速度を下げると、これはより明確になる可能性があります(極端に)。SimpleClassComplexClass

回答 2 : 回答 1 と同じです。

おまけ: 構造体の配列は、構造体の連続ブロックであり、配列ポインターによってのみ参照されます。クラスの配列は、クラス インスタンスへの参照の連続ブロックであり、配列ポインターによって参照されます。クラスはヒープ上に作成されるため (基本的に、空きがある場合はどこでも)、連続した順序付けされた 1 つのブロックにはなりません。これはスペースの最適化には優れていますが、CPU キャッシングには良くありません。その結果、配列を (順番に) 繰り返し処理する場合、大きなクラスへのポインタの大きな配列でより多くの CPU キャッシュ ミスが発生し、構造体の配列の順番での繰り返しで発生します。

なぜSimpleStruct遅いSimpleClassのか:私が理解していることから、構造体にはある程度のオーバーヘッドがあります(約76バイトと言われています)。何が何で、なぜそこにあるのかはわかりませんが、ネイティブ コード (C++ コンパイル) を使用してこの同じテストを実行すると、SimpleStruct配列のパフォーマンスが向上することがわかると思います。それはただの推測です。


いずれにせよ、これは面白そうです。今夜試してみます。私の結果を投稿します。完全なコードを取得することは可能ですか?

于 2012-12-15T00:05:42.697 に答える
2

それを証明する資料はありませんが、地域の問題である可能性があると思います。複雑なクラスはメモリの点でより広いため、カーネルがヒープまたはスタック上の離れたメモリ領域にアクセスするのに時間がかかります。ただし、客観的であるためには、問題がシステムのせいであるためには、測定値の差が非常に大きく聞こえると言わざるを得ません.

クラスと構造体の違いについては、私もこれを文書化することはできませんが、以前と同じ原則で、スタックはヒープ領域よりも頻繁にキャッシュされるため、キャッシュ ミスが少なくなる可能性があります。

最適化を有効にしてプログラムを実行しましたか?

編集:私は小さなテストを行い、パラメーターとしてwithをComplexStruct使用してから、構造体の各フィールドにパラメーターとして with 0 を追加しました。タイムは大幅にカットされており、 さんとほぼ同じくらいだったと思います。私はデバッグモードで、デバッガーをオンにして、最適化なしで実行しました。構造体はフィールドを保持していましたが、メモリ内のサイズが削減され、時間も削減されました。StructLayoutAttributeLayoutKind.ExplicitFieldOffsetAttributeSimpleStruct

于 2012-12-14T23:35:16.540 に答える
1

おそらく消費時間の大部分を占めるモジュラスを削除するためにベンチマークを少し変更しました。intモジュラス演算ではなく、フィールドアクセス時間を比較しているようです。

    const long iterations = 1000;
    GC.Collect();
    GC.WaitForPendingFinalizers();
    //long sMem = GC.GetTotalMemory(true);
    ComplexStruct[] array = new ComplexStruct[1 << 20];
    for (int i = 0; i < array.Length; i++) {
        array[i] = new ComplexStruct();
    }
    //long eMem = GC.GetTotalMemory(true);
    //Console.WriteLine("memDiff=" + (eMem - sMem));
    //Console.WriteLine("mem/elem=" + ((eMem - sMem) / array.Length));
    Stopwatch sw = Stopwatch.StartNew();
    for (int k = 0; k < iterations; k++) {
        for (int i = 0; i < array.Length; i++) {
            array[i].Value0 = i;
        }
    }
    Console.WriteLine("{0,-15}  {1}   {2:n0} iterations/s",
        typeof(ComplexStruct).Name, sw.Elapsed, (iterations * array.Length) * 1000d / sw.ElapsedMilliseconds);

(各テストのタイプを置き換えます)。これらの結果が得られます (数百万回の内部ループ割り当て/秒):

SimpleClass 357.1
SimpleStruct 411.5
ComplexClass 132.9
ComplexStruct 159.1

これらの数値は、Class と Struct のバージョンに関する限り、私が予想していた数値に近いものです。複雑なバージョンの遅い時間は、より大きなオブジェクト/構造体の CPU キャッシュ効果によって説明されると思います。コメントアウトされたメモリ測定コードを使用すると、構造体バージョンが全体的なメモリ消費量が少ないことがわかります。GC.Collect を追加したのは、メモリ測定コードが構造体バージョンとクラス バージョンの相対的な時間に影響することに気付いた後です。

于 2012-12-15T02:06:45.893 に答える