web-dev-qa-db-ja.com

構造体の配置が、フィールドタイプがプリミティブかユーザー定義かによって異なるのはなぜですか?

Noda Time v2では、ナノ秒の解像度に移行しています。つまり、関心のある時間範囲全体を表すために8バイトの整数を使用できなくなったことを意味します。そのため、野田タイムの(多くの)構造体のメモリ使用量を調査するようになりました。 CLRの配置決定のわずかな奇妙さを明らかにするため。

まず、このisは実装の決定であり、デフォルトの動作はいつでも変更できることを認識しています。 _[StructLayout]__[FieldOffset]_ を使用してcanを変更することに気付きましたが、可能であれば、それを必要としないソリューションを作成します。

私のコアシナリオは、参照型フィールドと他の2つの値型フィールドを含むstructを持っていることです。これらのフィールドはintの単純なラッパーです。 64ビットCLRでは16バイト(参照用に8、他の各用に4)として表される希望がありましたが、何らかの理由で24バイトを使用しています。ちなみに、配列を使用してスペースを測定しています-レイアウトは状況によって異なる可能性があることを理解していますが、これは合理的な出発点のように感じました。

問題を示すサンプルプログラムを次に示します。

_using System;
using System.Runtime.InteropServices;

#pragma warning disable 0169

struct Int32Wrapper
{
    int x;
}

struct TwoInt32s
{
    int x, y;
}

struct TwoInt32Wrappers
{
    Int32Wrapper x, y;
}

struct RefAndTwoInt32s
{
    string text;
    int x, y;
}

struct RefAndTwoInt32Wrappers
{
    string text;
    Int32Wrapper x, y;
}    

class Test
{
    static void Main()
    {
        Console.WriteLine("Environment: CLR {0} on {1} ({2})",
            Environment.Version,
            Environment.OSVersion,
            Environment.Is64BitProcess ? "64 bit" : "32 bit");
        ShowSize<Int32Wrapper>();
        ShowSize<TwoInt32s>();
        ShowSize<TwoInt32Wrappers>();
        ShowSize<RefAndTwoInt32s>();
        ShowSize<RefAndTwoInt32Wrappers>();
    }

    static void ShowSize<T>()
    {
        long before = GC.GetTotalMemory(true);
        T[] array = new T[100000];
        long after  = GC.GetTotalMemory(true);        
        Console.WriteLine("{0}: {1}", typeof(T),
                          (after - before) / array.Length);
    }
}
_

そして、私のラップトップでのコンパイルと出力:

_c:\Users\Jon\Test>csc /debug- /o+ ShowMemory.cs
Microsoft (R) Visual C# Compiler version 12.0.30501.0
for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.


c:\Users\Jon\Test>ShowMemory.exe
Environment: CLR 4.0.30319.34014 on Microsoft Windows NT 6.2.9200.0 (64 bit)
Int32Wrapper: 4
TwoInt32s: 8
TwoInt32Wrappers: 8
RefAndTwoInt32s: 16
RefAndTwoInt32Wrappers: 24
_

そう:

  • 参照型フィールドがない場合、CLRは_Int32Wrapper_フィールドを一緒にパックします(_TwoInt32Wrappers_のサイズは8です)
  • 参照型フィールドを使用しても、CLRはintフィールドをまとめてパックします(_RefAndTwoInt32s_のサイズは16です)
  • 2つを組み合わせると、各_Int32Wrapper_フィールドは8バイトにパディング/アラインされているように見えます。 (_RefAndTwoInt32Wrappers_のサイズは24です。)
  • デバッガで同じコードを実行すると(ただし、リリースビルドのまま)、サイズは12になります。

他のいくつかの実験でも同様の結果が得られています。

  • 値型フィールドの後に参照型フィールドを配置しても役に立ちません
  • objectの代わりにstringを使用しても効果はありません(「任意の参照型」だと思います)
  • 参照の「ラッパー」として別の構造体を使用しても役に立ちません
  • ジェネリック構造体を参照のラッパーとして使用しても役に立ちません
  • フィールドを(簡単にするためにペアで)追加し続けた場合、intフィールドは4バイトとしてカウントされ、_Int32Wrapper_フィールドは8バイトとしてカウントされます
  • [StructLayout(LayoutKind.Sequential, Pack = 4)]をすべての構造物に追加しても結果は変わりません

誰かがこれについて説明していますか(理想的には参照ドキュメントを使用して)、またはフィールドを定数フィールドオフセットを指定してパックする_なしが欲しいというCLRへのヒントを得る方法の提案がありますか?

