【问题标题】:How to handle many updating objects efficiently in C#?如何在 C# 中有效地处理许多更新对象?
【发布时间】:2010-02-25 16:56:22
【问题描述】:

我正在使用 C# 和 XNA 开发一款 2D 高空射击游戏。我有一个名为“bullet”的类,需要每分每秒更新其中的许多实例。

我的第一种方法是创建一个通用的项目符号列表,然后根据需要简单地删除和添加新项目符号。但这样做时 GC 经常启动,我的游戏有一些周期性的生涩延迟。 (剪掉了很多代码,只是想展示一个简单的sn-p)

if (triggerButton)
{
    bullets.Add(new bullet());
}
if (bulletDestroyed)
{
    bullets.Remove(bullet);
}

我的第二个也是当前的尝试是拥有一个单独的通用子弹堆栈,当我用完一个子弹时我推送它,当我需要一个新的子弹时弹出一个子弹,如果堆栈中有任何东西。如果堆栈中没有任何内容,那么我将一个新项目符号添加到列表中。 似乎可以减少生涩的延迟,但话说回来,有时仍然会出现一些生涩的延迟(尽管我不知道这是否相关)。

if (triggerButton)
{
    if (bulletStack.Count > 0)
    {
        bullet temp = bulletStack.Pop();
        temp.resetPosition();
        bullets.Add(temp);
    }
    else
    {
        bullets.Add(new bullet());
    }
}
if (bulletDestroyed)
{
    bulletStack.Push(bullet);
    bullets.Remove(bullet);
}

所以,我知道过早的优化是万恶之源,但这是我可以及早发现的非常明显的低效率(这是在不必担心敌人的子弹填满屏幕之前)。所以我的问题是:将未使用的对象推送到堆栈会调用垃圾收集吗?引用会保持活动状态还是对象仍在被破坏?有没有更好的方法来处理更新许多不同的对象?例如,我是不是太花哨了?只遍历列表并以这种方式找到未使用的项目符号会很好吗?

【问题讨论】:

  • .Net 约定是 PascalCase 类名,也就是说,让类名中的每个“单词”都以大写字母开头。在您的情况下,将 bullet 重命名为 Bullet。不过,String、Int32 和其他所谓的基本类型都有小写等价物,例如。 string 和 int,但这是 C# 特定的,不会暴露在外部。

标签: c# garbage-collection xna


【解决方案1】:

这里有很多问题,而且很难说。

首先,bullet 是结构体还是类?如果子弹是一个类,任何时候你构造一个类,然后取消它的根(让它超出范围或将其设置为空),你将添加 GC 需要收集的东西。

如果您要制作其中的许多,并在每一帧更新它们,您可能需要考虑使用 List<bullet>bullet 作为结构,并预先分配列表(使用大小足以容纳所有子弹,因此不会像您调用 List.Add 那样重新创建它)。这将极大地缓解 GC 压力。

还有,只是因为我需要咆哮:

所以,我知道过早的优化是万恶之源,但这是非常明显的低效率

永远不要害怕优化您知道会导致问题的例程。如果您发现性能问题(即:您的滞后),这不再是过早的优化。是的,您不想优化每一行代码,但您确实需要优化代码,尤其是当您看到真正的性能问题时。发现问题后立即对其进行优化比以后尝试优化要容易得多,因为在您添加许多使用 bullet 类的其他代码之前,所需的任何设计更改都将更容易实现。

【讨论】:

  • 感谢您的回复。是的,这是一堂课。它需要是一个类,因为它是从另一个类继承的。你是说我目前这样做的方式仍然会调用垃圾收集吗?我不知道在调用 Add 时会重新创建 List,我认为它只是添加了对另一个对象的新引用...
  • 结构有很多细微差别要考虑,但只要他保持结构小(成员少),这可能是一个不错的选择。此外,List.RemoveAll 在多次调用List.Remove 时提供了改进的性能(O(N) 而不是 O(MN),其中 N 是列表中的项目数,M 是已删除项目的数量)。
  • 我同意有时需要内联优化(即,现在而不是以后再做并不总是为时过早)。但是,我强调您需要确保在进行更改之前诊断出实际问题。过早的优化不是关于什么时候做某事,而是更多关于如果你应该这样做。
  • @Bob:不幸的是,对于游戏,有时您需要为优化目的做出一些牺牲,因为游戏(与商业应用程序不同)确实需要良好的性能。特征。还有其他选项 - 例如不从列表中删除项目符号,而只是停用它们,并且每隔几帧重新创建列表等。否则,大量相对较小的类可能会导致相当多的 GC 压力游戏...
  • @Bryan:这就是为什么我加入了粗体线“你知道会导致问题”——一旦你知道确实存在问题,就不再是过早的选择。在我看来,而是一些问题,并且在某些时候需要解决。如果您现在正在处理代码,那么您现在的优化效率会更高。
【解决方案2】:

您可能会发现flyweight design pattern 很有用。只需要一个子弹对象,但多个flyweights可以为其指定不同的位置和速度。享元可以存储在预先分配的数组中(比如 100)并标记为活动或不活动。

这应该完全消除垃圾收集,并且可能会减少跟踪每个子弹的可塑性属性所需的空间。

