40

次のコードを使用して、.NET配列(32ビットプロセス)のヘッダーのオーバーヘッドを特定しようとしました。

long bytes1 = GC.GetTotalMemory(false);
object[] array = new object[10000];
    for (int i = 0; i < 10000; i++)
        array[i] = new int[1];
long bytes2 = GC.GetTotalMemory(false);
array[0] = null; // ensure no garbage collection before this point

Console.WriteLine(bytes2 - bytes1);
// Calculate array overhead in bytes by subtracting the size of 
// the array elements (40000 for object[10000] and 4 for each 
// array), and dividing by the number of arrays (10001)
Console.WriteLine("Array overhead: {0:0.000}", 
                  ((double)(bytes2 - bytes1) - 40000) / 10001 - 4);
Console.Write("Press any key to continue...");
Console.ReadKey();

結果は

    204800
    Array overhead: 12.478

32ビットプロセスでは、object[1]はint[1]と同じサイズである必要がありますが、実際にはオーバーヘッドが3.28バイト増加して

    237568
    Array overhead: 15.755

誰もが理由を知っていますか?

(ちなみに、興味があれば、上記のループの(object)iなどの非配列オブジェクトのオーバーヘッドは約8バイト(8.384)です。64ビットプロセスでは16バイトだと聞きました。)

4

5 に答える 5

51

これは、同じことを示すための、少しすっきりした(IMO)短いが完全なプログラムです。

using System;

class Test
{
    const int Size = 100000;

    static void Main()
    {
        object[] array = new object[Size];
        long initialMemory = GC.GetTotalMemory(true);
        for (int i = 0; i < Size; i++)
        {
            array[i] = new string[0];
        }
        long finalMemory = GC.GetTotalMemory(true);
        GC.KeepAlive(array);

        long total = finalMemory - initialMemory;

        Console.WriteLine("Size of each element: {0:0.000} bytes",
                          ((double)total) / Size);
    }
}

しかし、同じ結果が得られます。参照型の配列のオーバーヘッドは16バイトですが、値型の配列のオーバーヘッドは12バイトです。CLI仕様の助けを借りて、私はまだそれがなぜであるかを解明しようとしています。参照型の配列は共変であり、関連する可能性があることを忘れないでください...

編集:cordbgの助けを借りて、私はブライアンの答えを確認することができます-参照型配列の型ポインターは、実際の要素型に関係なく同じです。おそらくobject.GetType()、これを説明するために(非仮想です、覚えておいてください)いくつかのファンキーさがあります。

したがって、次のコードを使用します。

object[] x = new object[1];
string[] y = new string[1];
int[] z = new int[1];
z[0] = 0x12345678;
lock(z) {}

最終的には次のようになります。

Variables:
x=(0x1f228c8) <System.Object[]>
y=(0x1f228dc) <System.String[]>
z=(0x1f228f0) <System.Int32[]>

Memory:
0x1f228c4: 00000000 003284dc 00000001 00326d54 00000000 // Data for x
0x1f228d8: 00000000 003284dc 00000001 00329134 00000000 // Data for y
0x1f228ec: 00000000 00d443fc 00000001 12345678 // Data for z

変数自体の値の1ワード前にメモリをダンプしたことに注意してください。

xおよびの場合y、値は次のとおりです。

  • ハッシュコードをロックするために使用される同期ブロック(またはシンロック-ブライアンのコメントを参照)
  • タイプポインタ
  • 配列のサイズ
  • 要素型ポインタ
  • ヌル参照(最初の要素)

の場合z、値は次のとおりです。

  • 同期ブロック
  • タイプポインタ
  • 配列のサイズ
  • 0x12345678(最初の要素)

異なる値型配列(byte []、int []など)は異なる型ポインターになりますが、すべての参照型配列は同じ型ポインターを使用しますが、異なる要素型ポインターを持ちます。要素の型ポインターは、その型のオブジェクトの型ポインターと同じ値です。したがって、上記の実行で文字列オブジェクトのメモリを見ると、タイプポインタは0x00329134になります。

タイプポインタの前の単語は、確かにモニターまたはハッシュコードのいずれかと関係があります。呼び出しGetHashCode()メモリのそのビットにデータを入力します。デフォルトobject.GetHashCode()では、オブジェクトの存続期間中、ハッシュコードの一意性を確保するために同期ブロックが取得されると思います。しかし、lock(x){}何もしなかったのでびっくりしました...

ちなみに、これはすべて「ベクトル」タイプにのみ有効です。CLRでは、「ベクトル」タイプは下限が0の1次元配列です。他の配列はレイアウトが異なります。 、下限を保存する必要があります...

