C#/XNA巨大的内存泄漏问题

3
这是我的第一篇帖子。 我正在使用XNA在Visual Studio 2010中制作游戏,但遇到了一个大内存泄漏问题。游戏开始时使用17k RAM,十分钟后达到65k。我运行了一些内存分析工具,它们都说正在创建String对象的新实例,但它们没有被使用。String的活动实例数量根本没有改变。它还创建了Char[](这是我期望的),Object[]和StringBuilder的实例。我的游戏还很新,但代码量太大,无法在此贴出。我不知道如何除去没有被使用的实例,请帮忙!

5
我不确定是否应该将65k视为“巨大的内存泄漏”。即使是30年前的个人计算机也可以容纳这么多。如果内存溢出到650MB,我可能会有点担心。 - Andy West
我猜你指的是最大65,000K,因为我认为现在甚至已经不能将托管进程大小缩小到17K了。即使是这样,按现代标准,65M大小也不算太大。 - Dan Bryant
好的,如果没问题的话,我就让它自己解决了。它偶尔会重置。非常感谢!是的,我是指65,000k哈哈。 - Kirbyfanner
2个回答

5

您没有提供足够的信息,因此我只能做出一个有根据的猜测。这是我的猜测:

如果您在Draw方法中执行以下操作:

spriteBatch.DrawString(font, "Score: " + score, location, Color.Black);
spriteBatch.DrawString(font, "Something else: " + foo, overHere, Color.Black);
spriteBatch.DrawString(font, "And also: " + bar, overThere, Color.Black);

然后,每次运行这些调用时,它们都会在你背后创建新的stringStringBuilder对象。因为它们位于你的Draw方法中,所以每个方法可能每秒运行60次。这将分配大量临时对象!
要验证这一点,请使用CLR Profiler。听起来你已经这样做了。
虽然这并不是真正的“泄漏”——垃圾回收器最终会清理它们——但这种分配模式在游戏中是不可取的。请参阅此博客文章,了解处理游戏中的垃圾收集的两种方法。方法1通常更容易、效果更好,因此我在这里讨论它。
值得一提的是,在PC上,GC足够快,像这样的分配实际上并不重要。GC将使用非常少的开销清理小对象(如临时字符串)。
另一方面,在Xbox 360上,即使定期产生这样微小的垃圾也会导致一些严重的性能问题。(我不确定WP7是否也是如此,但我个人会像对待Xbox一样对待它——小心!)
我们如何解决这个问题?
答案很简单:DrawString将在string的位置接受StringBuilder的实例。创建一个StringBuilder实例,然后在每次需要组合自定义字符串时重用它。
要注意的是,将数字或其他对象隐式或通过其ToString()方法转换为字符串也会导致分配。因此,您可能需要编写自己的自定义代码,在不引起分配的情况下附加到StringBuilder
以下是我使用的一个扩展方法,用于将整数附加到字符串而不进行分配:
public static class StringBuilderExtensions
{
    // 11 characters will fit -4294967296
    static char[] numberBuffer = new char[11];

    /// <summary>Append an integer without generating any garbage.</summary>
    public static StringBuilder AppendNumber(this StringBuilder sb, Int32 number)
    {
        bool negative = (number < 0);
        if(negative)
            number = -number;

        int i = numberBuffer.Length;
        do
        {
            numberBuffer[--i] = (char)('0' + (number % 10));
            number /= 10;
        }
        while(number > 0);

        if(negative)
            numberBuffer[--i] = '-';

        sb.Append(numberBuffer, i, numberBuffer.Length - i);

        return sb;
    }
}

从技术上讲,您不需要numberBuffer;只需一次附加1个字符(由于减少缓存命中而更快)。如果您担心调整大小,请使用sb.EnsureCapacity(sb.Length + 16)来防止它(16保留缓存行对齐)。 - GGulati
哇,这太有道理了。我会尽快尝试的,非常感谢! - Kirbyfanner
@GGulati,您说得没错,调用字符上的Append比在缓冲区中构建字符串并将其放置在原处要快一些,但请注意算法是从右到左构建字符串的。我会留下一个练习来解决从左到右的算法;) 此外:不必担心EnsureCapacity。这里的整个重点是重复使用缓冲区。因此,容量增加只会发生一次。而且CPU时间差异非常小。这是一种特定的GC优化。在其上添加不必要的额外优化是不好的做法。 - Andrew Russell

2
在C#中没有内存泄漏问题(或者说,它们非常难以出现)。你所经历的现象是正常的。垃圾回收器不会“感觉”需要回收内存,因此就不会回收。当内存不足时,垃圾回收才会进行。如果你确信没有保留不需要的string引用,那么一切都很好。
如果想强制进行垃圾回收,请使用GC.Collect()

6
在游戏中使用GC.Collect不是一个好主意,最好在游戏循环内使用对象池,并避免动态分配。 - Daniel Little
XNA的某些部分不是托管代码,因此GC将无法工作。就像这样:https://dev59.com/Uk_Ta4cB1Zd3GeqPAmsS - Steven Du

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接