Почему в Roslyn так много реализаций пула объектов?

ObjectPool - это тип, используемый в компиляторе Roslyn С# для повторного использования часто используемых объектов, которые обычно получают новые и мусорные собранных очень часто. Это уменьшает количество и размер операций по сбору мусора, которые должны произойти.

У компилятора 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 и 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<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();
}

Update

В .NET Core реализованы новые реализации пула объектов. См. Мой ответ для вопроса С# Object Pooling Pattern.

Ответ 1

Я возглавляю команду vs Team Roslyn. Все пулы объектов предназначены для снижения скорости распределения и, следовательно, частоты сбора мусора. Это происходит за счет добавления долгоживущих (gen 2) объектов. Это немного облегчает компиляцию, но основным эффектом является отзывчивость Visual Studio при использовании VB или С# IntelliSense.

почему существует так много реализаций ".

Нет быстрого ответа, но я могу думать о трех причинах:

  • Каждая реализация выполняет несколько другую цель, и они настроены для этой цели.
  • "Layering" - все пулы являются внутренними и внутренними деталями из уровня компилятора, на которые не может ссылаться слой Workspace или наоборот. У нас есть совместное использование кода через связанные файлы, но мы стараемся свести его к минимуму.
  • Никакие большие усилия не привели к унификации реализаций, которые вы видите сегодня.

какая предпочтительная реализация

ObjectPool<T> является предпочтительной реализацией и используется большая часть кода. Обратите внимание, что ObjectPool<T> используется ArrayBuilder<T>.GetInstance() и, возможно, самым большим пользователем пустых объектов в Roslyn. Поскольку ObjectPool<T> так сильно используется, это один из случаев, когда мы дублируем код через слои через связанные файлы. ObjectPool<T> настроен для максимальной пропускной способности.

На уровне рабочей области вы увидите, что SharedPool<T> пытается совместно использовать объединенные экземпляры для непересекающихся компонентов, чтобы уменьшить общее использование памяти. Мы старались избегать того, чтобы каждый компонент создавал свой собственный пул, предназначенный для определенной цели, и вместо этого делился на основе типа элемента. Хорошим примером этого является StringBuilderPool.

почему они выбрали размер пула 20, 100 или 128.

Обычно это результат профилирования и инструментария при типичных нагрузках. Обычно нам приходится балансировать между ставкой распределения ( "промахи" в пуле) и общим байтом в байтах в пуле. Эти два фактора играют:

  • Максимальная степень parallelism (параллельные потоки, обращающиеся к пулу)
  • Шаблон доступа, включающий перекрывающиеся выделения и вложенные распределения.

В великой схеме вещей память, хранящаяся в объекте в пуле, очень мала по сравнению с общей живой памятью (размером с кучей Gen 2) для компиляции, но мы также заботимся о том, чтобы не возвращать гигантские объекты (обычно большие коллекции) обратно в пул - мы просто опустим их на пол с призывом ForgetTrackedObject

В будущем я думаю, что одна область, которую мы можем улучшить, состоит в том, чтобы иметь пулы байт-массивов (буферов) с ограниченными длинами. Это поможет, в частности, реализовать реализацию MemoryStream в фазе испускания (PEWriter) компилятора. Эти MemoryStreams требуют смежных массивов байтов для быстрой записи, но они имеют динамический размер. Это означает, что они иногда нуждаются в изменении размера - обычно каждый раз удваивается. Каждое изменение размера является новым распределением, но было бы неплохо иметь возможность захватывать измененный буфер из выделенного пула и возвращать меньший буфер обратно в другой пул. Так, например, у вас будет пул для 64-байтовых буферов, другой для буферов на 128 байт и так далее. Общая память пула будет ограничена, но вы избегаете "взбалтывания" кучи GC по мере роста буферов.

Еще раз спасибо за вопрос.

Пол Харрингтон.