에서 노다 시간 V2, 우리는 나노초 해상도로 이동하고 있습니다. 이는 우리가 관심있는 전체 시간 범위를 나타 내기 위해 더 이상 8 바이트 정수를 사용할 수 없음을 의미합니다. 이로 인해 Noda Time의 (많은) 구조체의 메모리 사용량을 조사하게되었고 결과적으로 저를 이끌었습니다. CLR의 정렬 결정에서 약간의 이상한 점을 발견했습니다.
첫째, 나는 이것이 실현 이다 기본 동작은 언제든지 변경 될 수 있습니다 구현 결정하고있다. 나는 것을 깨닫게 수 사용하여 수정 [StructLayout]
하고 [FieldOffset]
,하지만 난 오히려 가능하면이 필요하지 않은 솔루션을 가지고 올 것입니다.
내 핵심 시나리오는 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). - 두 가지를 결합하면 각
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
중단 점에 도달하면 Debug + Windows + Memory + Memory 1을 사용합니다. 4 바이트 정수로 전환 &test
하고 Address 필드에 입력합니다.
0x000000E928B5DE98 0ed750e0 000000e9 11111111 00000000 22222222 00000000
0xe90ed750e0
내 컴퓨터의 문자열 포인터입니다 (당신의 것이 아닙니다). 쉽게 볼 수 있습니다Int32Wrappers
크기를 24 바이트로 바꾸는 추가 4 바이트의 패딩으로을 . 구조체로 돌아가서 문자열을 마지막에 넣으십시오. 반복하면 문자열 포인터가 여전히 첫 번째 임을 알 수 있습니다 . 위반 LayoutKind.Sequential
, 당신은 LayoutKind.Auto
.
마이크로 소프트가이 문제를 고치도록 설득하는 것은 어려울 것입니다. 너무 오랫동안 이런 식으로 작동했기 때문에 어떤 변화도 깨질 것입니다. 무언가를 뜨리게 것 입니다. CLR [StructLayout]
은 struct의 관리 버전 을 존중 하고 blittable로 만들 려고 시도 할 뿐이며 일반적으로 빠르게 포기합니다. DateTime을 포함하는 모든 구조체에 대해 유명합니다. 구조체를 마샬링 할 때만 진정한 LayoutKind 보장을받습니다. 마샬링 된 버전은 확실히 16 바이트 Marshal.SizeOf()
입니다.
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의 구조체 결합이 가장 많이 정렬 된 필드의 수에 의해 결정된다는 것입니다. 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는 길이와 동일하게 정렬되므로이 코드는 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 바이트이므로 실제로 모든 구조는 처음부터 최소 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
따라서 실제로 이것은 이번에 8 바이트 정렬되는 Union 유형과 더 비슷합니다 (모든 패딩은 부모 구조체와 정렬 됨). 그렇지 않다면 20 바이트로 끝날 것이고 그것은 최적이 아니므로 mem 할당자는 결코 그것을 허용하지 않을 것입니다. 다시 수학을하면 구조체의 크기가 실제로 40 바이트임을 알 수 있습니다.
따라서 메모리를 좀 더 보수적으로 사용하려면 구조체 사용자 지정 구조체 형식으로 압축하지 말고 대신 간단한 배열을 사용하십시오. 또 다른 방법은 힙 (예 : VirtualAllocEx)에서 메모리를 할당하는 것입니다. 이렇게하면 고유 한 메모리 블록이 제공되고 원하는 방식으로 관리 할 수 있습니다.
여기서 마지막 질문은 왜 갑자기 우리가 그런 레이아웃을 얻을 수 있는지입니다. int [] 증분의 지 티드 코드와 성능을 struct []와 카운터 필드 증분과 비교하면 두 번째는 유니온이되는 8 바이트 정렬 주소를 생성하지만 지팅하면 더 최적화 된 어셈블리 코드로 변환됩니다 (단일 LEA 대 다중 MOV). 그러나 여기에 설명 된 경우 성능이 실제로 더 나빠질 것이므로 여러 필드를 가질 수있는 사용자 지정 형식이므로 기본 CLR 구현과 일치하므로 시작 주소 대신 시작 주소를 입력하는 것이 더 쉬울 수 있습니다. value (불가능하기 때문에) 그리고 거기에서 구조체 패딩을 수행하여 더 큰 바이트 크기를 만듭니다.
답변
요약은 아마도 위의 @Hans Passant의 답변을 참조하십시오. Layout Sequential이 작동하지 않습니다.
일부 테스트 :
그것은 확실히 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 할당이 다르게 정렬 된 경우 배열 할당을 줄였습니다.
답변
믹스에 데이터를 추가하기 위해 보유한 유형에서 하나 이상의 유형을 만들었습니다.
struct RefAndTwoInt32Wrappers2
{
string text;
TwoInt32Wrappers z;
}
프로그램은 다음을 기록합니다.
RefAndTwoInt32Wrappers2: 16
따라서 TwoInt32Wrappers
구조체가 새 RefAndTwoInt32Wrappers2
구조체 에서 제대로 정렬 된 것처럼 보입니다 .