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

Muh*_*eed 33 .net c# garbage-collection roslyn

ObjectPool的是在罗斯林C#编译器用于重复使用,这通常得到new'ed起来,垃圾收集经常经常使用的对象类型.这减少了必须发生的垃圾收集操作的数量和大小.

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

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

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

更新

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

pha*_*ing 41

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

为什么有这么多的实施".

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

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

首选的实现是什么

ObjectPool<T>是首选实现,也是大多数代码使用的.需要注意的是ObjectPool<T>所使用的ArrayBuilder<T>.GetInstance(),这就是大概在罗斯林池对象的最大用户.由于ObjectPool<T>使用频繁,这是我们通过链接文件跨层复制代码的情况之一.ObjectPool<T>调整为最大吞吐量.

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

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

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

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

在宏观方案中,池中对象所拥有的内存与编译的总内存(Gen 2堆的大小)相比非常小,但是,我们也注意不要返回巨大的对象(通常是大的)收集)回到游泳池 - 我们只需要打电话给他们 ForgetTrackedObject

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

再次感谢您的提问.

保罗哈灵顿

  • 不,谢谢保罗的回答!这就是我喜欢的StackOverflow,询问有关某些软件的问题,开发人员会出现并为您解答.我正在寻找我的[ASP.NET MVC Boilerplate](https://visualstudiogallery.msdn.microsoft.com/6cf50a48-fc1e-4eaf-9e82-0b2a6705ca7d)项目的对象池. (3认同)