为什么Roslyn中有如此多的对象池实现?

时间:2015-06-03 10:55:00

标签: c# .net garbage-collection roslyn

ObjectPool是Roslyn C#编译器中使用的一种类型,用于重用经常使用的常用对象,这些对象通常会被新增和垃圾收集。这减少了必须发生的垃圾收集操作的数量和大小。

Roslyn编译器似乎有几个单独的对象池,每个池都有不同的大小。我想知道为什么有这么多的实现,首选的实现是什么,以及为什么他们选择了20,100或128的池大小。

1 - SharedPools - 如果使用BigDefault,则存储20个对象的池或100个。这个也很奇怪,因为它创建了一个新的PooledObject实例,当我们试图聚集对象而不是创建和销毁新对象时这没有任何意义。

// Example 1 - In a using statement, so the object gets freed at the end.
using (PooledObject<Foo> pooledObject = SharedPools.Default<List<Foo>>().GetPooledObject())
{
    // Do something with pooledObject.Object
}

// Example 2 - No using statement so you need to be sure no exceptions are not thrown.
List<Foo> list = SharedPools.Default<List<Foo>>().AllocateAndClear();
// Do something with list
SharedPools.Default<List<Foo>>().Free(list);

// Example 3 - I have also seen this variation of the above pattern, which ends up the same as Example 1, except Example 1 seems to create a new instance of the IDisposable [PooledObject<T>][3] object. This is probably the preferred option if you want fewer GC's.
List<Foo> list = SharedPools.Default<List<Foo>>().AllocateAndClear();
try
{
    // Do something with list
}
finally
{
    SharedPools.Default<List<Foo>>().Free(list);
}

2 - ListPoolStringBuilderPool - 不是严格单独的实现,而是围绕上面显示的SharedPools实现的包装器,专门用于List和StringBuilder。因此,这将重新使用存储在SharedPools中的对象池。

// Example 1 - No using statement so you need to be sure no exceptions are thrown.
StringBuilder stringBuilder= StringBuilderPool.Allocate();
// Do something with stringBuilder
StringBuilderPool.Free(stringBuilder);

// Example 2 - Safer version of Example 1.
StringBuilder stringBuilder= StringBuilderPool.Allocate();
try
{
    // Do something with stringBuilder
}
finally
{
    StringBuilderPool.Free(stringBuilder);
}

3 - PooledDictionaryPooledHashSet - 它们直接使用ObjectPool并拥有完全独立的对象池。存储128个对象的池。

// Example 1
PooledHashSet<Foo> hashSet = PooledHashSet<Foo>.GetInstance()
// Do something with hashSet.
hashSet.Free();

// Example 2 - Safer version of Example 1.
PooledHashSet<Foo> hashSet = PooledHashSet<Foo>.GetInstance()
try
{
    // Do something with hashSet.
}
finally
{
    hashSet.Free();
}

更新

.NET Core中有新的对象池实现。请参阅我对C# Object Pooling Pattern implementation问题的回答。

1 个答案:

答案 0 :(得分:41)

我是Roslyn表演v团队的领导者。所有对象池都旨在降低分配率,从而降低垃圾收集的频率。这是以添加长寿命(第2代)对象为代价的。这有助于编译器吞吐量,但主要影响是使用VB或C#IntelliSense时的Visual Studio响应。

  

为什么有这么多的实现“。

没有快速回答,但我可以想到三个原因:

  1. 每个实现的目的略有不同,并且为此目的进行了调整。
  2. “分层” - 所有池都是内部的,编译器层的内部详细信息可能无法从工作区层引用,反之亦然。我们确实通过链接文件进行了一些代码共享,但我们会尽量将其保持在最低限度。
  3. 没有付出太多努力来统一你今天看到的实现。
  4.   

    首选实现是什么

    ObjectPool<T>是首选实现,也是大多数代码使用的实现。请注意ObjectPool<T>使用ArrayBuilder<T>.GetInstance(),这可能是Roslyn中池化对象的最大用户。由于ObjectPool<T>被大量使用,这是我们通过链接文件跨层复制代码的情况之一。 ObjectPool<T>已调整为最大吞吐量。

    在工作区层,您将看到SharedPool<T>尝试跨不相交的组件共享池化实例,以减少总体内存使用量。我们试图避免让每个组件创建专用于特定目的的池,而是根据元素的类型进行共享。一个很好的例子就是StringBuilderPool

      

    为什么他们选择了20,100或128的游泳池大小。

    通常,这是典型工作负载下的性能分析和检测的结果。我们通常必须在分配率(池中的“未命中”)和池中的总活动字节之间取得平衡。发挥作用的两个因素是:

    1. 最大并行度(访问池的并发线程)
    2. 访问模式包括重叠分配和嵌套分配。
    3. 在宏观方案中,池中对象所拥有的内存与编译的总内存(Gen 2堆的大小)相比非常小,但是,我们也注意不要返回巨大的对象(通常是大型收藏品)回到游泳池 - 我们只需拨打ForgetTrackedObject

      就将它们放在地板上

      对于未来,我认为我们可以改进的一个领域是拥有约束长度的字节数组(缓冲区)。这将特别有助于编译器的emit阶段(PEWriter)中的MemoryStream实现。这些MemoryStream需要连续的字节数组来快速写入,但它们是动态调整大小的。这意味着他们偶尔需要调整大小 - 通常每次都会增加一倍。每个resize都是一个新的分配,但是能够从专用池中获取调整大小的缓冲区并将较小的缓冲区返回到不同的池会很好。因此,例如,您将拥有一个用于64字节缓冲区的池,另一个用于128字节缓冲区的池等等。总池内存将受到约束,但是当缓冲区增长时,您可以避免“搅拌”GC堆。

      再次感谢您的提问。

      保罗哈灵顿。