Ref. Type Memory Layout – Alignment & Packing



Introduction

In this post I'll shortly describe and demonstrate how the CLR Align & Pack the object's properties in memory (Managed Heap).
Meaning, the layout of the properties we'll declare in our object, is not according to the declaration order.

It is commonly thought that the order the developer declares the properties would be the same order they would reside in memory.

However, the CLR reorders them in the most effective way to fully exploit the object's memory, so we won't have any unused memory related to that object.

RemarkFor additional detailed information about .Net types - Memory Layout, please review the .Net Type Internals post.
  

Word size

'Word' in computer architecture context means - the basic unit of data (register) used by the processor, whereas the 'Word Size' refers to the size of the processor register.
·        On a 32 bit system, the word size is: 32 bits (4 Bytes).
·        On a 64 bit system, the word size is: 64 bits (8 Bytes).

The virtual memory addresses could be set in the range of the 'word lowest address value' to the 'word max address value', thus determines the process's 'Virtual Address Space' in memory.

This means that:
·        On a 32 bit system: reference types are aligned to the nearest 4 byte multiple.
Note: Even if the object contains only 1 byte property, its size would be 12 bytes:
           -        SBI (4) + MT Pointer (4) + (1 byte aligned to 4 bytes) = 12 bytes.

·        On a 64 bit system: reference types are aligned to the nearest 8 byte multiple.
Note: Even if the object contains only 1 byte property, its size would be 24 bytes:
           -        SBI (8) + MT Pointer (8) + (1 byte aligned to 8 bytes) = 24 bytes.



Remark: The StructLayout & FieldOffset attributes:

As stated above the order of the properties we'll declare in our object is not the same order of the object - memory layout.
However, in case we'll insists on controlling the properties order in memory (for particular scenarios, e.g. interoperability with COM), we could use the StructLayout and FieldOffset attributes.

For an example, please review the 'Controlling properties order & memory layout' section at the bottom of this post.



Demo-1

In this demo I'll create few objects and we'll observe theirs sizes in memory, and the effect of the Alignment & Packing behavior on the total object size.

-        I'll use Visual Studio different debug windows.
-        And we'll also debug the code in the immediate window, using SOS DLL extension.

Remark: I'm using 32 bit system for this example.


1.     Create a simple class - Employee:

/// Employee class - Running under 32bit system.
///
/// Total object size: 16 Bytes
///     - Properties: 2 Bytes + 4 Bytes + 2 Bytes = 8 Bytes
///     - MT Pointer + SBI: + 4 Bytes + 4 Bytes = 8 Bytes        
public class Employee
{
    public short Age { getset; } // Size: 2 Bytes

    public int Id { getset; } // Size: 4 Bytes

    public char Letter { getset; } // Size: 2 Bytes
}




I've created the 'Employee' class with 3 properties and ordered them according to my wish:
           -        Age: 2 bytes
           -        Id: 4 bytes
           -        Letter: 2 bytes


2.     Create an employee instance:

For simplicity, I've assigned these values.

private static void AlignmentAndPackingExample()
{
    Employee employee = new Employee();
    employee.Age = 9;
    employee.Id = 4;
    employee.Letter = 'A';
}



3.     Observe the employee instance in memory:

We could observe the object memory layout in V.S. Memory window.

Remark: For additional detailed information how to establish this view, please review the .Net Type Internals post.





Remark: All the values of the addresses and fields are hexadecimal values.

We could see that:
           -        SBI: occupied 4 bytes and its value is: 00000000
           -        MT Pointer: occupied 4 bytes and its value is: 001934a4
           -        Id: occupied 4 bytes and its value is: 00000004
           -        Letter: occupied 2 bytes and its value is: 0041 (41Hex=65Dec Ascii for 'A')
           -        Age: occupied 2 bytes and its value is: 0009


If the CLR was storing the properties according to their declaration order the 2 bytes properties had to occupy 4 bytes each, since on a 32 bit system reference types are aligned to the nearest 4 byte multiple.

Meaning the total object size was:
           -        Age: 2 bytes -> aligned to 4 bytes
           -        Id: 4 bytes
           -        Letter: 2 bytes -> aligned to 4 bytes
Total: 12 + 4 (SBI) + 4 (MT Pointer) = 20 bytes

Whereas, using the alignment & packing behavior, the total object size is:
           -        Id: 4 bytes
           -        Letter: 2 bytes + Age: 2 bytes -> aligned & packed to 4 bytes
