为什么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 pooledObject = SharedPools.Default<List>().GetPooledObject()) { // Do something with pooledObject.Object } // Example 2 - No using statement so you need to be sure no exceptions are not thrown. List list = SharedPools.Default<List>().AllocateAndClear(); // Do something with list SharedPools.Default<List>().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][3] object. This is probably the preferred option if you want fewer GC's. List list = SharedPools.Default<List>().AllocateAndClear(); try { // Do something with list } finally { SharedPools.Default<List>().Free(list); } 

2 – ListPool和StringBuilderPool – 不是严格单独的实现,而是围绕上面显示的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 – PooledDictionary和PooledHashSet – 它们直接使用ObjectPool并具有完全独立的对象池。 存储128个对象的池。

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

更新

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

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

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

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

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

什么是首选的实现

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

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

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

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

  1. 最大并行度(并发线程访问池)
  2. 访问模式包括重叠分配和嵌套分配。

在宏观方案中,池中对象所拥有的内存与编译的总内存(Gen 2堆的大小)相比非常小,但是,我们也注意不要返回巨大的对象(通常是大的)收集)回到游泳池 – 我们只需要调用ForgetTrackedObject将它们放在地板上

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

再次感谢您的提问。

保罗哈灵顿