Optimizing C# memory allocation

huangapple go评论66阅读模式
英文:

Optimizing C# memory allocation

问题

以下是翻译好的内容,已删除代码部分:

今天在尝试优化时,我遇到了一个相当奇怪的问题。我使用Monogame框架,并希望动态创建纹理。我在Visual Studio Community 2022中使用.NET 6。

因此,以下是执行此操作的直接代码,它正常工作:

我多次调用此方法,通常使用相同的宽度和高度,所以我认为我可以进行一些内存优化,并最终得到了类似的代码:

如您所见,我尝试避免在每次调用函数时分配colorArray。由于该数组是Color结构对象的列表,我认为应该替换每个结构对象中的R、G、B和A值,而不是替换整个结构。

为了测试它,我尝试在分开的调试会话中每秒多次调用这两个函数大约三十秒。我期望我的更改会改善各种与内存相关的事情,但事实并非如此!相反,我的第二个示例确实让垃圾收集器感到困扰,导致频繁的垃圾回收,Visual Studio的“诊断工具”显示分配的对象数量大约是前者的两倍,帧速率似乎也下降了。

有人了解这里发生了什么吗?我的第二个代码示例只分配了一次colorArray,当我使用相同的宽度和高度数千次调用它时,这是预期的行为。此外,为字节类型的R、G、B和A成员分配值不应导致任何内存分配。

编辑:

正如Jeremy在评论中指出的,是Texture2D是罪魁祸首。在我的测试代码中,在绘制后我没有调用Dispose()

现在,当我调用Dispose()时,代码按预期工作。但我对这两段代码之间的微小差异感到有些惊讶;为什么第一段代码不像它实际做的那样更加惹恼垃圾收集器?在我的简单测试中,我看不出真正的区别。

编辑2:

为了完成此案例,我更新了我的代码,使用tex.SetData()(感谢Strom!)而不是每次循环都创建新的Texture2D,然后进行Dispose()。效果更好。但是!我仍然遇到了一个奇怪的差异,如果我不使用静态的colorArray,帧速率会更好。没有查看已编译的代码(中间语言?),我的唯一猜测是编译器在代码中进行了更好的优化。

英文:

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...

答案1

得分: 1

从Texture2D中分离出你的颜色数据数组。

Texture2D会保持与显卡的内存链接,不应该在每一步中都创建。另一方面,Texture2D不应该在程序中的UnLoadContent或类析构函数之外被销毁。

只需创建一次纹理:

// 类级别变量
Texture2D tex;

// 在Initialize()中
// 用实际值替换width和height。
tex = new(graphicsDevice, width, height, true, SurfaceFormat.Color);

你的原始代码与后面的代码相比没有任何优势,我修改了它以在每次循环中生成一个新的Color结构以减少内存读取。更改结构的任何成员都会创建一个新的结构(值类型语义)。

唯一的改进是在不安全的上下文中对数组进行固定以消除每次访问时的偏移和边界检查的两次内存读取,但这是一个不同的问题。

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;
}

然后在Update中:

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

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 &lt; length; i++)
        {
            color[i] = New Color((byte)255, (byte)255, (byte)255, (byte)randomizer.Next(0, 256));
        }
        return colorArray;
    }

Then during Update:

       tex.SetData&lt;Color&gt;(CreateTexture(tex.Width,tex.Height));

huangapple
  • 本文由 发表于 2023年2月27日 07:22:01
  • 转载请务必保留本文链接:https://go.coder-hub.com/75575616.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定