我在实现IEnumerable接口的对象池中有以下代码。
public IEnumerable<T> ActiveNodes
{
get
{
for (int i = 0; i < _pool.Count; i++)
{
if (_pool[i].AvailableInPool)
{
yield return _pool[i];
}
}
}
}
据我所知(根据this问题),这将产生垃圾,因为需要收集IEnumerable对象。 _pool中的任何元素都不会被收集,因为池的目的是保持对所有元素的引用以防止垃圾创建。
任何人都可以建议一种允许迭代_pool以便不生成垃圾的方法吗?
在池上进行迭代时,池中具有AvailableInPool == true
的所有项都应该迭代。订单无关紧要。
答案 0 :(得分:46)
首先,一些人正在推翻奥尔霍夫斯基,暗示这一点都不用担心。在某些环境中的某些应用中,避免收集压力实际上非常重要。
紧凑的框架垃圾收集器有一个简单的策略;每次分配1000KB内存时,它会触发一个集合。现在假设您正在编写一个在紧凑框架上运行的游戏,并且物理引擎每次运行时都会产生1KB的垃圾。物理引擎通常每秒运行20次。所以这是每分钟1200KB的压力,嘿,这已经超过了每分钟一次的收集只是来自物理引擎。如果该集合在游戏中引起明显的口吃,那么这可能是不可接受的。在这种情况下,您可以采取的任何来降低收集压力。
即使我在桌面CLR上工作,我也在努力学习。我们在编译器中有一些场景,我们必须避免收集压力,我们正在跳过所有类型的对象池箍来实现这一点。奥尔霍夫斯基,我感受到了你的痛苦。
那么,为了解决您的问题,如何在不产生收集压力的情况下迭代池化对象的集合?
首先,让我们考虑为什么在典型情况下会发生收集压力。假设你有
foreach(var node in ActiveNodes) { ... }
逻辑上,这会分配两个对象。首先,它分配可枚举 - 序列 - 表示节点序列。其次,它分配枚举器 - 光标 - 表示序列中的当前位置。
在实践中,有时你可以作弊并有一个代表序列和枚举器的对象,但你仍然有一个对象被分配。
我们如何避免这种收集压力?想到三件事。
1)首先不要制作ActiveNodes方法。使调用者通过索引迭代池,并检查自己该节点是否可用。然后序列是已经分配的池,并且光标是整数,两者都没有创建新的收集压力。您支付的价格是重复的代码。
2)正如Steven建议的那样,编译器将采用任何具有正确公共方法和属性的类型;它们不必是IEnumerable和IEnumerator。您可以创建自己的可变结构序列和游标对象,按值传递它们,并避免收集压力。拥有可变结构是危险的,但它是可能的。请注意List<T>
将此策略用于其枚举器;研究其实施的想法。
3)正常地在堆上分配序列和枚举器并将它们集中在一起!你已经采用了汇集策略,所以没有理由不能集合一个枚举器。枚举器甚至有一个方便的“重置”方法,通常只会引发异常,但是你可以编写一个自定义的枚举器对象,当它返回池中时,它使用它将枚举器重置回序列的开头。
大多数对象一次只枚举一次,因此在典型情况下池可能很小。
(现在,你当然可能会遇到鸡与鸡蛋的问题;你如何列举普查员库?)
答案 1 :(得分:22)
在任何“普通”设计中迭代项将始终导致创建新的可枚举对象。创建和处理对象非常快,因此只有在非常特殊的情况下(低延迟才是最重要的)垃圾收集可能(我说'可能')是一个问题。
通过返回不实现IEnumerable
的结构,可以实现没有垃圾的设计。 C#编译器仍然可以迭代这些对象,因为foreach
语句使用duck typing。但同样,这不太可能对你有所帮助。
<强>更新强>
我相信有更好的方法可以提高应用程序的性能,而不是产生这种低级别的技巧,但这是你的号召。这是一个struct enumerable和struct枚举器。当你返回枚举时,C#编译器可以预先通过它:
public struct StructEnumerable<T>
{
private readonly List<T> pool;
public StructEnumerable(List<T> pool)
{
this.pool = pool;
}
public StructEnumerator<T> GetEnumerator()
{
return new StructEnumerator<T>(this.pool);
}
}
以下是StructEnumerator
:
public struct StructEnumerator<T>
{
private readonly List<T> pool;
private int index;
public StructEnumerator(List<T> 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<T>
,如下所示:
public StructEnumerable<T> Items
{
get { return new StructEnumerable<T>(this.pool); }
}
C#可以使用普通的foreach迭代:
foreach (var item in pool.Items)
{
Console.WriteLine(item);
}
请注意,您不能LINQ超过该项目。你需要IEnumerable接口,这涉及装箱和垃圾收集。
祝你好运。<强>更新强>
请注意,在数组和foreach
上使用List<T>
时,不会生成垃圾。在数组上使用foreach
时,C#会将操作转换为for
语句,而List<T>
已经实现了struct
枚举,导致foreach
生成没有垃圾。
答案 2 :(得分:5)
因为XBox的XNA也适用于Compact Framework(我怀疑你正在处理的是你给出的提示(1)),我们可以trust the XNA devs告诉我们foreach创建的时间垃圾。
引用最相关的观点(虽然整篇文章值得一读):
当对
Collection<T>
枚举器进行foreach时,将分配。当做大多数其他事情时 集合,包括数组, 列表,队列,链接列表和 其它:
- 如果明确使用集合,则枚举器不会 分配
- 如果将它们强制转换为接口,则会分配一个枚举器。
因此,如果_pool是List
,数组或类似的并且能够负担得起,您可以直接返回该类型或将IEnumerable<T>
强制转换为相应类型以避免在foreach期间出现垃圾。< / p>
作为一些额外的阅读,Shawn Hargreaves可以提供一些有用的附加信息。
(1)每秒60次调用,Compact Framework,无法转到本机代码,在触发GC之前分配1MB。
答案 3 :(得分:1)
它必须是IEnumerable吗?将重构为具有良好旧索引的acecss的数组吗?
答案 4 :(得分:0)
不要害怕那些微小的垃圾对象。池中的ActiveNodes将(应该)成本更高。因此,如果你摆脱重新创建它们就足够了。
@Edit:如果您使用托管平台并且真的想要实现零垃圾状态,则在foreach循环中放弃池的使用并以另一种方式迭代它,可能使用索引器。或者考虑创建一个潜在节点列表并返回它。
@ Edit2:当然,使用Current(),Next()等实现IEnumerator,也可以。
答案 5 :(得分:0)
您可以实现自己的IEnumerator类,它将枚举这些活动节点。
现在,如果您可以保证在任何时候只有一个客户端将枚举活动节点,那么您的类可以缓存此类,因此只存在一个实例。那么就不需要收集垃圾了。对ActiveNodes的调用将调用reset以从头开始枚举。
这是一个危险的假设,但如果你正在优化,你可以考虑它
如果您有多个客户端随时枚举这些节点,则每个客户端都需要自己的IEnumerator实例才能将其当前光标位置存储在集合中。在这种情况下,需要在每次调用时创建和收集这些内容 - 您也可以坚持使用原始设计。
答案 6 :(得分:0)
此外,您可以拥有一个预先分配的枚举器池。考虑一下你想要支持的同时枚举数量。
垃圾收集开销将以额外的内存消耗为代价。速度与内存优化的困境是最纯粹的形式。