これまでのところ、これは実験的なものですが、ここに当て推量があります。これは、システムが現在のように実装されている理由です。これからは、本当に推測しているだけです。

  • すべてのobject[]アレイは同じJITコードを共有できます。これらは、メモリ割り当て、配列アクセス、Lengthプロパティ、および(重要な)GCの参照のレイアウトに関して同じように動作します。これを値型配列と比較してください。値型配列が異なれば、GCの「フットプリント」も異なります(たとえば、1つはバイトを持ってから参照を持ち、他はまったく参照を持たないなど)。
  • ランタイム内で値を割り当てるたびにobject[]、それが有効であることを確認する必要があります。新しい要素値に参照を使用しているオブジェクトのタイプが、配列の要素タイプと互換性があることを確認する必要があります。例えば:

    object[] x = new object[1];
    object[] y = new string[1];
    x[0] = new object(); // Valid
    y[0] = new object(); // Invalid - will throw an exception
    

これは先に述べた共分散です。これがすべての割り当てで発生することを考えると、間接参照の数を減らすことは理にかなっています。特に、要素の型を取得するために各割り当ての型オブジェクトに移動する必要があるため、キャッシュを壊したくないのではないかと思います。私は、テストが次のようなものであると疑っています(そして、私のx86アセンブリはこれを検証するのに十分ではありません)。

  • コピーされる値はnull参照ですか?もしそうなら、それは大丈夫です。(終わり。)
  • 参照が指すオブジェクトのタイプポインタをフェッチします。
  • その型ポインターは要素型ポインター(単純なバイナリ等価性チェック)と同じですか?もしそうなら、それは大丈夫です。(終わり。)
  • その型ポインタの割り当ては、要素型ポインタと互換性がありますか?(継承とインターフェイスが関係する、はるかに複雑なチェック。)そうであれば、それで問題ありません。それ以外の場合は、例外をスローします。

最初の3つのステップで検索を終了できれば、間接参照はそれほど多くありません。これは、配列の割り当てと同じくらい頻繁に発生することになる場合に適しています。静的に検証可能であるため、値型の割り当てではこれを行う必要はありません。

したがって、参照型の配列は値型の配列よりもわずかに大きいと私は信じています。

素晴らしい質問-それを掘り下げるのは本当に興味深いです:)

于 2009-10-19T16:46:42.227 に答える
24

配列は参照型です。すべての参照型には、2つの追加の単語フィールドがあります。タイプ参照とSyncBlockインデックスフィールド。これらは、とりわけCLRでロックを実装するために使用されます。したがって、参照型の型オーバーヘッドは32ビットで8バイトです。その上、配列自体もさらに4バイトの長さを格納します。これにより、合計オーバーヘッドが12バイトになります。

そして、私はJon Skeetの答えから学びました。参照型の配列には、さらに4バイトのオーバーヘッドがあります。これは、WinDbgを使用して確認できます。追加の単語は、配列に格納されている型の別の型参照であることがわかります。参照型のすべての配列はobject[]、実際の型の型オブジェクトへの追加の参照とともに、として内部に格納されます。したがって、astring[]は、実際にはobject[]、型への追加の型参照を持つだけstringです。詳しくは下記をご覧ください。

配列に格納された値:参照型の配列はオブジェクトへの参照を保持するため、配列の各エントリは参照のサイズです(つまり、32ビットで4バイト)。値型の配列は値をインラインで格納するため、各要素は問題の型のサイズを占めます。

この質問も興味深いかもしれません:C#リスト<double>サイズとdouble[]サイズ

ゴーリーの詳細

次のコードを検討してください

var strings = new string[1];
var ints = new int[1];

strings[0] = "hello world";
ints[0] = 42;

WinDbgをアタッチすると、次のようになります。

まず、値型配列を見てみましょう。

