Пулы объектов: ArrayPool, ObjectPool, RecyclableMemoryStream
↑ 2.2 .NET runtime: CLR, память, GC · ← Предыдущая
Зачем это на собесе
Пулы — стандартный ответ на вопрос «как уменьшить аллокации и давление на GC». Ждут понимания, когда пул оправдан, как безопасно вернуть буфер, чем
ArrayPoolотличается отObjectPoolи зачемRecyclableMemoryStreamвместоMemoryStream.
Объяснение
Зачем пулы
Частое создание больших или тяжёлых объектов нагружает GC (особенно LOH ≥ 85 000 байт). Пул переиспользует объекты: взял, использовал, вернул. Выигрыш — в меньшем числе аллокаций и сборок Gen 2.
ArrayPool<T>
Пул массивов (System.Buffers). Общий пул: ArrayPool<byte>.Shared.
var buffer = ArrayPool<byte>.Shared.Rent(64 * 1024); // может вернуть массив БОЛЬШЕ запрошенного, содержимое не обнулено
try
{
int read;
while ((read = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), ct)) > 0)
Process(buffer.AsSpan(0, read)); // работаем только с реально прочитанной частью
}
finally
{
ArrayPool<byte>.Shared.Return(buffer, clearArray: true); // вернуть; clearArray, если внутри секреты
}Правила:
Rent(n)возвращает массив длиной не меньше n. Используйте размер, который вы сами знаете, а неbuffer.Length.- Возвращайте буфер ровно один раз, в
finally. ПовторныйReturnили использование послеReturn— use-after-free на уровне логики, данные будут испорчены другим потребителем. - Не храните арендованный массив в долгоживущих объектах и не отдавайте наружу.
- Содержимое при аренде не обнулено: могут быть чужие данные.
- Для чувствительных данных —
clearArray: true.
ObjectPool<T>
Microsoft.Extensions.ObjectPool — пул произвольных объектов (StringBuilder, тяжёлые парсеры, буферы).
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
services.AddSingleton(sp => sp.GetRequiredService<ObjectPoolProvider>().CreateStringBuilderPool());
public sealed class Formatter(ObjectPool<StringBuilder> pool)
{
public string Build(IEnumerable<string> parts)
{
var sb = pool.Get();
try { foreach (var p in parts) sb.Append(p).Append(';'); return sb.ToString(); }
finally { pool.Return(sb); } // политика Return сбрасывает состояние (Clear)
}
}Для своего типа реализуют IPooledObjectPolicy<T> (создание и сброс состояния). Объект перед возвратом нужно сбросить.
RecyclableMemoryStream
Пакет Microsoft.IO.RecyclableMemoryStream — замена MemoryStream, использует пул блоков вместо одного большого массива.
private static readonly RecyclableMemoryStreamManager Manager = new();
await using var ms = Manager.GetStream();
await report.ExportAsync(ms, ct);
ms.Position = 0;
await ms.CopyToAsync(response.Body, ct);Плюсы против MemoryStream: не создаёт больших массивов в LOH при росте (нет удвоения буфера), меньше фрагментации, блоки переиспользуются. Полезно при генерации PDF/Excel/архивов в памяти.
Другие приёмы уменьшения аллокаций
Span<T>,stackallocдля небольших буферов (см. тему про Span).StringBuilderвместо конкатенаций,string.Create.structдля маленьких неизменяемых значений,ValueTaskвместоTask.System.IO.Pipelinesдля сетевого ввода-вывода.- Кэширование неизменяемых объектов,
static readonlyдля повторно используемых значений.
Нюансы и подводные камни
- Пул оправдан только для крупных или очень частых аллокаций. Для маленьких коротких объектов GC быстрее: пул добавляет накладные расходы и риск ошибок.
- Забыли вернуть объект — пул создаст новый, утечки нет (для
ArrayPool.Shared), но выигрыш потерян. - Вернули дважды или использовали после возврата — повреждение данных, трудноуловимые баги.
- Несброшенное состояние объекта из
ObjectPool— утечка данных между запросами (безопасность!). - Пул без ограничений держит память постоянно; у
ArrayPool.Sharedограничение на размер и число массивов по корзинам. - Проверяйте бенчмарком (
[MemoryDiagnoser]): пул иногда не даёт выигрыша.
Практика
- Переписать чтение файла с
new byte[81920]наArrayPoolи сравнитьAllocatedв BenchmarkDotNet. - Заменить
MemoryStreamв генерации отчёта наRecyclableMemoryStreamи посмотреть размер LOH под нагрузкой. - Добавить
ObjectPool<StringBuilder>в сервис форматирования.
Вопросы с ответами
Зачем нужны пулы объектов?
Чтобы переиспользовать дорогие или большие объекты вместо постоянного создания. Это снижает число аллокаций и нагрузку на GC, особенно на LOH.
Какие правила безопасной работы с
ArrayPool?Возвращать буфер ровно один раз в
finally, не использовать после возврата, работать только с нужной длиной (массив может быть больше запрошенного), помнить, что содержимое не обнулено, и очищать секреты черезclearArray.
Чем
RecyclableMemoryStreamлучшеMemoryStream?
MemoryStreamрастёт удвоением единого массива, что порождает большие временные массивы в LOH.RecyclableMemoryStreamсобирается из переиспользуемых блоков, поэтому нагрузка на GC и фрагментация меньше.
Когда пул не нужен?
Для маленьких короткоживущих объектов: gen 0 сборки дешёвые, а пул усложняет код и может дать баги. Решение принимают по профилю и бенчмарку.
Чем
ArrayPoolотличается отObjectPool?
ArrayPool<T>арендует массивы нужного размера (с округлением вверх) и не сбрасывает содержимое.ObjectPool<T>управляет произвольными объектами через политику создания и сброса состояния.
Связанные темы
- Предыдущая: Утечки памяти в .NET и как их искать · Следующий раздел: 2.3 Асинхронность и многопоточность
- Span и Memory: Span и Memory: stackalloc, ref struct
- Сборщик мусора: Сборщик мусора: поколения, LOH, режимы GC
- Аллокации и zero-alloc: Аллокации и зеро-аллокационный код
- Генерация документов и отчётов: 5.6 Генерация документов и отчётов