Meet the readonly ref struct – Part II

Introduction

In part I of this series, we familiarize ourselves with the program code through a small analysis as well as performance measurement. Now we will try to implement possible improvements aimed at better utilize memory resources.

Improvements

Before we continue to implement the possible improvements, we will try to better understand the metrics extracted from the benckmark by observing the following points:

  • 1 µs (microsecond) = 1 second ÷ 1 000 000 = 0.000001 seconds;
  • The Gen X/1k Op column contains the number of Gen X collections per 1 000 operations. If the value is equal 1, then it means that GC collects memory once per one thousand of benchmark invocations in generation X;
  • - in the Gen column means that no garbage collection was performed;
  • The Allocated Memory/Op column contains only the size of the allocated managed memory. That is, the extra overhead required to instantiate each type. Think of it as the extra objects we set aside when we weigh ourselves in the gym. For more info on extra overhead I recommend this great article from Konrad Kokosa;
  • - in the Allocated column means that no managed memory was allocated;
  • CLR does some aligning. If you try to allocate new byte[7] array, it will allocate byte[8] array;

Our goal is to improve this program by keeping the code readable so that it does not look like low level code, making it easier for programmers of any level to maintain it. Also, we’re not going to talk about the difference between memory management types or do an intermediate language (IL) analysis. Let’s try to keep in mind that we want to focus only on the benefits of using struct rather than using class .

Speaking of struct , the first improvement we are going to do is to create a version of our Dto with struct . The new features in C# 7.2 allow you to write safer code with better performance. When used wisely, these new techniques can minimize value types copying operations as well as allocations and pressure in the GC with fewer collections.

Note: As we will be using features added in C # 7.2, you must configure your project to use C # 7.2 or later. For more information about how to set the language version, see: Select the C# language version

This is how the DrawingDtoStruct  is defined in Listing 1. We have changed the signature of the class to readonly ref struct . This way the compiler will not attempt to create defensive copies of the struct . Adding the modifier  readonly we are required to make the struct immutable, so all it’s member fields and properties must be made read only as well. Then by adding the modifier ref we can change the Color  property to a ReadOnlySpan<byte>  which is also a readonly ref struct by definition. If you expose a method that accepts a ReadOnlySpan<T>, then both Span<T> and a ReadOnlySpan<T> can be passed to it.

Listing 1. DrawingDtoStruct definition.

To minimize allocations and copy operations wisely you need to look at the following resource management techniques:

  • Declare a readonly struct to express that a type is immutable and enables the compiler to save copies when using in  parameters.
  • Use a ref readonly return when the return value is a struct larger than IntPtr.Size and the storage lifetime is greater than the method returning the value.
  • When the size of a readonly struct is bigger than IntPtr.Size , you should pass it as an in parameter for performance reasons. For example, IntPtr.Size is 8 bytes, an int is 4 bytes, so if your struct has more than two int properties it will be larger than IntPtr.Size . Thus, almost all your struct will be larger than the IntPtr.Size and you will want to use the in parameter most times.
  • Never pass a struct as an in parameter unless it’s declared with the readonly  modifier because it may negatively affect performance and could lead to an obscure behavior.
  • Use a ref struct , or a readonly ref struct such as Span<T> or ReadOnlySpan<T>  to work with memory as a sequence of bytes.

And now this is how the Draw method looks like in Listing 2. No major changes. Special attention for the in parameter before the readonly struct  type.

Listing 2. Draw method refactored.

The service has also been changed as in Listing 3 below.

Listing 3. DrawingServiceStruct refactored.

After making these changes it’s necessary to change the way the colors are defined according to the new type DrawingDtoStruct . It wouldn’t be necessary to use stackalloc here (see Listing 4), but I’m just using it to demonstrate that it’s possible to do this in a safe context with the type Span<T>. Keep in mind that it’s not a good practice to work with this memory management type when you are dealing with very large objects and/or objects that has long storage lifetime. stackalloc resources don’t await for the GC (No GC involved at all) and immediately goes away with the context. 

Listing 4. Drawing image blocks.

After these minor changes, it’s time for more measurements, see Listing 5. This new version of program using struct took an average of 0.73% of the time of the version with  class to process and did not even allocate in managed memory (no overhead!). All right, we’re talking about microseconds and this is less than the blink of an eye. In addition, this program is very small in both code and data. But even so we completed our goal of improving the program by taking advantage of the use of struct rather than class , while still keeping the code readable.

Listing 5. Final result.

Note: If what you really want is speed, you may want to use parallel and/or asynchronous programming. It would take less than half the time to process compared to the version with class. But that could result in a huge waste of resources on such a small program.

And lastly, let’s see how the Save  method is as shown in Listing 6 just below.

Listing 6. Save method.

I wanted to talk about the Save method only at the end because it is outside the scope of the purpose of this post and so was not considered in our benchmark.  The Save method is unsafe , it takes our image buffer and creates a bitmap saving the generated image to your desktop. If you want to consider this method in your benchmark just be careful because it can generate thousands of images on your desktop and can crash your computer. And, if you intend to run this program on your computer, make sure to enable unsafe code execution in your *.csproj  file in a similar way to this:

Listing 7. Allowing unsafe blocks.

Summary

I know most C# programmers have used lifelong class in their implementations and have not even thought of struct . But after C# 7.2, it’s worth getting to know that there are struct. That’s why I decided to write this post, to get your attention to start trying to think a little differently. You’re not going to go out there by simply changing all your class to struct but when you are implementing a high-performance system, you will remember that you have new tools to make things easier. So, isolate parts of your implementations and take measurements on different machines. Only in this way will you be sure of the benefits it can bring (or not).

Thanks! 🙂

Sources (the most relevant of course)

Last edited on Sep, 22, 2019 at 6:47 pm