【讨论】:

  • 虽然,我怀疑子弹可能只不过是一个位置、速度和质量(或类型枚举)——在这种情况下,蝇量级可能和原来的一样“重量级”对象...
  • @[Reed Copsey]:完全有可能。但话又说回来,它们可能还有纹理贴图、颜色、声音效果等。
【解决方案3】:

我承认我本身没有任何经验,但我会考虑使用传统数组。将数组初始化为大于您需要的大小,并且将是理论上的最大项目符号数,例如 100。然后从 0 开始在数组的开头分配项目符号,将最后一个元素保留为空。因此,如果您有四个活动项目符号,您的数组将如下所示:

0 乙 1乙 2乙 3乙 4 空 ... 99 空

这样做的好处是数组总是被分配的,因此你不需要处理更复杂的数据结构的开销。这实际上与字符串的工作方式非常相似,因为它们实际上是带有空终止符的 char[]。

可能值得一试。一个缺点是,您必须在移除子弹时进行一些手动操作,可能会将子弹之后的所有东西移到一个插槽中。但是此时您只是在移动指针,所以我认为它不会像分配内存或 GC 那样有很高的惩罚。

【讨论】:

  • 您也可以使用 List 来做到这一点,并且使用语义更清晰。只需使用 new List(initialSize) 进行分配,它会在内部预先分配一个数组,但也允许它根据需要增长。
  • 这很好。数据结构的内存开销会更大,但使用 List 会获得更强的可读性。由于内存不是优化的对象,因此 List 可能是更好的选择。
【解决方案4】:

您认为通过将未使用的子弹放在堆栈中可以防止它们被垃圾收集是正确的。

关于延迟的原因,您是否尝试过任何分析工具?只是为了找出问题所在。

【讨论】:

  • 谢谢,我还没试过分析工具。你有什么建议?我正在使用 Visual C# Express
  • 我通常不使用任何工具,所以我不能推荐任何工具。 (我敢肯定这里有人可以:))我通常只使用带有时间戳/时间跨度的 Console.Writelines 来确定代码在哪里陷入困境。
  • 一些流行的分析工具是 Ants 和 dotTrace。它们都需要花钱,但您可能可以为这个案例进行 30 天试用:red-gate.com/products/ants_performance_profiler/index.htmjetbrains.com/profiler/index.html
【解决方案5】:

您的基于堆栈的解决方案非常接近我编写的通常执行此类资源池的类:
http://codecube.net/2010/01/xna-resource-pool/

您提到这使问题基本上消失了,但它仍然到处出现。正在发生的事情是,使用这种基于堆栈/队列的池化方法,一旦您不再请求比池所能提供的更多新对象,系统将达到稳定点。但是,如果请求数高于您之前的最大请求项目数,则将导致您必须创建一个新实例来为请求提供服务(因此不时调用 GC)。

您可以避免这种情况的一种方法是检查并预先分配您认为在高峰期可能需要的尽可能多的实例。这样,您将不会有任何新分配(至少来自池对象),并且不会触发 GC :-)

【讨论】:

    【解决方案6】:

    List 实际上有一个内置的容量来防止每次添加/删除的分配。一旦超过容量,它就会增加更多(我想我每次都会加倍)。问题可能更多的是删除而不是添加。 Add 只会在按大小跟踪的第一个开放点上掉下来。要删除,必须压缩列表以填充现在的空槽。如果您总是在列表的前面删除,那么每个元素都需要向下滑动。

    堆栈仍然使用数组作为其内部存储机制。所以你仍然受到数组的添加/删除属性的约束。

    要使数组正常工作,您需要创建所有项目符号,并为每个项目符号设置一个 Active 属性。当您需要一个新的项目符号时,将 Active 标志填充为 true 并设置所有新项目符号属性。完成后,将 Active 标志翻转为 false。

    如果您想尝试消除每次重绘迭代列表的需要(这可能非常大,具体取决于您要允许的内容),您可以尝试在数组中实现双链表。当需要新的子弹时,向数组询问第一个可用的免费条目。转到最后一个活动项目符号(变量)并将新项目符号数组位置添加到其下一个活动项目符号属性中。当需要删除它时,转到上一个项目符号并将其活动项目符号属性更改为已删除的下一个活动项目。

    //I am using public fields for demonstration.  You will want to make them properties
    public class Bullet {
      public bool Active;
      public int thisPosition;
      public int PrevBullet = -1;
      public int NextBullet = -1;
      public List<Bullet> list;
    
      public void Activate(Bullet lastBullet) {
        this.Active = true;
        this.PrevBullet = lastBullet.thisPosition;
        list[this.PrevBullet].NextBullet = this.thisPosition;
      }
    
      public void Deactivate() {
        this.Active = false;
        list[PrevBullet].NextBullet = this.NextBullet;
        list[NextBullet].PrevBullet= this.PrevBullet;
      }
    }
    

    这样,您就有了一个包含所有需要的子弹的预构建数组,但无论它们在数组中的位置如何,油漆都只会击中处于活动状态的子弹。您只需要维护指向第一个活动项目符号的链接即可开始绘制,并维护最后一个活动项目符号以了解列表从何处重新开始。

    现在您只关心保存整个列表的内存,而不是 GC 何时清理。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-05
      • 1970-01-01
      • 2014-05-23
      • 1970-01-01
      • 2020-07-18
      • 2022-11-24
      相关资源
      最近更新 更多