Programing

필드 정렬이 기본 유형인지 사용자 정의인지에 따라 구조체 정렬이 왜 달라 집니까?

crosscheck 2020. 7. 13. 20:35
반응형

필드 정렬이 기본 유형인지 사용자 정의인지에 따라 구조체 정렬이 왜 달라 집니까?


에서 노다 시간 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.

참고URL : https://stackoverflow.com/questions/24742325/why-does-struct-alignment-depend-on-whether-a-field-type-is-primitive-or-user-de

반응형