Total: 8 + 4 (SBI) + 4 (MT Pointer) = 16 bytes


Conclusion

Even though I declared the order of the properties as:
           -        Age: 2 bytes
           -        Id: 4 bytes
           -        Letter: 2 bytes

The CLR aligned & packed the Letter and Age fields and order them as:
           -        Id: 4 bytes
           -        Letter: 2 bytes & Age: 2 bytes (total 4 bytes)

If the CLR wasn't maintaining this behavior, we would get a lot of wasted and unreachable memory due to the fact that some of the properties are less than 4 bytes in a 32 bit system, and less than 8 bytes in a 64 bit system.




Demo-2

In this demo I'd like to use SOS.dll extension to observe some objects sizes in memory.

1.     Examine Employee size

/// Employee class - Running under 32bit system.
///
/// Total object size: 16 Bytes
///     - Properties: 2 Bytes + 4 Bytes + 2 Bytes = 8 Bytes
///     - MT Pointer + SBI: + 4 Bytes + 4 Bytes = 8 Bytes

public class Employee
{
    public short Age { getset; } // Size: 2 Bytes

    public int Id { getset; } // Size: 4 Bytes

    public char Letter { getset; } // Size: 2 Bytes

}



I've performed the following:
           -        Load sos.dll.
           -        Reviewed the type on the Managed Heap.
           -        Reviewed the object data.

.load sos.dll
extension C:\Windows\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded

---
!DumpHeap -type TestPerfExample.Employee

PDB symbol for mscorwks.dll not loaded
 Address       MT     Size
01d29424 002934a4       16    
total 1 objects
Statistics:
      MT    Count    TotalSize Class Name
002934a4        1           16 TestPerfExample.Employee
Total 1 objects

---
!do 01d29424

Name: TestPerfExample.Employee
MethodTable: 002934a4
EEClass: 00291964
Size: 16(0x10) bytes
 …\TestPerfExample\bin\Debug\TestPerfExample.exe)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
6740ed4c  4000001        8         System.Int16  1 instance        9 k__BackingField
67412f74  4000002        4         System.Int32  1 instance        4 k__BackingField
67411a08  4000003        a          System.Char  1 instance       41 k__BackingField




2.     Class with 1 property

I've created the following class with only 1 property of 2 bytes size.

public class Rectangle
{
    public short Id { get; set; } // Short is Int16 - 2 bytes
}

private static void CreateRectangleExample()
{
    Rectangle rectangle = new Rectangle();
}

I performed the following:
           -        Load sos.dll.
           -        Reviewed the type on the Managed Heap.

.load sos.dll
extension C:\Windows\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded

---
!DumpHeap -type TestPerfExample.Rectangle

 Address       MT     Size
01b19424 001a347c       12    
total 1 objects
Statistics:
      MT    Count    TotalSize Class Name
001a347c        1           12 TestPerfExample.Rectangle
Total 1 objects

---
!ObjSize 01b19424

sizeof(01b19424) = 12 (0xc) bytes (TestPerfExample.Rectangle)



We could see that the total object size is 12 bytes, even though it had only 1 property of size 2 bytes.
The SBI and MT Pointer are 8 bytes + the 2 bytes of the property aligned to 4 bytes, thus the total is 12 bytes.


3.     Class without properties

I've created the following class without any properties, to examine its size.

public class Point
{
    // Empty class
}

private static void CreatePointExample()
{
    Point point = new Point();
}

I performed the following:
           -        Load sos.dll.
           -        Reviewed the type on the Managed Heap.

.load sos.dll
extension C:\Windows\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded

---
!DumpHeap -type TestPerfExample.Point

PDB symbol for mscorwks.dll not loaded
 Address       MT     Size
01c29424 003b3438       12    
total 1 objects
Statistics:
      MT    Count    TotalSize Class Name
003b3438        1           12 TestPerfExample.Point
Total 1 objects


We could see that the Point size is 12 bytes, even though we haven't declared a single property.
There are off course the SBI and the MT Pointer fields, each occupies 4 bytes of memory, but the additional 4 bytes relates to the Alignment & Packing behavior!


Conclusion

We managed to demonstrate how the Alignment & Packing behavior effects the objects sizes on the Managed Heap to exploit the maximum memory space for that object, and to prevent unused and unreachable memory.




