Optimize struct performances using StructLayout

 
 
  • Gérald Barré

A struct type is a value type that is typically used to encapsulate small groups of related variables. It is often used for interop with native DLL ([DllImport]). Structs are also used for performance reasons. Value types are allocated on the stack so it reduces the number of objects the garbage collector (GC) must clean. Also, structs are smaller than classes in memory as they doesn't have a header. There are also CLR specific optimizations such as struct promotion which may improve the performance of structs. But there is another point to consider when you care about performance: the order of fields in the struct. By default, a user-defined struct that contains only primitive fields (blittable types) has the Sequential layout with Pack equal to 0. Here is a rule that the CLR follows:

Each field must align with fields of its size (1, 2, 4, 8, etc., bytes) or the alignment of the type, whichever is smaller. Because the default alignment of the type is the size of its largest element, which is greater than or equal to all other field lengths, this usually means that fields are aligned by their size. For example, even if the largest field in a type is a 64-bit (8-byte) integer or the Pack field is set to 8, Byte fields align on 1-byte boundaries, Int16 fields align on 2-byte boundaries, and Int32 fields align on 4-byte boundaries. If a struct contains a reference type, its layout is changed to Auto.

StructLayoutAttribute.Pack Field

Let's see what it means by inspecting the layout of a struct. You can use ObjectLayoutInspector to view the layout of an object.

C#
class Program
{
    static void Main()
    {
        TypeLayout.PrintLayout<Sample>();
    }
}

struct Sample
{
    byte field1;   // 1 byte
    int field2;    // 4 bytes
    bool field3;   // 1 byte
    short field4;  // 2 bytes
}
Type layout for 'Sample'
Size: 12 bytes. Paddings: 4 bytes (%33 of empty space)
|================================|
|     0: Byte field1 (1 byte)    |
|--------------------------------|
|   1-3: padding (3 bytes)       | 👈 Int32 field is align on a multiple of 4, so it needs 3 padding bytes
|--------------------------------|
|   4-7: Int32 field2 (4 bytes)  |
|--------------------------------|
|     8: Boolean field3 (1 byte) |
|--------------------------------|
|     9: padding (1 byte)        | 👈 Int16 field is align on a multiple of 2, so it needs 1 padding byte
|--------------------------------|
| 10-11: Int16 field4 (2 bytes)  |
|================================|

As you can see, .NET adds some padding for the fields to be memory aligned. You can change the order of the fields to remove the padding and so save some bytes:

C#
// Change the order of the fields to remove the paddings
struct Sample
{
    int field2;   // 4 bytes
    short field4; // 2 bytes
    byte field1;  // 1 byte
    bool field3;  // 1 byte
}
Type layout for 'Sample'
Size: 8 bytes. Paddings: 0 bytes (%0 of empty space) 👈 No more padding
|================================|
|   0-3: Int32 field2 (4 bytes)  |
|--------------------------------|
|   4-5: Int16 field4 (2 bytes)  |
|--------------------------------|
|     6: Byte field1 (1 byte)    |
|--------------------------------|
|     7: Boolean field3 (1 byte) |
|================================|

There is no more padding in the struct and the size of the struct is now 8 bytes instead of 12 bytes. By re-ordering the fields in the struct, we have reduced its size by 33%!

Instead of re-ordering all fields in your structs manually, you can use the attribute [StructLayout(LayoutKink.Auto)] to allow .NET to automatically re-order the fields in the best way to avoid padding:

C#
[StructLayout(LayoutKind.Auto)]
struct Sample
{
    byte field1;   // 1 byte
    int field2;    // 4 bytes
    bool field3;   // 1 byte
    short field4;  // 2 bytes
}
Type layout for 'Sample'
Size: 8 bytes. Paddings: 0 bytes (%0 of empty space)
|================================|
|   0-3: Int32 field2 (4 bytes)  |
|--------------------------------|
|   4-5: Int16 field4 (2 bytes)  |
|--------------------------------|
|     6: Byte field1 (1 byte)    |
|--------------------------------|
|     7: Boolean field3 (1 byte) |
|================================|

Note that the sequential layout is only possible if a type doesn't have reference types in it. If a struct has at least one field of a reference type, the layout is automatically changed to LayoutKind.Auto. For instance, if we replace the int by a string in the first example, the layout is changed to Auto:

C#
struct Sample
{
    byte field1;   // 1 byte
    string field2; // 4 bytes (Reference type => .NET uses LayoutKind.Auto automatically)
    bool field3;   // 1 byte
    short field4;  // 2 bytes
}
Type layout for 'Sample'
Size: 8 bytes. Paddings: 0 bytes (%0 of empty space)
|================================|
|   0-3: String field2 (4 bytes) | 👈 .NET has re-order the fields
|--------------------------------|
|   4-5: Int16 field4 (2 bytes)  |
|--------------------------------|
|     6: Byte field1 (1 byte)    |
|--------------------------------|
|     7: Boolean field3 (1 byte) |
|================================|

#Adding the attribute using a Roslyn Analyzer

You can check if you should add [StructLayout(LayoutMode.Auto)] for structs in your applications using a Roslyn analyzer. The good news is the free analyzer I've made already contains rules for that: https://github.com/meziantou/Meziantou.Analyzer.

You can install the Visual Studio extension or the NuGet package to analyze your code:

#Additional references:

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub