Why does struct alignment depend on whether a field type is primitive or user-defined?

后端 未结 4 825
天涯浪人
天涯浪人 2020-12-07 14:38

In Noda Time v2, we\'re moving to nanosecond resolution. That means we can no longer use an 8-byte integer to represent the whole range of time we\'re interested in. That ha

相关标签:
4条回答
  • 2020-12-07 15:00

    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.

    0 讨论(0)
  • 2020-12-07 15:04

    EDIT2

    struct RefAndTwoInt32Wrappers
    {
        public int x;
        public string s;
    }
    

    This code will be 8 byte aligned so the struct will have 16 bytes. By comparison this:

    struct RefAndTwoInt32Wrappers
    {
        public int x,y;
        public string s;
    }
    

    Will be 4 byte aligned so this struct also will have 16 bytes. So the rationale here is that struct aligment in CLR is determined by the number of most aligned fields, clases obviously cannot do that so they will remain 8 byte aligned.

    Now if we combine all that and create struct:

    struct RefAndTwoInt32Wrappers
    {
        public int x,y;
        public Int32Wrapper z;
        public string s;
    }
    

    It will have 24 bytes {x,y} will have 4 bytes each and {z,s} will have 8 bytes. Once we introduce a ref type in the struct CLR will always align our custom struct to match the class alignment.

    struct RefAndTwoInt32Wrappers
    {
        public Int32Wrapper z;
        public long l;
        public int x,y;  
    }
    

    This code will have 24 bytes since Int32Wrapper will be aligned the same as long. So the custom struct wrapper will always align to the highest/best aligned field in the structure or to it's own internal most significant fields. So in the case of a ref string that is 8 byte aligned the struct wrapper will align to that.

    Concluding custom struct field inside struct will always be aligned to the highest aligned instance field in the structure. Now if i'm not sure if this is a bug but without some evidence I'm going to stick by my opinion that this might be conscious decision.


    EDIT

    The sizes are actually accurate only when allocated on a heap but the structs themselves have smaller sizes (the exact sizes of it's fields). Further analysis seam to suggest that this might be a bug in the CLR code, but needs to be backed up by evidence.

    I will inspect cli code and post further updates if something useful will be found.


    This is a alignment strategy used by .NET mem allocator.

    public static RefAndTwoInt32s[] test = new RefAndTwoInt32s[1];
    
    static void Main()
    {
        test[0].text = "a";
        test[0].x = 1;
        test[0].x = 1;
    
        Console.ReadKey();
    }
    

    This code compiled with .net40 under x64, In WinDbg lets do the following:

    Lets find the type on the Heap first:

        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
    

    Once we have it lets see what's under that address:

        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.

    0 讨论(0)
  • 2020-12-07 15:06

    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.

    0 讨论(0)
  • 2020-12-07 15:10

    I think this is a bug. You are seeing the side-effect of automatic layout, it likes to align non-trivial fields to an address that's a multiple of 8 bytes in 64-bit mode. It occurs even when you explicitly apply the [StructLayout(LayoutKind.Sequential)] attribute. That is not supposed to happen.

    You can see it by making the struct members public and appending test code like this:

        var test = new RefAndTwoInt32Wrappers();
        test.text = "adsf";
        test.x.x = 0x11111111;
        test.y.x = 0x22222222;
        Console.ReadLine();      // <=== Breakpoint here
    

    When the breakpoint hits, use Debug + Windows + Memory + Memory 1. Switch to 4-byte integers and put &test in the Address field:

     0x000000E928B5DE98  0ed750e0 000000e9 11111111 00000000 22222222 00000000 
    

    0xe90ed750e0 is the string pointer on my machine (not yours). You can easily see the Int32Wrappers, with the extra 4 bytes of padding that turned the size into 24 bytes. Go back to the struct and put the string last. Repeat and you'll see the string pointer is still first. Violating LayoutKind.Sequential, you got LayoutKind.Auto.

    It is going to be difficult to convince Microsoft to fix this, it has worked this way for too long so any change is going to be breaking something. The CLR only makes an attempt to honor [StructLayout] for the managed version of a struct and make it blittable, it in general quickly gives up. Notoriously for any struct that contains a DateTime. You only get the true LayoutKind guarantee when marshaling a struct. The marshaled version certainly is 16 bytes, as Marshal.SizeOf() will tell you.

    Using LayoutKind.Explicit fixes it, not what you wanted to hear.

    0 讨论(0)
提交回复
热议问题