Controlling properties order & memory layout

Prior to completing this post, I'd like to show a quick example of how to control the order and memory layout of our object's properties.

As described above, some scenarios dictate this control, and for that we'll use the StructLayout & FieldOffset attributes.

 /// Employee class - Running under 32bit system.
 ///
 /// Total object size: 16 Bytes
 ///     - Properties: 2 Bytes + 4 Bytes + 2 Bytes = 8 Bytes
 ///     - MT Pointer + SBI: + 4 Bytes + 4 Bytes = 8 Bytes
      
 [StructLayout(LayoutKind.Explicit)]
    public class Employee
    {
        [FieldOffset(offset: 0)]
        public short Age; // Size: 2 Bytes

        [FieldOffset(offset: 4)]
        public int Id; // Size: 4 Bytes

        [FieldOffset(offset: 8)]
        public char Letter; // Size: 2 Bytes

    }

    
    

For this example, I used the StructLayout attribute with the LayoutKind.Explicit.

This means that the members of the object are laid out in memory the way I declared them using the FieldOffset attribute.

Now, when we'll examine the 'Employee' memory layout and size, we'll get the following:

Memory layout:





We could see from the memory layout above that the Age and Letter fields occupy 4 bytes each, even though they only need 2 bytes each. Thus, in this way, we are creating 4 bytes of unused and unreachable memory related to this objects.

Size: 20 Bytes

.load sos.dll
extension C:\Windows\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded

---
!DumpHeap -type TestPerfExample.Employee

PDB symbol for mscorwks.dll not loaded
 Address       MT     Size
01c79424 00483474       20    
total 1 objects
Statistics:
      MT    Count    TotalSize Class Name
00483474        1           20 TestPerfExample.Employee
Total 1 objects

---
!ObjSize 01c79424

sizeof(01c79424) = 20 (0x14) bytes (TestPerfExample.Employee)




  



Value Types – Structures

One last remark concerning Value Types (structures) Memory Layout.

When creating a structure, the CLR stores its properties in the Thread Stack in the order in which they were declared.
This would mean that uncareful declaration of some properties in a structure could cause a lot of unused and unreachable memory.


Example:

Consider the following struct:

public struct StructPerson
{
   public short Value1 { get; set; }

   public int Value2 { get; set; }

   public short Value3 { get; set; }
}

It appears as if this struct size should be: 8 Bytes.

However in fact we could observe the following code and find out that it size is actually: 12 Bytes.

I've created 2 StructPerson and assigned values.

private static void CreateStructPersonExample()
{
    StructPerson structPerson1 = new StructPerson();
    structPerson1.Value1 = 1;
    structPerson1.Value2 = 2;
    structPerson1.Value3 = 3;

    StructPerson structPerson2 = new StructPerson();
    structPerson2.Value1 = 4;
    structPerson2.Value2 = 5;
    structPerson2.Value3 = 6;

    unsafe
    {
        Console.WriteLine(sizeof(StructPerson));
    }
}



Memory layout on the Thread Stack:





Now, if we'll examine the
Thread Stack memory, we'll notice the following: 

      -        We've declared 2 StructPerson variables on the Thread Stuck.
      -        We could see the Stuck Memory using the EBP (Base Pointer) register.
      -        The Stuck Memory starts in address: 0x05D6E5C4 and the addresses values are descending. Meaning the stack frame starts in a high address and descent as the stack fills in.
      -        We could see that in a Value Type the SBI & MT Pointer fields don’t exist.
      -        The struct size: we could see that each field in the struct occupies 4 bytes of memory, even the short fields (value1, value3), thus its size is 12 bytes.


We could fix this problem quite easily actually, using the StructLayout & FieldOffset attributes.

In this example I only used the StructLayout attribute with the LayoutKind.Auto.
This means that the CLR would automatically choose an appropriate layout for the members of this struct.

[StructLayout(LayoutKind.Auto)]
public struct StructPerson
{
   public short Value1 { get; set; }

   public int Value2 { get; set; }

   public short Value3 { get; set; }
}


Memory layout on the Thread Stack:





Now we could see that
Value1 and Value3 which are 2 bytes each are Aligned & Packed together. 
And the total size of the StructPerson is 8 bytes (instead as 12 bytes since the members were ordered 'wrong').





The End

Hope you enjoyed!
Appreciate your comments…

Yonatan Fedaeli

No comments: