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

时间:2011-03-30 13:03:54

标签: c# compact-framework garbage-collection iteration

我在实现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的所有项都应该迭代。订单无关紧要。

7 个答案:

答案 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)

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

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