允许迭代而不产生任何垃圾

我在实现IEnumerable接口的对象池中有以下代码。

public IEnumerable ActiveNodes { get { for (int i = 0; i < _pool.Count; i++) { if (_pool[i].AvailableInPool) { yield return _pool[i]; } } } } 

据我所知(根据这个问题),这将产生垃圾,因为需要收集IEnumerable对象。 _pool中的所有元素都不会被收集,因为池的目的是保持对所有元素的引用以防止垃圾创建。

任何人都可以建议一种允许迭代_pool的方法,以便不生成垃圾?

在池上迭代时,池中具有AvailableInPool == true所有项都应该迭代。 订单无关紧要。

迭代项将在任何“正常”设计中始终导致创建新的可枚举对象。 创建和处理对象非常快,因此只有在非常特殊的情况下(低延迟是最重要的优先事项)垃圾收集才能(我说’可能’)成为一个问题。

通过返回实现IEnumerable结构, 可以实现没有垃圾的设计。 C#编译器仍然可以迭代这些对象,因为foreach语句使用duck typing。 但同样,这不太可能对你有所帮助。

UPDATE

我相信有更好的方法可以提高你的应用程序的性能,而不是产生这种低级别的技巧,但这是你的号召。 这是一个struct enumerable和struct枚举器。 当您返回可枚举时,C#编译器可以预告它:

 public struct StructEnumerable { private readonly List pool; public StructEnumerable(List pool) { this.pool = pool; } public StructEnumerator GetEnumerator() { return new StructEnumerator(this.pool); } } 

这是StructEnumerator

 public struct StructEnumerator { private readonly List pool; private int index; public StructEnumerator(List pool) { this.pool = pool; this.index = 0; } public T Current { get { if (this.pool == null || this.index == 0) throw new InvalidOperationException(); return this.pool[this.index - 1]; } } public bool MoveNext() { this.index++; return this.pool != null && this.pool.Count >= this.index; } public void Reset() { this.index = 0; } } 

您只需返回StructEnumerable ,如下所示:

 public StructEnumerable Items { get { return new StructEnumerable(this.pool); } } 

并且C#可以使用正常的foreach迭代:

 foreach (var item in pool.Items) { Console.WriteLine(item); } 

请注意,您不能LINQ超过该项目。 你需要IEnumerable接口,这涉及拳击和垃圾收集。

祝好运。

更新:

请注意,在数组和List使用foreach ,不会生成垃圾。 在数组上使用foreach时,C#会将操作转换为for语句,而List已经实现了一个struct枚举器,导致foreach不产生垃圾。

首先,一些人正在推翻奥尔霍夫斯基,暗示这一点都不用担心。 在某些环境中的某些应用中,避免收集压力实际上非常重要。

紧凑的框架垃圾收集器有一个简单的策略; 每次分配1000KB内存时,它会触发一个集合。 现在假设你正在编写一个在紧凑框架上运行的游戏,物理引擎每次运行时都会产生1KB的垃圾。 物理引擎通常每秒运行20次。 所以这是每分钟1200KB的压力,嘿,这已经超过了物理引擎每分钟一次的收集。 如果该集合在游戏中引起明显的口吃,那么这可能是不可接受的。 在这种情况下,您可以采取的任何措施来减少收集压力。

即使我在桌面CLR上工作,我也是在努力学习。 我们在编译器中有一些场景,我们必须避免收集压力,我们正在跳过所有类型的对象池箍来实现这一点。 奥尔霍夫斯基,我感受到你的痛苦。

那么,为了解决您的问题,如何在不产生收集压力的情况下迭代池化对象的集合?

首先,让我们考虑一下典型场景中为什么会出现收集压力。 假设你有

  foreach(var node in ActiveNodes) { ... } 

从逻辑上讲,这会分配两个对象。 首先,它分配可枚举 – 序列 – 表示节点序列。 其次,它分配枚举器 – 光标 – 表示序列中的当前位置。

在实践中,有时你可以作弊,并有一个对象代表序列和枚举器,但你仍然有一个对象分配。

我们怎样才能避免这种收集压力? 想到三件事。

1)首先不要制作ActiveNodes方法。 使调用者按索引遍历池,并检查自己该节点是否可用。 然后序列就是已经分配的池,并且游标是一个整数,它们都没有创建新的收集压力。 您支付的价格是重复的代码。

2)正如史蒂文所建议的那样,编译器将采用任何具有正确公共方法和属性的类型; 它们不必是IEnumerable和IEnumerator。 您可以创建自己的可变结构序列和游标对象,按值传递它们,并避免收集压力。 拥有可变结构是危险的,但它是可能的。 请注意, List将此策略用于其枚举器; 研究其实施的想法。

3)正常地在堆上分配序列和枚举器并将它们集中在一起! 您已经采用了池策略,因此没有理由不能集合枚举器。 枚举器甚至有一个方便的“重置”方法,通常只会引发exception,但您可以编写一个自定义枚举器对象,使用它将枚举器重新返回到序列的开头,当它返回池中时。

大多数对象一次只枚举一次,因此在典型情况下池可能很小。

(现在,你当然可能会遇到鸡和鸡蛋的问题;你如何列举普查员库?)

因为XBox的XNA也可以在Compact Framework上运行(我怀疑你正在使用的是你给出的提示(1)),我们可以相信XNA开发人员可以告诉我们foreach何时创建垃圾。

引用最相关的观点(虽然整篇文章值得一读):

在对Collection进行foreach时,将分配一个枚举器。

在对大多数其他集合执行foreach时,包括数组,列表,队列,链接列表等:

  • 如果明确使用集合,则不会分配枚举器。
  • 如果它们被转换为接口,则将分配枚举器。

因此,如果_pool是一个List ,数组或类似的并且可以负担得起,您可以直接返回该类型或将IEnumerable转换为相应的类型以避免在foreach期间出现垃圾。

作为一些额外的阅读, Shawn Hargreaves可以提供一些有用的附加信息。

(1)每秒60次调用,Compact Framework,不能归结为本机代码,在触发GC之前分配1MB。

它必须是IEnumerable吗? 重构到具有良好旧索引acecss的数组有帮助吗?

不要害怕那些微小的垃圾对象。 池中的ActiveNodes将(应该)成本更高。 因此,如果你摆脱重新创建它们应该就足够了。

@Edit:如果你想使用托管平台并且真的想要实现零垃圾状态,则在foreach循环中放弃池的使用并以另一种方式迭代它,可能使用索引器。 或者考虑创建一个潜在节点列表并返回它。

@ Edit2:当然,使用Current(),Next()等实现IEnumerator,也可以。

您可以实现自己的IEnumerator类,它将枚举这些活动节点。

现在,如果您可以保证在任何时候只有一个客户端将枚举活动节点,则您的类可以缓存此类,因此只存在一个实例。 然后不需要收集垃圾。 对ActiveNodes的调用将调用reset以从头开始枚举。

这是一个危险的假设,但如果你正在优化,你可能会考虑它

如果您有多个客户端随时枚举这些节点,则每个客户端都需要自己的IEnumerator实例才能将其当前光标位置存储在集合中。 在这种情况下,需要在每次调用时创建和收集这些内容 – 您也可以坚持使用原始设计。

此外,您可以拥有一个预先分配的枚举器池。 考虑一下您想要支持的同时枚举数量。

垃圾收集开销将以额外的内存消耗为代价。 最纯粹的速度与内存优化困境。