为什么 Roslyn 中有这么多对象池的实现?
Why are there so many implementations of Object Pooling in Roslyn?
ObjectPool 是 Roslyn C# 编译器中使用的一种类型,用于重用经常使用的对象,这些对象通常会被更新并经常被垃圾收集。这减少了必须发生的垃圾收集操作的数量和大小。
Roslyn 编译器似乎有几个独立的对象池,每个池都有不同的大小。我想知道为什么有这么多实现,首选实现是什么以及为什么他们选择池大小为 20、100 或 128。
1 - SharedPools - 存储 20 个对象的池或 100 个对象(如果使用 BigDefault)。这个也很奇怪,因为它创建了一个新的 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 - ListPool and 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 and PooledHashSet - 这些直接使用 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 代)对象为代价的。这有助于稍微提高编译器吞吐量,但主要影响是 Visual Studio 使用 VB 或 C# IntelliSense 时的响应能力。
why there are so many implementations".
没有快速的答案,但我可以想到三个原因:
- 每个实现的目的略有不同,并且针对该目的进行了调整。
- "Layering" - 所有池都是内部的,编译器层的内部细节可能不会从工作区层引用,反之亦然。我们确实通过链接文件进行了一些代码共享,但我们尽量将其保持在最低限度。
- 在统一您今天看到的实现方面并没有付出太多努力。
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.
通常,这是在典型工作负载下进行分析和检测的结果。我们通常必须在分配率(池中的"misses")和池中的总活动字节数之间取得平衡。起作用的两个因素是:
- 最大并行度(并发线程访问池)
- 访问模式包括重叠分配和嵌套分配。
在宏伟的计划中,池中对象持有的内存与编译的总活动内存(第 2 代堆的大小)相比非常小,但我们也要注意不要 return 巨大的对象(通常是大型集合)返回到池中 - 我们将通过调用 ForgetTrackedObject
将它们放在地板上
对于未来,我认为我们可以改进的一个领域是拥有长度受限的字节数组(缓冲区)池。这将特别有助于编译器发射阶段 (PEWriter) 中的 MemoryStream 实现。这些 MemoryStreams 需要连续的字节数组才能快速写入,但它们的大小是动态的。这意味着它们偶尔需要调整大小——通常每次都会加倍。每次调整大小都是一个新的分配,但如果能够从专用池中获取调整大小的缓冲区并将 return 较小的缓冲区返回到不同的池,那就太好了。因此,例如,您将有一个用于 64 字节缓冲区的池,另一个用于 128 字节缓冲区的池,依此类推。总池内存将受到限制,但随着缓冲区的增长,您可以避免 "churning" GC 堆。
再次感谢您的提问。
保罗·哈灵顿。
ObjectPool 是 Roslyn C# 编译器中使用的一种类型,用于重用经常使用的对象,这些对象通常会被更新并经常被垃圾收集。这减少了必须发生的垃圾收集操作的数量和大小。
Roslyn 编译器似乎有几个独立的对象池,每个池都有不同的大小。我想知道为什么有这么多实现,首选实现是什么以及为什么他们选择池大小为 20、100 或 128。
1 - SharedPools - 存储 20 个对象的池或 100 个对象(如果使用 BigDefault)。这个也很奇怪,因为它创建了一个新的 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 - ListPool and 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 and PooledHashSet - 这些直接使用 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 代)对象为代价的。这有助于稍微提高编译器吞吐量,但主要影响是 Visual Studio 使用 VB 或 C# IntelliSense 时的响应能力。
why there are so many implementations".
没有快速的答案,但我可以想到三个原因:
- 每个实现的目的略有不同,并且针对该目的进行了调整。
- "Layering" - 所有池都是内部的,编译器层的内部细节可能不会从工作区层引用,反之亦然。我们确实通过链接文件进行了一些代码共享,但我们尽量将其保持在最低限度。
- 在统一您今天看到的实现方面并没有付出太多努力。
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.
通常,这是在典型工作负载下进行分析和检测的结果。我们通常必须在分配率(池中的"misses")和池中的总活动字节数之间取得平衡。起作用的两个因素是:
- 最大并行度(并发线程访问池)
- 访问模式包括重叠分配和嵌套分配。
在宏伟的计划中,池中对象持有的内存与编译的总活动内存(第 2 代堆的大小)相比非常小,但我们也要注意不要 return 巨大的对象(通常是大型集合)返回到池中 - 我们将通过调用 ForgetTrackedObject
对于未来,我认为我们可以改进的一个领域是拥有长度受限的字节数组(缓冲区)池。这将特别有助于编译器发射阶段 (PEWriter) 中的 MemoryStream 实现。这些 MemoryStreams 需要连续的字节数组才能快速写入,但它们的大小是动态的。这意味着它们偶尔需要调整大小——通常每次都会加倍。每次调整大小都是一个新的分配,但如果能够从专用池中获取调整大小的缓冲区并将 return 较小的缓冲区返回到不同的池,那就太好了。因此,例如,您将有一个用于 64 字节缓冲区的池,另一个用于 128 字节缓冲区的池,依此类推。总池内存将受到限制,但随着缓冲区的增长,您可以避免 "churning" GC 堆。
再次感谢您的提问。
保罗·哈灵顿。