121
Jon Skeet

これはバグだと思います。自動レイアウトの副作用が見られます。64ビットモードでは、8バイトの倍数のアドレスに重要なフィールドを配置するのが好きです。 [StructLayout(LayoutKind.Sequential)]属性を明示的に適用した場合でも発生します。それは起こるはずがありません。

Structメンバをパブリックにし、次のようなテストコードを追加することで確認できます。

_    var test = new RefAndTwoInt32Wrappers();
    test.text = "adsf";
    test.x.x = 0x11111111;
    test.y.x = 0x22222222;
    Console.ReadLine();      // <=== Breakpoint here
_

ブレークポイントに到達したら、Debug + Windows + Memory + Memory 1を使用します。4バイト整数に切り替えて、アドレスフィールドに_&test_を入力します。

_ 0x000000E928B5DE98  0ed750e0 000000e9 11111111 00000000 22222222 00000000 
_

_0xe90ed750e0_は私のマシン上の文字列ポインターです(あなたのものではありません)。 _Int32Wrappers_を簡単に確認でき、余分な4バイトのパディングによってサイズが24バイトになりました。構造体に戻り、文字列を最後に配置します。繰り返します。文字列ポインタがstillであることがわかります。 _LayoutKind.Sequential_に違反すると、_LayoutKind.Auto_になります。

Microsoftにこれを修正するよう説得するのは難しいでしょう。この方法はあまりにも長い間働いているので、どんな変更でも壊してしまいますsomething。 CLRは、マネージバージョンのstructに対して_[StructLayout]_を尊重し、それをblittableにしようとするだけで、一般的にはすぐにgivesめます。悪名高いDateTimeを含む構造体。構造体をマーシャリングするときにのみ、真のLayoutKind保証が得られます。 Marshal.SizeOf()からわかるように、マーシャリングされたバージョンは確かに16バイトです。

_LayoutKind.Explicit_を使用すると、聞きたいものではなく、修正されます。

85
Hans Passant

EDIT2

struct RefAndTwoInt32Wrappers
{
    public int x;
    public string s;
}

このコードは8バイトで整列されるため、構造体は16バイトになります。これと比較して:

struct RefAndTwoInt32Wrappers
{
    public int x,y;
    public string s;
}

4バイトで整列されるため、この構造体も16バイトになります。したがって、ここでの理論的根拠は、CLRの構造体の整列は、最も整列されたフィールドの数によって決定されるということです。

すべてを組み合わせて構造体を作成すると、次のようになります。

struct RefAndTwoInt32Wrappers
{
    public int x,y;
    public Int32Wrapper z;
    public string s;
}

24バイトの{x、y}にはそれぞれ4バイトがあり、{z、s}には8バイトがあります。構造体にref型を導入すると、CLRは常にカスタム構造体をクラスの配置に合わせて配置します。

struct RefAndTwoInt32Wrappers
{
    public Int32Wrapper z;
    public long l;
    public int x,y;  
}

Int32Wrapperはlongと同じように整列されるため、このコードは24バイトになります。したがって、カスタム構造ラッパーは常に、構造内の最高/最高の位置合わせされたフィールド、または内部の最も重要なフィールドに位置合わせされます。したがって、8バイトにアライメントされたref文字列の場合、struct wrapperはそれに合わせます。

構造体内のカスタム構造体フィールドを終了すると、常に構造体の最上位に位置合わせされたインスタンスフィールドに位置合わせされます。今、これがバグかどうかわからないが、何らかの証拠がなければ、これは意識的な決定であるかもしれないという私の意見に固執するつもりです。


