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

Olh*_*sky 25 c# iteration garbage-collection compact-framework

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

public IEnumerable<T> ActiveNodes
{
    get
    {
        for (int i = 0; i < _pool.Count; i++)
        {
            if (_pool[i].AvailableInPool)
            {
                yield return _pool[i];
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

Eri*_*ert 47

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

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

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

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

首先,让我们考虑为什么在典型情况下会发生收集压力.假设你有

 foreach(var node in ActiveNodes) { ... }
Run Code Online (Sandbox Code Playgroud)

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

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

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

1)首先不要制作ActiveNodes方法.使调用者通过索引迭代池,并检查自己该节点是否可用.然后序列是已经分配的池,并且光标是整数,两者都没有创建新的收集压力.您支付的价格是重复的代码.

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

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

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

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

  • @ user492238:游戏实施者拥有各种技术来最小化,缓解或消除游戏中垃圾收集造成的暂停.这些技术是否足以让游戏设计师在某些版本的CLR上制作可玩且有利可图的游戏并不是人们可以轻松覆盖500个评论字符的主题.大量人*为各种版本的CLR制作游戏的事实证明它适用于许多游戏. (7认同)
  • "如果这个集合在游戏中引起明显的口吃",那么游戏有时候就会口吃,对吧?因此,如果我们无法避免任何收集,CLR将不适合实施游戏? (2认同)

Ste*_*ven 22

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

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

UPDATE

我相信有更好的方法可以提高应用程序的性能,而不是产生这种低级别的技巧,但这是你的号召.这是一个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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这是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;
    }
}
Run Code Online (Sandbox Code Playgroud)

你可以简单地返回StructEnumerable<T>如下:

public StructEnumerable<T> Items
{
    get { return new StructEnumerable<T>(this.pool); }
}
Run Code Online (Sandbox Code Playgroud)

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

foreach (var item in pool.Items)
{
    Console.WriteLine(item);
}
Run Code Online (Sandbox Code Playgroud)

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

祝好运.

更新:

请注意,当同时使用foreach两个数组时,List<T>不会生成任何垃圾.在foreach数组上使用时,C#会将操作转换为for语句,同时List<T>已经实现了一个struct枚举器,导致foreach不产生垃圾.

  • 我的场景很特别,并且避免垃圾创建对我的应用程序至关重要,该应用程序运行在.NET压缩CLR上,后者与桌面解释器具有非常不同的垃圾收集方法. (3认同)
  • @Steven:在我的情况下,每个池每秒有60个对象,乘以许多池.紧凑的CLR在每1MB分配后收集.集合引起的延迟非常重要. (2认同)

Kyt*_*yte 5

由于XBox的XNA也可以在Compact Framework上工作(我怀疑你正在处理的是你给出的提示(1)),我们可以相信XNA开发人员可以在foreach创建垃圾的时候告诉我们.

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

在对一个Collection<T>普查员进行预告时,将分配.

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

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

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

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

(1)每秒60次调用,紧凑型框架,不能归结为本机代码,在触发GC之前分配1MB.