Struct memory layout optimizations, practical considerations
In my previous post I discussed how we could store the exact same information in several ways, leading to space savings of 66%! That leads to interesting questions with regard to actually making use of this technique in the real world.
The reason I posted about this topic is that we just gained a very significant reduction in memory (and we obviously care about reducing resource usage). The question is whether this is something that you want to do in general.
Let’s look at that in detail. For this technique to be useful, you should be using structs in the first place. That is… not quite true, actually. Let’s take a look at the following declarations:
We define the same shape twice. Once as a class and once as a structure. How does this look in memory?
Type layout for ‘PersonClass’ Size: 32 bytes. Paddings: 2 bytes %12 of Object Header 8 bytes Method Table Ptr 8 bytes 16 19: Int32 Id 4 bytes 20 21: UInt16 Kids 2 bytes 22 23: padding 2 bytes 24 32: DateTime Birthday 8 bytes empty space Type layout for ‘PersonStruct’ Size: 24 bytes. Paddings: 10 bytes %41 of 0 3: Int32 Id 4 bytes 4 7: padding 4 bytes 8 15: DateTime Birthday 8 bytes 16 17: UInt16 Kids 2 bytes 18 23: padding 6 bytes empty space
Here you can find some really interesting differences. The struct is smaller than the class, but the amount of wasted space is much higher in the struct. What is the reason for that?
The class needs to carry 16 bytes of metadata. That is the object header and the pointer to the method table. You can read more about the topic here. So the memory overhead for a class is 16 bytes at a minimum. But look at the rest of it.
You can see that the layout in memory of the fields is different in the class versus the structure. C# is free to re-order the fields to reduce the padding and get better memory utilization for classes, but I would need [StructLayout(LayoutKind.Auto)] to do the same for structures.
The difference between the two options can be quite high, as you can imagine. Note that automatically laying out the fields in this manner means that you’re effectively declaring that the memory layout is an implementation detail. This means that you cannot persist it, send it to native code, etc. Basically, the internal layout may change at any time. Classes in C# are obviously not meant for you to poke into their internals, and LayoutKind.Auto comes with an explicit warning about its behavior.
Interestingly enough, [StructLayout] will work on classes, you can use to force LayoutKind.Sequential on a class. That is by design, because you may need to pass a part of your class to unmanaged code, so you have the ability to control memory explicitly. (Did I mention that I love C#?)
Going back to the original question, why would you want to go into this trouble? As we just saw, if you are using classes (which you are likely to default to), you already benefit from the automatic layout of fields in memory. If you are using structs, you can enable LayoutKind.Auto to get the same behavior.
This technique is for the 1% of the cases where that is not sufficient, when you can see that your memory usage is high and you can benefit greatly from manually doing something about it.
That leads to the follow-up question, if we go about implementing this, what is the overhead over time? If I want to add a new field to an optimized struct, I need to be able to understand how it is laid out in memory, etc.
Like any optimization, you need to maintain that. Here is a recent example from RavenDB.
In this case, we used to have an optimization that had a meaningful impact. The .NET code changed, and the optimization now no longer makes sense, so we reverted that to get even better perf.
At those levels, you don’t get to rest on your laurels. You have to keep checking your assumptions.
If you got to the point where you are manually optimizing memory layouts for better performance, there are two options:
- You are doing that for fun, no meaningful impact on your system over time if this degrades.
- There is an actual need for this, so you’ll need to invest the effort in regular maintenance.
You can make that easier by adding tests to verify those assumptions. For example, verifying the amount of padding in structs match expectation. A simple test that would verify the size of a struct would mean that any changes to that are explicit. You’ll need to modify the test as well, and presumably that is easier to catch / review / figure out than just adding a field and not noticing the impact.
In short, this isn’t a generally applicable pattern. This is a technique that is meant to be applied in case of true need, where you’ll happily accept the additional maintenance overhead for better performance and reduced resource usage.