Search code examples
c#garbage-collectionmonogame

Optimizing C# memory allocation


I ran into a rather peculiar thing today when trying to optimize. I use the Monogame framework and want to create textures on the fly. I use .NET 6 in Visual Studio Community 2022.

So this is the straightforward code for doing this, which is working fine:

    public static Texture2D CreateTexture(GraphicsDevice graphicsDevice, int width, int height)
    {
        Texture2D tex = new(graphicsDevice, width, height, true, SurfaceFormat.Color);
        Color color = new((byte)255, (byte)255, (byte)255, (byte)255);
        int length = width * height;
        Color[] colorArray = new Color[length];

        for (int i = 0; i < length; i++)
        {
            color.A = (byte)randomizer.Next(0, 256);
            colorArray[i] = color;
        }

        tex.SetData<Color>(colorArray);

        return tex;

    }

Since I call this method many times, usually with the same width and height, I thought I could do some memory optimizations, and ended up with the similar code:

    static int texW = 1;
    static int texH = 1;
    static Color[] colorArray = new Color[1];

    public static Texture2D CreateTexture2(GraphicsDevice graphicsDevice, int width, int height)
    {
        Texture2D tex = new(graphicsDevice, width, height, true, SurfaceFormat.Color);
        int length = width * height;

        if (texW != width || texH != height)
        {
            texW = width;
            texH = height;
            colorArray = new Color[length];
        }

        for (int i = 0; i < length; i++)
        {
            colorArray[i].A = (byte)randomizer.Next(0, 256);
            colorArray[i].R = 255;
            colorArray[i].G = 255;
            colorArray[i].B = 255;
        }

        tex.SetData<Color>(colorArray);

        return tex;
    }

As you see I try to avoid allocating the colorArray each time the function is called. Since the array is a list of Color-struct-objects I assumed I should replace the R,G,B and A values in each struct object with new rather than replacing the entire struct.

For testing it out I tried to call these two functions a few thousand times a second for about thirty seconds each, in separate debug sessions.

I expected my changes to improve various memory-related things, but no! Instead, my second example really upset the garbage collector which started to do frequent GCs, visual studios 'Diagnostic Tools' show about twice as many allocated objects and the framerate seems to drop as well.

Does anyone have a clue what's going on here? My second code example allocates the colorArray one time, as expected when I call it thousands of times with the same width and height. Also, assigning a value to the R,G,B and A members, which are of byte-type should not cause any memory allocations.


EDIT:

As Jeremy pointed out in the comments, it was Texture2D that was the culprit. I did not call Dispose() on the texture after drawing it in my test code.

Now when I call Dispose() the code works as expected. But I am a bit surprised about the small difference between the two pieces of code; Why is the first piece of code not upsetting the GC more than it does? In my simple test, I see no real difference.


EDIT 2:

Just to complete the case, I updated my code to use tex.SetData() (thanx Strom!) instead of creating a new Texture2D and then Dispose() each loop. Better results. But! I still get the peculiar difference where the framerate is better if I don't use a static colorArray. Without looking at the compiled code (intermediate language?) my only guess is that the compiler makes better optimizations in the code than I can do...


Solution

  • Separate your Color data array from the Texture2D.

    The Texture2D maintains memory links to the video card and should not be created every step. The other side of this statement is that Texture2D's should not be destroyed within the program outside of UnLoadContent or the class destructor.

    Create the texture once:

    // Class level variable
    Texture2D tex;
    
    // in Initialize()
    // replace width and heigh with actual values.
    tex = new(graphicsDevice, width, height, true, SurfaceFormat.Color);
    

    Your original code had no benefit over the later one, I did modify it to save a memory read per loop by generating a new Color struct each loop. Changing any member of a struct creates a new struct(value type semanitics).

    The only improvement would be to pin the array in an unsafe context to eliminate the the two memory reads for offset and bounds checks on each access, but that is a different question.

    public static Color[] CreateTexture(int width, int height)
        {
            int length = width * height;
            Color[] colorArray = new Color[length];
    
            for (int i = 0; i < length; i++)
            {
                color[i] = New Color((byte)255, (byte)255, (byte)255, (byte)randomizer.Next(0, 256));
            }
            return colorArray;
        }
    

    Then during Update:

           tex.SetData<Color>(CreateTexture(tex.Width,tex.Height));