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

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 has prompted me to investigate the memory usage of the (many) structs of Noda Time, which has in turn led me to uncover a slight oddity in the CLR's alignment decision.

Firstly, I realize that this is an implementation decision, and that the default behaviour could change at any time. I realize that I can modify it using [StructLayout] and [FieldOffset], but I'd rather come up with a solution which didn't require that if possible.

My core scenario is that I have a struct which contains a reference-type field and two other value-type fields, where those fields are simple wrappers for int. I had hoped that that would be represented as 16 bytes on the 64-bit CLR (8 for the reference and 4 for each of the others), but for some reason it's using 24 bytes. I'm measuring the space using arrays, by the way - I understand that the layout may be different in different situations, but this felt like a reasonable starting point.

Here's a sample program demonstrating the issue:

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);
    }
}

And the compilation and output on my laptop:

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

So:

  • If you don't have a reference type field, the CLR is happy to pack Int32Wrapper fields together (TwoInt32Wrappers has a size of 8)
  • Even with a reference type field, the CLR is still happy to pack int fields together (RefAndTwoInt32s has a size of 16)
  • Combining the two, each Int32Wrapper field appears to be padded/aligned to 8 bytes. (RefAndTwoInt32Wrappers has a size of 24.)
  • Running the same code in the debugger (but still a release build) shows a size of 12.

A few other experiments have yielded similar results:

  • Putting the reference type field after the value type fields doesn't help
  • Using object instead of string doesn't help (I expect it's "any reference type")
  • Using another struct as a "wrapper" around the reference doesn't help
  • Using a generic struct as a wrapper around the reference doesn't help
  • If I keep adding fields (in pairs for simplicity), int fields still count for 4 bytes, and Int32Wrapper fields count for 8 bytes
  • Adding [StructLayout(LayoutKind.Sequential, Pack = 4)] to every struct in sight doesn't change the results

Does anyone have any explanation for this (ideally with reference documentation) or a suggestion of how I can get hint to the CLR that I'd like the fields to be packed without specifying a constant field offset?


Solution 1:

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.

Solution 2:

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.