[〜#〜] edit [〜#〜]

サイズは実際にはヒープに割り当てられた場合にのみ正確ですが、構造体自体のサイズは小さくなります(フィールドの正確なサイズ)。これはCLRコードのバグかもしれませんが、証拠によってバックアップする必要があることを示唆するさらなる分析の継ぎ目です。

有用なものが見つかった場合は、CLIコードを検査し、さらなる更新を投稿します。


これは、.NET memアロケーターで使用されるアライメント戦略です。

public static RefAndTwoInt32s[] test = new RefAndTwoInt32s[1];

static void Main()
{
    test[0].text = "a";
    test[0].x = 1;
    test[0].x = 1;

    Console.ReadKey();
}

X64で.net40でコンパイルされたこのコードでは、WinDbgで次のことができます。

最初にヒープ上のタイプを見つけましょう:

    0:004> !dumpheap -type Ref
       Address               MT     Size
0000000003e72c78 000007fe61e8fb58       56    
0000000003e72d08 000007fe039d3b78       40    

Statistics:
              MT    Count    TotalSize Class Name
000007fe039d3b78        1           40 RefAndTwoInt32s[]
000007fe61e8fb58        1           56 System.Reflection.RuntimeAssembly
Total 2 objects

取得したら、そのアドレスの下にあるものを確認できます。

    0:004> !do 0000000003e72d08
Name:        RefAndTwoInt32s[]
MethodTable: 000007fe039d3b78
EEClass:     000007fe039d3ad0
Size:        40(0x28) bytes
Array:       Rank 1, Number of elements 1, Type VALUETYPE
Fields:
None

これがValueTypeであり、作成したものであることがわかります。これは配列であるため、配列内の単一要素のValueType defを取得する必要があります。

    0:004> !dumparray -details 0000000003e72d08
Name:        RefAndTwoInt32s[]
MethodTable: 000007fe039d3b78
EEClass:     000007fe039d3ad0
Size:        40(0x28) bytes
Array:       Rank 1, Number of elements 1, Type VALUETYPE
Element Methodtable: 000007fe039d3a58
[0] 0000000003e72d18
    Name:        RefAndTwoInt32s
    MethodTable: 000007fe039d3a58
    EEClass:     000007fe03ae2338
    Size:        32(0x20) bytes
    File:        C:\ConsoleApplication8\bin\Release\ConsoleApplication8.exe
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        000007fe61e8c358  4000006        0            System.String      0     instance     0000000003e72d30     text
        000007fe61e8f108  4000007        8             System.Int32      1     instance                    1     x
        000007fe61e8f108  4000008        c             System.Int32      1     instance                    0     y

16バイトはパディング用に予約されているため、構造は実際には32バイトです。したがって、実際にはすべての構造のサイズは少なくとも16バイトです。

intから16バイトと文字列参照を0000000003e72d18 + 8バイトEE/paddingに追加すると、最終的には0000000003e72d30になります。これは文字列参照の開始点であり、すべての参照は最初の実際のデータフィールドから8バイト埋め込まれますこれは、この構造の32バイトを補います。

文字列が実際にそのようにパディングされているかどうかを見てみましょう:

0:004> !do 0000000003e72d30    
Name:        System.String
MethodTable: 000007fe61e8c358
EEClass:     000007fe617f3720
Size:        28(0x1c) bytes
File:        C:\WINDOWS\Microsoft.Net\Assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String:      a
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007fe61e8f108  40000aa        8         System.Int32  1 instance                1 m_stringLength
000007fe61e8d640  40000ab        c          System.Char  1 instance               61 m_firstChar
000007fe61e8c358  40000ac       18        System.String  0   shared           static Empty
                                 >> Domain:Value  0000000001577e90:NotInit  <<

上記のプログラムを同じ方法で分析しましょう:

public static RefAndTwoInt32Wrappers[] test = new RefAndTwoInt32Wrappers[1];

static void Main()
{
    test[0].text = "a";
    test[0].x.x = 1;
    test[0].y.x = 1;

    Console.ReadKey();
}

0:004> !dumpheap -type Ref
     Address               MT     Size
0000000003c22c78 000007fe61e8fb58       56    
0000000003c22d08 000007fe039d3c00       48    

Statistics:
              MT    Count    TotalSize Class Name
000007fe039d3c00        1           48 RefAndTwoInt32Wrappers[]
000007fe61e8fb58        1           56 System.Reflection.RuntimeAssembly
Total 2 objects

構造体は現在48バイトです。

0:004> !dumparray -details 0000000003c22d08
Name:        RefAndTwoInt32Wrappers[]
MethodTable: 000007fe039d3c00
EEClass:     000007fe039d3b58
Size:        48(0x30) bytes
Array:       Rank 1, Number of elements 1, Type VALUETYPE
Element Methodtable: 000007fe039d3ae0
[0] 0000000003c22d18
    Name:        RefAndTwoInt32Wrappers
    MethodTable: 000007fe039d3ae0
    EEClass:     000007fe03ae2338
    Size:        40(0x28) bytes
    File:        C:\ConsoleApplication8\bin\Release\ConsoleApplication8.exe
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        000007fe61e8c358  4000009        0            System.String      0     instance     0000000003c22d38     text
        000007fe039d3a20  400000a        8             Int32Wrapper      1     instance     0000000003c22d20     x
        000007fe039d3a20  400000b       10             Int32Wrapper      1     instance     0000000003c22d28     y

ここで状況は同じです。0000000003c22d18+ 8バイトの文字列refを追加すると、最初のIntラッパーの先頭で値が実際にアドレスを指します。

これで、各値がオブジェクト参照であることがわかります。再度、0000000003c22d20をピークして確認します。

0:004> !do 0000000003c22d20
<Note: this object has an invalid CLASS field>
Invalid object

構造体のアドレスは、これがobjまたはvtの場合、何も通知しないため、実際は正しいです。

0:004> !dumpvc 000007fe039d3a20   0000000003c22d20    
Name:        Int32Wrapper
MethodTable: 000007fe039d3a20
EEClass:     000007fe03ae23c8
Size:        24(0x18) bytes
File:        C:\ConsoleApplication8\bin\Release\ConsoleApplication8.exe
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007fe61e8f108  4000001        0         System.Int32  1 instance                1 x

したがって、実際には、これは今度は8バイトに揃えられるUnion型に似ています(すべてのパディングは親構造体に揃えられます)。そうでなければ、20バイトになってしまい、最適ではないため、memアロケーターはそれを許可しません。再度計算すると、構造体のサイズが実際に40バイトであることがわかります。

したがって、メモリをより保守的にしたい場合は、構造体のカスタム構造体にパックするのではなく、単純な配列を使用する必要があります。別の方法は、ヒープ(たとえばVirtualAllocEx)からメモリを割り当てることです。これにより、独自のメモリブロックが与えられ、必要に応じて管理できます。

ここでの最後の質問は、なぜ突然そのようなレイアウトになるのかということです。 jitedコードと、int []インクリメントのパフォーマンスをstruct []でカウンターフィールドインクリメントと比較すると、2番目のものは8バイト境界のアドレスを生成します。 LEA対複数のMOV)。ただし、ここで説明するケースでは実際にパフォーマンスが低下するため、複数のフィールドを持つことができるカスタム型であるため、基になるCLR実装と一貫性があります。値(不可能なため)とそこに構造体のパディングを行います。その結果、バイトサイズが大きくなります。

19

まとめ@Hans Passantの上記の回答を参照してください。レイアウトシーケンシャルが機能しない


いくつかのテスト:

確かに64ビットのみであり、オブジェクト参照は構造体を「毒」します。 32ビットはあなたが期待していることをします:

Environment: CLR 4.0.30319.34209 on Microsoft Windows NT 6.2.9200.0 (32 bit)
ConsoleApplication1.Int32Wrapper: 4
ConsoleApplication1.TwoInt32s: 8
ConsoleApplication1.TwoInt32Wrappers: 8
ConsoleApplication1.ThreeInt32Wrappers: 12
ConsoleApplication1.Ref: 4
ConsoleApplication1.RefAndTwoInt32s: 12
ConsoleApplication1.RefAndTwoInt32Wrappers: 12
ConsoleApplication1.RefAndThreeInt32s: 16
ConsoleApplication1.RefAndThreeInt32Wrappers: 16

オブジェクト参照が追加されるとすぐに、すべての構造体が4バイトサイズではなく8バイトに拡張されます。テストの拡張:

Environment: CLR 4.0.30319.34209 on Microsoft Windows NT 6.2.9200.0 (64 bit)
ConsoleApplication1.Int32Wrapper: 4
ConsoleApplication1.TwoInt32s: 8
ConsoleApplication1.TwoInt32Wrappers: 8
ConsoleApplication1.ThreeInt32Wrappers: 12
ConsoleApplication1.Ref: 8
ConsoleApplication1.RefAndTwoInt32s: 16
ConsoleApplication1.RefAndTwoInt32sSequential: 16
ConsoleApplication1.RefAndTwoInt32Wrappers: 24
ConsoleApplication1.RefAndThreeInt32s: 24
ConsoleApplication1.RefAndThreeInt32Wrappers: 32
ConsoleApplication1.RefAndFourInt32s: 24
ConsoleApplication1.RefAndFourInt32Wrappers: 40

参照が追加されるとすぐにわかるように、すべてのInt32Wrapperは8バイトになるため、単純なアライメントではありません。配列の割り当てを縮小しましたが、LoHの割り当てが異なって配置されていたためです。

9
Ben Adams

ミックスにいくつかのデータを追加するために-私はあなたが持っていたものからもう1つのタイプを作成しました:

struct RefAndTwoInt32Wrappers2
{
    string text;
    TwoInt32Wrappers z;
}

プログラムは次のように書き出します。

RefAndTwoInt32Wrappers2: 16

したがって、TwoInt32Wrappers構造体は、新しいRefAndTwoInt32Wrappers2構造体。

4
Jesse C. Slicer