16

以下のプログラム (デバッガーを接続せずにリリース モードで実行) に非常に興味があります。最初のループは、配列の各要素に新しいオブジェクトを割り当て、実行に約 1 秒かかります。

そこで、オブジェクトの作成と代入のどの部分に最も時間がかかっているのだろうかと考えていました。そこで、オブジェクトの作成に必要な時間をテストするために 2 番目のループを作成し、割り当て時間をテストするために 3 番目のループを作成しました。どちらもわずか数ミリ秒で実行されます。どうしたの?

static class Program
{
    const int Count = 10000000;

    static void Main()
    {
        var objects = new object[Count];
        var sw = new Stopwatch();
        sw.Restart();
        for (var i = 0; i < Count; i++)
        {
            objects[i] = new object();
        }
        sw.Stop();
        Console.WriteLine(sw.ElapsedMilliseconds); // ~800 ms
        sw.Restart();
        object o = null;
        for (var i = 0; i < Count; i++)
        {
            o = new object();
        }
        sw.Stop();
        Console.WriteLine(sw.ElapsedMilliseconds); // ~ 40 ms
        sw.Restart();
        for (var i = 0; i < Count; i++)
        {
            objects[i] = o;
        }
        sw.Stop();
        Console.WriteLine(sw.ElapsedMilliseconds); // ~ 50 ms
    }
}
4

2 に答える 2

16

85,000 バイト未満の RAM を占有し、配列ではないオブジェクトdoubleが作成されると、ジェネレーション ゼロ ヒープと呼ばれるメモリ領域に配置されます。Gen0 ヒープが特定のサイズに成長するたびに、システムがライブ参照を見つけることができる Gen0 ヒープ内のすべてのオブジェクトが Gen1 ヒープにコピーされます。その後、Gen0 ヒープは一括消去されるため、新しいオブジェクトを追加する余地があります。Gen1 ヒープが特定のサイズに達すると、参照が存在するすべてのものが Gen2 ヒープにコピーされ、Gen0 ヒープを一括消去できます。

多くのオブジェクトが作成され、すぐに破棄されると、Gen0 ヒープが繰り返しいっぱいになりますが、Gen0 ヒープから Gen1 ヒープにコピーする必要があるオブジェクトはほとんどありません。その結果、Gen1 ヒープは、あったとしても、非常にゆっくりといっぱいになります。対照的に、Gen0 ヒープがいっぱいになったときに Gen0 ヒープ内のほとんどのオブジェクトがまだ参照されている場合、システムはそれらのオブジェクトを Gen1 ヒープにコピーする必要があります。これにより、システムはこれらのオブジェクトのコピーに時間を費やさなければならなくなり、Gen1 ヒープがいっぱいになり、ライブ オブジェクトをスキャンする必要が生じ、そこからのすべてのライブ オブジェクトを Gen2 ヒープに再度コピーする必要があります。 . これにはさらに時間がかかります。

最初のテストを遅らせるもう 1 つの問題は、すべてのライブ Gen0 オブジェクトを識別しようとすると、最後の Gen0 コレクション以降に触れられていない場合にのみ、システムが Gen1 または Gen2 オブジェクトを無視できることです。最初のループの間、objects配列は絶えず触れられます。したがって、すべての Gen0 コレクションは、その処理に時間を費やさなければなりません。2 番目のループではまったく触れられないため、同じ数の Gen0 コレクションがあっても、実行にそれほど時間はかかりません。3 番目のループでは、配列は常に処理されますが、新しいヒープ オブジェクトは作成されないため、ガベージ コレクションのサイクルは必要なく、どれだけ時間がかかるかは問題ではありません。

各パスでオブジェクトを作成して放棄するが、既存のオブジェクトへの参照を配列スロットに格納する 4 番目のループを追加する場合、2 番目の合計時間よりも長くかかると予想されます。同じ操作を実行しているにもかかわらず、3 番目のループ。新しく作成されたオブジェクトのほとんどが Gen0 ヒープからコピーされる必要があるため、おそらく最初のループほどではありませんが、どのオブジェクトがまだ生きているかを判断するために余分な作業が必要なため、2 番目よりも長くなります。さらに詳しく調べたい場合は、ネストされたループで 5 番目のテストを行うと面白いかもしれません。

for (int ii=0; ii<1024; ii++)
  for (int i=ii; i<Count; i+=1024)
     ..

正確な詳細はわかりませんが、.NET は、大きな配列全体をスキャンする必要がないようにしようとします。その配列の一部のみが変更されている場合は、それらをチャンクに分割します。大きな配列のチャンクが操作された場合、そのチャンク内のすべての参照をスキャンする必要がありますが、最後の Gen0 コレクション以降に操作されていないチャンクに保存されている参照は無視される場合があります。上記のようにループを分割すると、.NET が Gen0 コレクション間で配列内のほとんどのチャンクにアクセスすることになり、最初のループよりも時間が遅くなる可能性があります。

于 2013-08-10T19:27:09.837 に答える
14
  1. 1,000 万個のオブジェクトを作成し、それらをメモリ内の別々の場所に保存します。ここでメモリ消費量が最大になります。
  2. 1,000 万個のオブジェクトを作成しますが、それらはどこにも保存されず、 破棄されるだけです。
  3. 1 つのオブジェクトを作成し、それに対して1,000 万回の参照を行い、メモリ消費を最小限に抑えます。

そして、はい、以下のパフォーマンス分析は、10,000 個のオブジェクトのみを対象としています (1,000 個では時間がかかりすぎます)。

10,000 オブジェクトのみのパフォーマンス

更新:この図は、最初のケースでのメモリ割り当ての CPU 作業を示しています。JIT_New@@...関数が CPU 時間の 80.5% を占めていることに注意してください。

CPU性能ケース1

UPDATE2: CaseTwoの完全性のための CPU 時間。

CPU性能ケース2

UPDATE3:完全を期すために、3番目のケース

CPU性能ケース3

于 2013-08-10T18:50:48.853 に答える