0:000> !dumparray -details 017e2acc 
Name: System.Int32[]
MethodTable: 63b9aa40
EEClass: 6395b4d4
Size: 16(0x10) bytes
Array: Rank 1, Number of elements 1, Type Int32
Element Methodtable: 63b9aaf0
[0] 017e2ad4
    Name: System.Int32
    MethodTable 63b9aaf0
    EEClass: 6395b548
    Size: 12(0xc) bytes
     (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    63b9aaf0  40003f0        0         System.Int32  1 instance       42 m_value <=== Our value

0:000> !objsize 017e2acc 
sizeof(017e2acc) =           16 (        0x10) bytes (System.Int32[])

0:000> dd 017e2acc -0x4
017e2ac8  00000000 63b9aa40 00000001 0000002a <=== That's the value

まず、配列と値42の1つの要素をダンプします。ご覧のとおり、サイズは16バイトです。つまり、int32値自体に4バイト、通常の参照型のオーバーヘッドに8バイト、配列の長さにさらに4バイトです。

rawダンプには、SyncBlock、メソッドテーブルint[]、長さ、および42の値(16進数で2a)が表示されます。SyncBlockがオブジェクト参照の直前にあることに注意してください。

string[]次に、を見て、追加の単語が何に使用されているかを調べましょう。

0:000> !dumparray -details 017e2ab8 
Name: System.String[]
MethodTable: 63b74ed0
EEClass: 6395a8a0
Size: 20(0x14) bytes
Array: Rank 1, Number of elements 1, Type CLASS
Element Methodtable: 63b988a4
[0] 017e2a90
    Name: System.String
    MethodTable: 63b988a4
    EEClass: 6395a498
    Size: 40(0x28) bytes <=== Size of the string
     (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    String:     hello world    
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    63b9aaf0  4000096        4         System.Int32  1 instance       12 m_arrayLength
    63b9aaf0  4000097        8         System.Int32  1 instance       11 m_stringLength
    63b99584  4000098        c          System.Char  1 instance       68 m_firstChar
    63b988a4  4000099       10        System.String  0   shared   static Empty
    >> Domain:Value  00226438:017e1198 <<
    63b994d4  400009a       14        System.Char[]  0   shared   static WhitespaceChars
    >> Domain:Value  00226438:017e1760 <<

0:000> !objsize 017e2ab8 
sizeof(017e2ab8) =           60 (        0x3c) bytes (System.Object[]) <=== Notice the underlying type of the string[]

0:000> dd 017e2ab8 -0x4
017e2ab4  00000000 63b74ed0 00000001 63b988a4 <=== Method table for string
017e2ac4  017e2a90 <=== Address of the string in memory

0:000> !dumpmt 63b988a4
EEClass: 6395a498
Module: 63931000
Name: System.String
mdToken: 02000024  (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
BaseSize: 0x10
ComponentSize: 0x2
Number of IFaces in IFaceMap: 7
Slots in VTable: 196

まず、配列と文字列をダンプします。次に、のサイズをダンプしstring[]ます。WinDbgがタイプをSystem.Object[]ここにリストしていることに注意してください。この場合のオブジェクトサイズには文字列自体が含まれるため、合計サイズは配列の20に文字列の40を加えたものになります。

インスタンスのrawバイトをダンプすると、次のことがわかります。最初にSyncBlockがあり、次にのメソッドテーブルobject[]、次に配列の長さがわかります。その後、文字列のメソッドテーブルへの参照を含む追加の4バイトを見つけます。これは、上記のようにdumpmtコマンドで確認できます。最後に、実際の文字列インスタンスへの単一の参照を見つけます。

結論は

アレイのオーバーヘッドは、次のように分類できます(32ビットの場合)

  • 4バイトのSyncBlock
  • 配列自体のメソッドテーブル(型参照)用に4バイト
  • 配列の長さは4バイト
  • 参照型の配列は、実際の要素型のメソッドテーブルを保持するためにさらに4バイトを追加します(参照型の配列は内部にありますobject[]

つまり、オーバーヘッドは、値型配列の場合は12バイト参照型配列の場合は16バイトです

于 2009-10-19T16:36:57.830 に答える
2

ループ中の(GetTotalMemoryを介した)メモリ割り当ては、配列だけに実際に必要なメモリとは異なる可能性があるため、測定中にいくつかの誤った仮定をしていると思います-メモリはより大きなブロックに割り当てられる可能性があり、他のオブジェクトがループ中に再利用されるメモリなど。

アレイのオーバーヘッドに関する情報は次のとおりです。

于 2009-10-19T16:27:27.133 に答える
1

ヒープ管理(GetTotalMemoryを処理するため)はかなり大きなブロックしか割り当てることができないため、後者はCLRによるプログラマーの目的で小さなチャンクによって割り当てられます。

于 2009-10-19T16:24:07.140 に答える
1

オフトピックで申し訳ありませんが、今日の朝、メモリのオーバーヘッドに関する興味深い情報を見つけました。

大量のデータ(最大2GB)を運用するプロジェクトがあります。主要なストレージとして使用しますDictionary<T,T>。何千もの辞書が実際に作成されています。List<T>キーとList<T>値(自分で実装)に変更した後IDictionary<T,T>、メモリ使用量は約30〜40%減少しました。

なんで?

于 2009-10-19T16:43:27.927 に答える