Skip to content

Repository files navigation

PoolProof

Замеры вокруг ArrayPool. Один и тот же массив получают тремя способами — new byte[n], GC.AllocateUninitializedArray<byte>(n) и ArrayPool<byte>.Shared.Rent(n) с возвратом, — и смотрят, что при этом происходит с памятью и со временем. Отдельно проверяется, куда попадает буфер Stream.CopyTo.

BenchmarkDotNet 0.15.8, Release. Всё на .NET 8, .NET 9 и .NET 10, на обычном сборщике и на серверном, на четырёх машинах.

Что показали замеры

Пул хранит массивы степенями двойки. Запрос на 81 920 байт — размер буфера по умолчанию у Stream.CopyTo — попадает в бакет 13, и длина массива в нём 131 072 байта. Порог кучи больших объектов для byte[] проходит по длине 84 976: к длине добавляется заголовок в 24 байта, и 84 976 + 24 = 85 000.

Аренда отправляет массив в эту кучу на размерах от 65 537 до 84 975 байт, new на тех же размерах оставляет массив в нулевом поколении.

FileStream.CopyTo, CopyToAsync и наследники MemoryStream выделяют 139 296 байт на первом копировании — на четырёх машинах, трёх рантаймах и двух сборщиках. MemoryStream без наследования не выделяет ничего.

Прирост самой кучи при этом виден не всегда: пул через Gen2GcCallback освобождает лишние массивы после сборки второго поколения, и на части машин это происходит между двумя чтениями размера кучи. Поэтому рядом с приростом стоит второй счётчик — выделено байт.

64 буфера по 81 920 байт, удерживаемых одновременно, дают 8 195 КБ в этой куче. Те же 64 запроса с возвратом после каждого — 128 КБ.

Return по умолчанию массив не чистит: следующий арендатор получает все 16 байт из 16, а массив ссылок держит все 64 объекта из 64. Двойной возврат пул принимает без ошибки, массив из другого пула отклоняет исключением.

Аренда идёт по нижней границе — по времени готового массива. Вместимость пула зависит от числа логических процессоров: на 1 024 удерживаемых буферах машины с 20 процессорами выделяют 401 КБ на вызов, машины с 32 и 64 — ноль.

Полные отчёты в Results.

Прирост кучи больших объектов при удержании буферов

Во сколько раз возврат с очисткой медленнее обычного

Машины

CPUЯдра/потокиОСПроцессоров
№1AMD Ryzen 9 5950X16 / 32Windows 10 180932
№2Intel Core i9-10900KF10 / 20Windows 10 22H220
№32 × Intel Xeon Silver 43142×16 / 64Windows Server 202264
№4Intel Xeon W-225510 / 20Windows Server 202220

Как воспроизвести

Нужны SDK .NET 8, 9 и 10 — BenchmarkDotNet поднимает по процессу на каждый рантайм. Проверить, что все три на месте:

dotnet --list-sdks

Весь прогон одной командой, без аргументов:

all.bat

Скрипт собирает проект, снимает текстовые отчёты на трёх рантаймах и двух сборщиках, повторяет часть отчётов с поднятым порогом кучи больших объектов, запускает замеры и складывает всё в Results\<имя машины>. Имя папки берётся из имени машины.

Вручную, без скрипта:

dotnet run -c Release -f net10.0 -- reports
dotnet run -c Release -f net10.0 -- extra
dotnet run -c Release -f net10.0 -- loh 64
dotnet run -c Release -f net10.0 -- seq
dotnet run -c Release -f net10.0 -- copy file
dotnet run -c Release -f net10.0
dotnet run -c Release -f net10.0 -- --filter *PoolTouchBench*

Что меряется

КлассЧто сравнивает
PoolRentBenchаренда с возвратом против new, по размерам, на трёх рантаймах
PoolThresholdBenchтот же вопрос мелким шагом: с какого размера аренда быстрее
PoolUninitializedBenchаренда против выделения без обнуления, без записи в массив
PoolTouchBenchто же самое, но массив заполняется целиком
PoolClearBenchвозврат с очисткой против возврата без неё
PoolWasteBenchте же способы на размерах из разных профилей
PoolThreadBenchвозврат в своём потоке против возврата в чужом
PoolParallelBenchаренда против выделения в нескольких потоках сразу
PoolCapacityBenchсколько буферов пул удерживает, пока не начнёт выделять

Отчёты вне BenchmarkDotNet:

ОтчётЧто печатает
reportsокругление запроса до бакета, поколение массива, что пул держит после возврата
extraгде проходит порог кучи больших объектов, до какого размера пул хранит массивы, потеря на округлении
lohприрост кучи больших объектов и выделено байт, когда буферы удерживают одновременно
seqто же самое, когда буфер возвращают сразу
copyто же самое на Stream.CopyTo, по одному виду потока на запуск

Как устроен замер

Все измеряемые методы лежат в Subjects.cs, у каждого способа свой метод с NoInlining.

Прирост кучи больших объектов читается через GC.GetGCMemoryInfo, а этот метод возвращает состояние на момент последней завершённой сборки. Поэтому перед каждым чтением идёт принудительная полная сборка, и притом дважды: после первой могут отработать финализаторы.

Рядом с приростом стоит второй счётчик — GC.GetTotalAllocatedBytes. Он меряет другое: сколько памяти запрошено, а не сколько её осталось после сборки. Две величины расходятся там, где пул успел освободить массив между двумя чтениями, и без второго счётчика такой случай не отличить от того, где буфер вообще не арендовали. Параметр precise намеренно false: с true рантайм ради точного счёта делает сборку мусора, а сборка здесь и есть то, на что реагирует пул.

Отчёты разнесены по разным аргументам намеренно. Все они работают с общим пулом, и в одном процессе первый оставлял бы после себя массивы, а следующий показывал бы нулевой прирост там, где его быть не должно. По той же причине каждый вид потока в отчёте copy и каждое количество буферов в отчёте loh снимаются своим процессом.

В PoolTouchBench массив после получения заполняется целиком. Без записи сравнение с GC.AllocateUninitializedArray неполное: new обнуляет память и тем самым обращается ко всем страницам, а выделение без обнуления переносит эту работу на первую запись. Строка Existing — нижняя граница: массив создан и уже заполнен, остаётся только запись.

Порог кучи больших объектов настраивается. Скрипт повторяет отчёты extra, copy file и seq с DOTNET_GCLOHThreshold=0x30000, то есть с порогом 196 608 — выше бакета в 131 072. Отчёт показывает, применилась ли переменная: если граница осталась на 84 976, значит нет.

Кодировка вывода задаётся в Program.cs. Консоль на разных машинах пишет по-разному, а отчёты уезжают в репозиторий одним набором.

Что где лежит

PoolProof.csproj многоцелевой: net8.0, net9.0, net10.0
PoolProof.slnx
Program.cs точка входа, разбор аргументов
Subjects.cs все измеряемые методы
README.md
all.bat весь прогон одной командой
Benchmarks/ девять классов, по одному на файл
Enums/ SizeProfile
Diagnostics/
Diagnostic.cs округление, поколение, что держит пул
Extra.cs порог кучи, вместимость по размеру, потеря
LohGrowth.cs прирост кучи и выделено байт
PayloadStream.cs наследник MemoryStream для проверки CopyTo
Results/
<машина>/ выгрузки прогона
<машина>/bench/ отчёты BenchmarkDotNet
Docs/ графики

Ссылки

About

Замеры ArrayPool на четырёх машинах: куда попадает буфер Stream.CopyTo, что остаётся в массиве после Return и сколько занимает аренда

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages