構造体の整列がフィールドタイプがプリミティブであるかユーザー定義であるかに依存するのはなぜですか?


121

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

まず、これ実装の決定であり、デフォルトの動作はいつでも変更される可能性があることを理解しています。とを使用して変更できることを理解していますが、可能であればそれを必要としないソリューションを考え出します。[StructLayout][FieldOffset]

私の中心的なシナリオはstruct、参照タイプのフィールドと他の2つの値タイプのフィールドを含むaがあり、これらのフィールドが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へのヒントを得る方法についての提案はありますか?


1
あなたは実際には使用していないようですRef<T>が、string代わりに使用しています。違いを生むはずではありません。
tvanfosson 2014

2
2つを置くと、2つTwoInt32Wrappers、またはan Int64とa を持つ構造体を作成するとTwoInt32Wrappersどうなりますか?ジェネリックPair<T1,T2> {public T1 f1; public T2 f2;}を作成してPair<string,Pair<int,int>>から作成するとPair<string,Pair<Int32Wrapper,Int32Wrapper>>どうですか?JITterがパディングを強制するのはどの組み合わせですか?
スーパーキャット2014

7
@supercat:コードをコピーして自分で試してみるのがおそらく最善ですが、16バイトPair<string, TwoInt32Wrappers> しか与えないので、この問題に対処できます。魅力的です。
Jon Skeet、2014

9
@SLaks:構造がネイティブコードに渡されると、ランタイムはすべてのデータを異なるレイアウトの構造にコピーします。 Marshal.SizeOfネイティブコードに渡される構造のサイズを返します。これは、.NETコードの構造のサイズとは何の関係もありません。
スーパーキャット2014

5
興味深い観察:Monoは正しい結果を与えます。環境:CLR 4.0.30319.17020 on Unix 3.13.0.24(64ビット)Int32Wrapper:4 TwoInt32s:8 TwoInt32Wrappers:8 RefAndTwoInt32s:16 RefAndTwoInt32Wrappers:16
AndreyAkinshin

回答:


85

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

構造体のメンバーを公開し、次のようなテストコードを追加することで確認できます。

    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、Addressフィールドに入力します。

 0x000000E928B5DE98  0ed750e0 000000e9 11111111 00000000 22222222 00000000 

0xe90ed750e0私のマシンの文字列ポインタです(あなたのものではありません)。Int32Wrappersサイズを24バイトに変えた4バイトのパディングを追加すると、を簡単に確認できます。構造体に戻り、文字列を最後に置きます。繰り返しますと、文字列ポインタがまだ最初に表示されます。違反LayoutKind.Sequential、あなたが得たLayoutKind.Auto

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

LayoutKind.Explicitあなたが聞きたかったのではなく、それを使ってそれを修正する。


7
「これを修正するようにマイクロソフトを説得するのは難しいだろう、それはあまりにも長い間この方法で働いてきたので、どんな変化も何かを壊すことになるだろう。」これが明らかに32ビットまたはモノラルで現れないという事実は役立つかもしれません(他のコメントのとおり)。
NPSF3000 2014

StructLayoutAttributeのドキュメントはかなり興味深いものです。基本的に、管理可能なメモリのStructLayoutを介して制御されるのはblittable型のみです。興味深い、それを知らなかった。
Michael Stum

@ソナーいいえそれを修正しません。レイアウトを両方のフィールドにオフセット8で配置しましたか?その場合、xとyは同じであり、一方を変更すると他方が変更されます。Jonの目的は明らかに異なります。
BartoszAdamczewski 14

代入string別の新しい参照型(とclass1が適用されているに)[StructLayout(LayoutKind.Sequential)]変更何も表示されません。反対の方向で[StructLayout(LayoutKind.Auto)]struct Int32Wrapper変更に適用すると、のメモリ使用量が変化しますTwoInt32Wrappers
Jeppe Stig Nielsen

1
「これを修正するようにマイクロソフトを説得するのは難しいだろう、それはあまりにも長い間この方法で働いてきたので、どんな変化も何かを壊すことになるだろう。」xkcd.com/1172
iCodeSometime

19

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バイトに整列された参照文字列の場合、構造体ラッパーはそれに整列します。

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


編集

サイズは実際にはヒープに割り当てられた場合にのみ正確ですが、構造体自体のサイズは小さくなります(フィールドの正確なサイズ)。これは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バイトです。実際には、すべての構造体はget goから少なくとも16バイトのサイズです。

intから16バイトと文字列参照を0000000003e72d18 + 8バイトのEE /パディングに追加すると、最終的に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バイトの文字列参照に追加すると、値が実際にあるアドレスを指す最初の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

したがって、実際には、これはUnionタイプに似ており、今回は8バイトで整列されます(すべてのパディングは親構造体で整列されます)。そうでなければ、20バイトになってしまい、それは最適ではないので、memアロケータがそれを発生させることは決してありません。もう一度計算すると、構造体は実際に40バイトのサイズであることがわかります。

そのため、メモリをより保守的にしたい場合は、それをstructカスタムstructタイプにパックしないでください。代わりに、単純な配列を使用してください。もう1つの方法は、ヒープからメモリを割り当てることです(たとえば、VirtualAllocEx)。これにより、独自のメモリブロックが与えられ、必要な方法で管理できます。

ここで最後の質問は、なぜ突然このようなレイアウトになるのかということです。まあ、jitされたコードと、struct []を使用したint []のインクリメントとカウンタフィールドのインクリメントのパフォーマンスを比較すると、2番目のアドレスは8バイト境界で整列されたアドレスをユニオンとして生成しますが、jitすると、これはより最適化されたアセンブリコードに変換されます(singe LEA vs複数のMOV)。ただし、ここで説明するケースではパフォーマンスが実際に悪くなるため、複数のフィールドを持つことができるカスタムタイプであるため、基になるCLR実装と一貫しているので、開始アドレスを指定するのではなく、より簡単/優れている可能性があります。値(それは不可能であるため)とそこに構造体のパディングを行うため、バイトサイズが大きくなります。


1
これを私自身で見てみると、サイズRefAndTwoInt32Wrappers 32バイトではなく、24です。これは、私のコードで報告されているものと同じです。を使用する代わりにメモリビューdumparrayを見て、区別可能な値を持つ(たとえば)3つの要素を持つ配列のメモリを見ると、各要素が8バイトの文字列参照と2つの8バイトの整数で構成されていることがはっきりとわかります。 。値dumparrayを表示Int32Wrapperする方法がわからないという理由だけで、値を参照として表示しているのではないかと思います。それらの「参照」は自分自身を指します。それらは別個の値ではありません。
Jon Skeet、2014

1
「16バイトのパディング」がどこから得られるかはよくわかりませんが、「16バイト+カウント*エレメントサイズ」になる配列オブジェクトのサイズを見ていることが原因である可能性があります。したがって、カウント2の配列のサイズは72(16 + 2 * 24)であり、これがdumparray示しています。
Jon Skeet、2014

@ジョンあなたはあなたの構造体をダンプヒープし、それがヒープ上にどれだけのスペースを占有しているかをチェックしましたか?通常、配列のサイズは配列の先頭に保持されますが、これも確認できます。
BartoszAdamczewski 2014

@jon報告されたサイズには、8から始まる文字列のオフセットも含まれます。ほとんどの配列の要素は最初の要素のアドレスの前にあるため、これらの追加の8バイトは配列に由来するとは思われませんが、ダブルチェックしてそれについてコメントしてください。
BartoszAdamczewski 14

1
いいえ、ThreeInt32Wrappersは12バイトになり、FourInt32Wrappersは16、FiveInt32Wrappersは20になります。レイアウトを大幅に変更する参照型フィールドの追加について論理的なものは何もありません。また、フィールドがtypeの場合、8バイトのアライメントを無視しても非常に嬉しいことに注意してくださいInt32。私はスタックで何をするかについてあまり気にしていません。正直に言うと、チェックしていません。
Jon Skeet、2014

9

要約は、おそらく上記の@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の割り当てである場合は、配列の割り当てを縮小しました。


4

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

struct RefAndTwoInt32Wrappers2
{
    string text;
    TwoInt32Wrappers z;
}

プログラムは次のように書きます:

RefAndTwoInt32Wrappers2: 16

したがって、TwoInt32Wrappers構造体は新しいRefAndTwoInt32Wrappers2構造体で適切に整列しているように見えます。


64ビットを実行していますか?32ビットでの配置は問題あり
ベンアダムス

私の調査結果は、さまざまな環境で他の人と同じです。
ジェシーC.スライサー2014
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.