Meet the readonly ref struct – Part I

Introduction

Okay, as a .Net programmer, you’ll probably say that talking about memory management is a joke because the .Net Framework already has the Garbage Collector (GC) that does this dirty work for you.

Day after day, computer hardware is cheaper than ever. People can buy a wide variety of powerful devices without a lot of money and play up 3D games on a 4k screen in the palm of their hands with no performance issues.

So why should we care about memory management?

In addition, in the world of cloud computing, we can easily scale virtually any type of application at a mouse click away…

So this is the point. Before we talk about scalability, we must take care of the code we write. If you can scale an application with a small problem, this problem will be escalated together and will become a VERY big problem.

TL;DR;

There are several memory management techniques out there. I just started reading a book called Pro. Net Memory Management of over 1 000 pages on this from Konrad Kokosa, which I highly recommend. But in this post, we’ll briefly talk about struct or, more specifically, the readonly ref struct  feature introduced in C# 7.2 to help you more easily avoid memory allocations and hence pressure on the GC.

To help us understand the concepts presented here, I wrote a small program which draws the Windows® logo from a byte array as shown below only for illustrative and allegorical purposes.

Figure 1. Final result

You can clone the source code from my GitHub account if you want.

Show me the code!

Let’s start by looking at a code that was written using only class (reference types). I think most .Net programmers would start with something similar because class also deal fairly with most solutions. And if you’re not sure, just go with class . It just works!

The program begins by defining some colors that will be used to assemble our drawing, as shown in Listing 1 below.

Listing 1. Defining some colors.

We use this class  in order to represent two things:

  • The YStart , YEnd , XStart and XEnd properties represent squares.
  • The colors are being passed as byte[]  in RGB (Red, Green, Blue).

The values of each color can be set to integers from 0 to 255, but I decided to represent those colors in hexadecimal values. So I wrote a small program that served me with a color table as shown in Listing 2.

Listing 2. Hexadecimal color table.

And this is how the DrawingDtoClass  is defined, quite obvious:

Listing 3. DrawingDtoClass definition.

Then we call the Draw method from the DrawingServiceClass class. It only makes two for loops by plotting the colors in their given positions. So let’s see what the Draw  method does in the Listing 4.

Listing 4. Draw method implementation.

And you may be wondering what the PlotPixel method does. Well, it just assigns the values of the colors that were passed by parameter in their given positions in the array, thus filling our ImageBuffer .

Listing 5. PlotPixel method implementation.

As you can see, there is nothing special here. We just set an offset so that the colors are positioned correctly in the array, because since this image will have 32 bits per pixel later, the colors will be defined in the ARGB color space.

What have you found so far? It sounds like a very trivial program that has no rocket science. So let’s measure this program and analyze how and if we can improve it.

Note: Let’s talk about the Save method in the end because it should be disregarded. Therefore, it will be omitted from our benchmark metrics.

Measure, measure, measure…

Now that we have familiarized ourselves with the code and already know what the program does (or tries to do), let’s measure it!

Note: All metrics are being made with BenchmarkDotNet which is a powerful .NET library for benchmarking. It is worth remembering however that the metrics may vary slightly depending on the configuration of the computer where the program is being measured.

Here’s my setting for all these metrics on Listing 6:

Listing 6. Metrics settings.

As you can see in the benchmark result below in Listing 7, and some information has been omitted for brevity, this program does not seem too bad in terms of performance or memory consumption. It takes on average just over a second to complete its execution and although it doesn’t give any extra work to the GC, it represents the cost of 400 B per operation. But, yey, this is less than half a kilobyte (1 kB = 1 024 bytes). Not bad!

Listing 7. Metrics settings.

Note: In decimal systems, kilo stands for 1,000, but in binary systems, a kilo is 1,024 (2 to the 10th power). Technically, therefore, a kilobyte is 1,024 bytes, but it is often used loosely as a synonym for 1,000 bytes. In computer literature, kilobyte is usually abbreviated as K or KB. To distinguish between a decimal K (1,000) and a binary K (1,024), the IEEE has suggested following the convention of using a small k for a decimal kilo and a capital K for a binary kilo, but this convention is by no means strictly followed. Source: https://www.webopedia.com/TERM/K/kilobyte.html

But as I said before, this program is just to illustrate the way we think. So, let’s pretend it’s a program that being scaled in the cloud is causing a bottleneck and we have to solve this “performance problem.”

Let’s see how the improvements were applied in part II of this series

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