c# - 为什么 Roslyn 中有这么多对象池的实现?

标签 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 的回答问题。

最佳答案

我是 Roslyn 性能 V 团队的负责人。所有对象池都旨在降低分配率,从而降低垃圾收集的频率。这是以添加长生命周期(第 2 代)对象为代价的。这对编译器吞吐量有轻微帮助,但主要影响是在使用 VB 或 C# IntelliSense 时对 Visual Studio 的响应能力。

why there are so many implementations".

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

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

what the preferred implementation is

ObjectPool<T>是首选实现方式,也是大多数代码使用的方式。注意 ObjectPool<T>ArrayBuilder<T>.GetInstance() 使用这可能是 Roslyn 中池化对象的最大用户。因为ObjectPool<T>被如此频繁地使用,这是我们通过链接文件跨层复制代码的情况之一。 ObjectPool<T>针对最大吞吐量进行了调整。

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

why they picked a pool size of 20, 100 or 128.

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

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

在宏伟的计划中,池中对象持有的内存与编译的总事件内存(第 2 代堆的大小)相比非常小,但我们也注意不要返回巨大的对象(通常是大型收藏品)回到游泳池 - 我们只需调用 ForgetTrackedObject 将它们放在地板上

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

再次感谢您的提问。

保罗·哈林顿。

关于c# - 为什么 Roslyn 中有这么多对象池的实现?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30618067/

相关文章:

c# - Entity Framework : map varchar to DateTime property

c# - .NET Windows 窗体 DataGridView 单元格文本在以编程方式添加时消失

c# - 如何正确使用rsa验证数据?

javascript - 如何使用asp.net c#获取谷歌地图标记的坐标并将其存储在文本框或标签中

c# - 适用于 Windows Phone 8.1 的 XMPP 库

c# - Unity3D 中 GC 和卸载 Assets 的顺序

go - goroutine不会将内存返回操作系统

c# - 如何使用 Linq 搜索分层数据

c# - ZedGraph 垂直线与 LineObj 问题

python - 即使没有非弱引用指向对象,weakref 也不能评估为 None 吗?