필드 정렬이 기본 유형인지 사용자 정의인지에 따라 구조체 정렬이 왜 달라 집니까?
에서 노다 시간 V2, 우리는 나노초 해상도로 이동하고 있습니다. 그것은 우리가 더 이상 우리가 관심있는 전체 시간 범위를 나타내는 데 8 바이트 정수를 사용할 수 없다는 것을 의미합니다. 그것은 Noda Time의 (많은) 구조체의 메모리 사용량을 조사하도록 유도했습니다. CLR의 정렬 결정에서 약간의 이상한 점을 발견했습니다.
첫째, 나는 이것이 실현 이다 기본 동작은 언제든지 변경 될 수 있습니다 구현 결정하고있다. 나는 and 을 사용하여 수정할 수 있다는 것을 알고 있지만 가능하면 필요하지 않은 솔루션을 제안합니다.[StructLayout]
[FieldOffset]
내 핵심 시나리오는 struct
참조 유형 필드와 두 개의 다른 값 유형 필드가 포함되어 있으며이 필드는 간단한 래퍼입니다 int
. I는 한 희망 즉 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). - 이 두 가지를 결합하면 각
Int32Wrapper
필드가 8 바이트로 채워지거나 정렬 된 것처럼 보입니다. (RefAndTwoInt32Wrappers
크기는 24입니다.) - 디버거에서 동일한 코드를 실행하면 (하지만 여전히 릴리스 빌드) 크기는 12입니다.
다른 몇 가지 실험에서도 비슷한 결과가 나왔습니다.
- 값 유형 필드 뒤에 참조 유형 필드를 두는 것이 도움이되지 않습니다.
object
대신 사용하면string
도움 이 되지 않습니다 ( "모든 참조 유형"인 것으로 예상됩니다)- 참조 주위에 다른 래퍼를 "래퍼"로 사용하면 도움이되지 않습니다.
- 참조 주위에 래퍼로 일반 구조체를 사용하면 도움이되지 않습니다.
- 필드를 계속 추가하면 (단순화를 위해 쌍으로)
int
필드는 여전히 4 바이트로Int32Wrapper
계산 되고 필드는 8 바이트로 계산됩니다. [StructLayout(LayoutKind.Sequential, Pack = 4)]
보이는 모든 구조체에 추가해도 결과가 변경되지 않습니다
누구든지 이것에 대한 설명 (이상적으로 참조 문서와 함께)이나 상수 필드 오프셋 을 지정 하지 않고 필드를 포장하고 싶다는 CLR 힌트를 얻는 방법에 대한 제안이 있습니까?
나는 이것이 버그라고 생각한다. 자동 레이아웃의 부작용을보고 있습니다. 사소한 필드를 64 비트 모드에서 8 바이트의 배수 인 주소에 맞추는 것을 좋아합니다. [StructLayout(LayoutKind.Sequential)]
속성 을 명시 적으로 적용 할 때도 발생 합니다. 그런 일은 일어나지 않아야합니다.
구조체 멤버를 공개하고 다음과 같이 테스트 코드를 추가하여 볼 수 있습니다.
var test = new RefAndTwoInt32Wrappers();
test.text = "adsf";
test.x.x = 0x11111111;
test.y.x = 0x22222222;
Console.ReadLine(); // <=== Breakpoint here
중단 점에 도달하면 디버그 + Windows + 메모리 + 메모리 1을 사용하십시오. 4 바이트 정수로 전환 &test
하고 주소 필드에 넣으십시오 .
0x000000E928B5DE98 0ed750e0 000000e9 11111111 00000000 22222222 00000000
0xe90ed750e0
내 컴퓨터의 문자열 포인터입니다 (여러분이 아닙니다). Int32Wrappers
추가 4 바이트의 패딩을 사용하여 크기를 24 바이트로 쉽게 전환 할 수 있습니다 . 구조체로 돌아가서 문자열을 마지막에 놓습니다. 반복하면 문자열 포인터가 여전히 첫 번째 임을 알 수 있습니다 . 위반 LayoutKind.Sequential
하면 알 수 있습니다 LayoutKind.Auto
.
이 문제를 해결하려면 Microsoft 설득하기 어려울 것입니다, 어떤 변화가 파괴 될 것입니다 때문에 너무 오랫동안 이런 식으로 근무하고있다 뭔가를 . CLR [StructLayout]
은 관리되는 버전의 구조체 를 존중 하고 블 리터 블하게 만들 려고 시도 합니다. 일반적으로 빨리 포기합니다. 유감스럽게도 DateTime이 포함 된 모든 구조체에 적합합니다. 구조체를 마샬링 할 때 진정한 LayoutKind 보장 만 얻습니다. 마샬링 된 버전은 확실히 16 바이트 Marshal.SizeOf()
입니다.
사용하면 LayoutKind.Explicit
듣고 싶지 않은 문제가 해결됩니다.
편집 2
struct RefAndTwoInt32Wrappers
{
public int x;
public string s;
}
이 코드는 8 바이트로 정렬되므로 구조체는 16 바이트가됩니다. 비교하면 다음과 같습니다.
struct RefAndTwoInt32Wrappers
{
public int x,y;
public string s;
}
이 구조체는 16 바이트를 갖도록 4 바이트 정렬됩니다. 따라서 CLR의 구조체 aligment는 가장 정렬 된 필드의 수에 의해 결정되며 clases는 분명히 그렇게 할 수 없으므로 8 바이트 정렬 상태로 유지됩니다.
이제 우리가 모든 것을 결합하고 구조체를 만들면 :
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 내부의 커스텀 struct 필드는 항상 구조에서 가장 정렬 된 인스턴스 필드에 정렬됩니다. 이것이 버그인지 확실하지 않지만 증거가 없으면 이것이 의식적인 결정일 수 있다는 내 견해를 고수 할 것입니다.
편집하다
크기는 실제로 힙에 할당 된 경우에만 정확하지만 구조체 자체는 더 작은 크기 (필드의 정확한 크기)를 갖습니다. 추가 분석은 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으로 컴파일되었으며 In 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
We see that this is a ValueType and its the one we created. Since this is an array we need to get the ValueType def of a single element in the array:
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
The structure is actually 32 bytes since it's 16 bytes is reserved for padding so in actuality every structure is at least 16 bytes in size from the get go.
if you add 16 bytes from ints and a string ref to: 0000000003e72d18 + 8 bytes EE/padding you will end up at 0000000003e72d30 and this is the staring point for string reference, and since all references are 8 byte padded from their first actual data field this makes up for our 32 bytes for this structure.
Let's see if the string is actually padded that way:
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 <<
Now lets analyse the above program the same way:
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
Our struct is 48 bytes now.
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
Here the situation is the same, if we add to 0000000003c22d18 + 8 bytes of string ref we will end up at the start of the first Int wrapper where the value actually point to the address we are at.
Now we can see that each value is an object reference again lets confirm that by peeking 0000000003c22d20.
0:004> !do 0000000003c22d20
<Note: this object has an invalid CLASS field>
Invalid object
Actually thats correct since its a struct the address tells us nothing if this is an obj or 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
So in actuality this is a more like an Union type that will get 8 byte aligned this time around (all of the paddings will be aligned with the parent struct). If it weren't then we would end up with 20 bytes and that's not optimal so the mem allocator will never allow it to happen. If you do the math again it will turn out that the struct is indeed 40 bytes of size.
So if you want to be more conservative with memory you should never pack it in a struct custom struct type but instead use simple arrays. Another way is to allocate memory off heap (VirtualAllocEx for e.g) this way you are given you own memory block and you manage it the way you want.
The final question here is why all of a sudden we might get layout like that. Well if you compare the jited code and performance of a int[] incrementation with struct[] with a counter field incrementation the second one will generate a 8 byte aligned address being an union, but when jited this translates to more optimized assembly code (singe LEA vs multiple MOV). However in the case described here the performance will be actually worse so my take is that this is consistent with the underlying CLR implementation since it's a custom type that can have multiple fields so it may be easier/better to put the starting address instead of a value (since it would be impossible) and do struct padding there, thus resulting in bigger byte size.
Summary see @Hans Passant's answer probably above. Layout Sequential doesn't work
Some testing:
It is definitely only on 64bit and the object reference "poisons" the struct. 32 bit does what you are expecting:
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
As soon as the object reference is added all the structs expand to be 8 bytes rather their 4 byte size. Expanding the tests:
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
As you can see as soon as the reference is added every Int32Wrapper becomes 8 bytes so isn't simple alignment. I shrunk down the array allocation incase it was LoH allocation which is differently aligned.
Just to add some data to the mix - I created one more type from the ones you had:
struct RefAndTwoInt32Wrappers2
{
string text;
TwoInt32Wrappers z;
}
The program writes out:
RefAndTwoInt32Wrappers2: 16
So it looks like the TwoInt32Wrappers
struct aligns properly in the new RefAndTwoInt32Wrappers2
struct.
'Programing' 카테고리의 다른 글
Android Studio 실행 오류 (0) | 2020.07.14 |
---|---|
Java 웹 애플리케이션에 대한 Javascript 축소를 어떻게 자동화합니까? (0) | 2020.07.13 |
HTTP 응답 헤더에서 컨텐츠 처리 사용 (0) | 2020.07.13 |
const-reference로 std :: function을 전달해야합니까? (0) | 2020.07.13 |
그리드 / 타일 뷰를 만드는 방법? (0) | 2020.07.13 |