122

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?

Jon Skeet
  • 1,261,211
  • 792
  • 8,724
  • 8,929
  • 1
    You don't actually seem to be using `Ref` but are using `string` instead, not that it should make a difference. – tvanfosson Jul 14 '14 at 17:37
  • @tvanfosson: Humbug - `Ref` shouldn't have been there - it was an experiment that didn't help (the "generic struct as a wrapper around the reference" part). I've removed it now - thanks for mentioning it. – Jon Skeet Jul 14 '14 at 17:38
  • 2
    What happens if you put two create a struct with two `TwoInt32Wrappers`, or an `Int64` and a `TwoInt32Wrappers`? How about if you create a generic `Pair {public T1 f1; public T2 f2;}` and then create `Pair>` and `Pair>`? Which combinations force the JITter to pad things? – supercat Jul 14 '14 at 17:44
  • 7
    @supercat: It's probably best for you to copy the code and experiment for yourself - but `Pair` *does* give just 16 bytes, so that would address the issue. Fascinating. – Jon Skeet Jul 14 '14 at 17:50
  • When structures are passed by value, they are sometimes placed on the stack and sometimes in registers, except in generic code where from what I can tell they are always placed on the stack. I suspect the JITter might have recognized `Int32Wrapper` as a type which can benefit from being passed as a register, and `TwoInt32Wrappers` as benefiting from having each part in a wrapper, but it loses sight of the fact that the things to be passed as registers only need to be 32 bits. – supercat Jul 14 '14 at 17:55
  • Have you tried `Marshal.SizeOf()`? (for simpler measurements) – SLaks Jul 14 '14 at 18:08
  • @supercat: It's odd that the fact that there's a reference type field is what alters things though. – Jon Skeet Jul 14 '14 at 18:09
  • @SLaks: No, partly because I'm not interested in marshalling to native code, and partly because the documentation seems wholly unclear to me in terms of what it actually does. – Jon Skeet Jul 14 '14 at 18:10
  • 9
    @SLaks: Sometimes when a structure is passed to native code, the Runtime will copy all of the data to a structure with a different layout. `Marshal.SizeOf` will return the size of the structure which would be passed to native code, which need not have any relation to the size of the structure in .NET code. – supercat Jul 14 '14 at 18:23
  • You're probably aware but want to point out your division isn't clean (e.g. Int32Wrapper is really 4.00016, 4.00024 on x86, x64 respectively) – Rafael Rivera Jul 14 '14 at 18:31
  • @RafaelRivera: Yes, that's because of the tiny overhead of the actual array object itself. I've deliberately used integer division to keep it to the element size via rounding :) – Jon Skeet Jul 14 '14 at 18:43
  • Does field order make any difference? Also on 32-bit it behaves like you would expect it to. – leppie Jul 14 '14 at 19:09
  • @leppie: Nope, field order doesn't help. I haven't bothered testing under 32 bit :) – Jon Skeet Jul 14 '14 at 19:11
  • Is a ref and 3 ints the same size? – Ben Adams Jul 14 '14 at 21:19
  • @BenAdams: I've not tried yet. But I'd expect padding to 8 bytes anyway. – Jon Skeet Jul 14 '14 at 21:21
  • [StructLayout(LayoutKind.Sequential, Pack=1)] ? – Ben Adams Jul 14 '14 at 21:43
  • @BenAdams: Think I tried that, but will double check. – Jon Skeet Jul 14 '14 at 21:44
  • 1
    The explanation for DateTime causing automatic layout is simple: it has automatic layout itself. [Why does LayoutKind.Sequential work differently if a struct contains a DateTime field?](http://stackoverflow.com/q/4132533/445517) – CodesInChaos Jul 15 '14 at 00:41
  • 5
    The interesting observation: Mono gives correct results. Environment: CLR 4.0.30319.17020 on Unix 3.13.0.24 (64 bit) Int32Wrapper: 4 TwoInt32s: 8 TwoInt32Wrappers: 8 RefAndTwoInt32s: 16 RefAndTwoInt32Wrappers: 16 – AndreyAkinshin Jul 15 '14 at 01:59
  • So just for fun I turned on unsafe and made an unsafe struct with `char*` and the two Int32Wrappers and when I run `ShowSize` on Int64 I get 16 for the size. – Maurice Reeves Jul 17 '14 at 01:59
  • http://blogs.msdn.com/b/oldnewthing/archive/2004/08/25/220195.aspx – Vladimir Sep 04 '14 at 13:33
  • @VladimirFrolov: That doesn't really explain why two types which are both 64 bits end up causing different alignment in a containing struct. – Jon Skeet Sep 04 '14 at 13:47

4 Answers4

85

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.

Hans Passant
  • 873,011
  • 131
  • 1,552
  • 2,371
  • 7
    "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 fact that this apparently does not manifest in 32 bit or mono may help (as per other comments). – NPSF3000 Jul 15 '14 at 03:24
  • The documentation of StructLayoutAttribute is pretty interesting. Basically, only blittable types are controlled through StructLayout in managed memory. Interesting, never knew that. – Michael Stum Jul 15 '14 at 07:01
  • @Soner no it does not fix it. Did you put the Layout on both fields to be offset 8 ? If so then x and y are the same and changing one changes the other. Clearly not what Jon is after. – BartoszAdamczewski Jul 15 '14 at 07:04
  • Substituting `string` with another new reference type (`class`) to which one has applied `[StructLayout(LayoutKind.Sequential)]` does not appear to change anything. In the opposite direction, applying `[StructLayout(LayoutKind.Auto)]` to the `struct Int32Wrapper` changes the memory usage in `TwoInt32Wrappers`. – Jeppe Stig Nielsen Jul 15 '14 at 08:26
  • 1
    "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." http://xkcd.com/1172/ – iCodeSometime Mar 17 '15 at 20:38
19

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.

  • 1
    Looking at this myself, the size of `RefAndTwoInt32Wrappers` *isn't* 32 bytes - it's 24, which is the same as reported with my code. If you look in the memory view instead of using `dumparray`, and look at the memory for an array with (say) 3 elements with distinguishable values, you can clearly see that each element consists of an 8-byte string reference and two 8-byte integers. I suspect that `dumparray` is showing the values as references simply because it doesn't know how to display `Int32Wrapper` values. Those "references" point to themselves; they're not separate values. – Jon Skeet Jul 15 '14 at 09:21
  • 1
    I'm not quite sure where you get the "16 byte padding" from, but I suspect it may be because you're looking at the size of the array object, which will be "16 bytes + count * element size". So an array with count 2 has a size of 72 (16 + 2 * 24), which is what `dumparray` shows. – Jon Skeet Jul 15 '14 at 09:27
  • @jon did you dumpheap your struct and checked how many space does it occupy on the heap? Normally the array size is kept at the start of the array, this can also be verified. – BartoszAdamczewski Jul 15 '14 at 09:49
  • @jon the size reported contains also the offset of the string which starts at 8. I don't think that those extra 8 bytes mentioned come from array since most of the array stuff resides before the first element address, but I will double check and comment on that. – BartoszAdamczewski Jul 15 '14 at 10:00
  • Yes, I looked at the array object: it consisted of a type reference (as for any object), the count, and then the elements. Each element was a reference (8 bytes) and then the two integers (each 8 bytes). – Jon Skeet Jul 15 '14 at 10:03
  • If you add the WinDbg reported size you will go to the next object which will be the first field of a string object so in this regard size check out (again will double check). – BartoszAdamczewski Jul 15 '14 at 10:10
  • Yes, WinDbg is reporting the right size - but for the whole array object, which isn't the struct size. That's my point. I think we're talking at cross-purposes... but I'm sure that the size of `RefAndTwoInt32Wrappers` really *is* 24 bytes. – Jon Skeet Jul 15 '14 at 10:13
  • Ok yes since it's on the heap now so the reported size on the heap is actually correct but the struct layout is 24 bytes this is correct. – BartoszAdamczewski Jul 15 '14 at 10:19
  • Right. And that's the problem - for `RefAndTwoInt32s` it's 16, and for just `TwoInt32Wrappers` it's 8. There's no good reason for the expansion here, as far as I can see. Why would the presence of another field affect the padding of a different one? – Jon Skeet Jul 15 '14 at 10:27
  • This might be true is it's on the heap, have you checked the stack? – BartoszAdamczewski Jul 15 '14 at 10:30
  • @jon this is logical like I wrote the CLR did this to preserve 8 byte alignment it would be otherwise impossible. If you would do that with 3 IntWrappers and no string you should still end up having 16 or 24 bytes. – BartoszAdamczewski Jul 15 '14 at 10:33
  • 1
    No, ThreeInt32Wrappers ends up as 12 bytes, FourInt32Wrappers as 16, FiveInt32Wrappers as 20. I don't see anything logical about the addition of a reference type field changing the layout so drastically. And note that it's quite happy to ignore 8-byte alignment when the fields are of type `Int32`. I'm not too bothered about what it does on the stack, to be honest - but I haven't checked it. – Jon Skeet Jul 15 '14 at 10:58
  • Good point, I will check the mem allocator class in sscli that might give some more answers with respect to structs. Another thing to try is to put in a string inside the IntWrapper. – BartoszAdamczewski Jul 15 '14 at 11:06
  • Thanks for the extra detail in edit 2. It doesn't explain *why* it has this behaviour, but it's interesting anyway. – Jon Skeet Jul 16 '14 at 06:26
  • @BartoszAdamczewski Just a note: You can remove your older edits and just keep the latest. Because each question and answer on StackOverflow has a history where you can review all edits. – Gottlieb Notschnabel Jul 16 '14 at 09:40
9

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.

Ben Adams
  • 3,057
  • 21
  • 26
4

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.

Jesse C. Slicer
  • 19,000
  • 3
  • 63
  • 80