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
_
そう:
Int32Wrapper
_フィールドを一緒にパックします(_TwoInt32Wrappers
_のサイズは8です)int
フィールドをまとめてパックします(_RefAndTwoInt32s
_のサイズは16です)Int32Wrapper
_フィールドは8バイトにパディング/アラインされているように見えます。 (_RefAndTwoInt32Wrappers
_のサイズは24です。)他のいくつかの実験でも同様の結果が得られています。
object
の代わりにstring
を使用しても効果はありません(「任意の参照型」だと思います)int
フィールドは4バイトとしてカウントされ、_Int32Wrapper
_フィールドは8バイトとしてカウントされます[StructLayout(LayoutKind.Sequential, Pack = 4)]
をすべての構造物に追加しても結果は変わりません誰かがこれについて説明していますか(理想的には参照ドキュメントを使用して)、またはフィールドを定数フィールドオフセットを指定してパックする_なしが欲しいというCLRへのヒントを得る方法の提案がありますか?
これはバグだと思います。自動レイアウトの副作用が見られます。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
_を使用すると、聞きたいものではなく、修正されます。
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実装と一貫性があります。値(不可能なため)とそこに構造体のパディングを行います。その結果、バイトサイズが大きくなります。
まとめ@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の割り当てが異なって配置されていたためです。
ミックスにいくつかのデータを追加するために-私はあなたが持っていたものからもう1つのタイプを作成しました:
struct RefAndTwoInt32Wrappers2
{
string text;
TwoInt32Wrappers z;
}
プログラムは次のように書き出します。
RefAndTwoInt32Wrappers2: 16
したがって、TwoInt32Wrappers
構造体は、新しいRefAndTwoInt32Wrappers2
構造体。