Аллокации и зеро-аллокационный код

↑ 7.1 Производительность .NET и кэширование · ← Предыдущая · → Следующая

Желательно~3 мин чтенияУровень: senior

Зачем это на собесе

Про низкоуровневую производительность: Span<T>, пулы, struct и когда это оправдано.

Объяснение

Каждая аллокация в куче нагружает GC. В горячих путях (парсеры, сериализация, middleware) снижают число аллокаций.

ПриёмЭффект
Span<T>/ReadOnlySpan<T>/Memory<T>срезы без копирования массивов и подстрок
stackallocнебольшие буферы на стеке
ArrayPool<T>.Sharedпереиспользование больших массивов
ObjectPool<T>, StringBuilder из пулапереиспользование объектов
struct/readonly struct/ref structзначения без кучи (осторожно с копированием)
ValueTaskбез аллокации при синхронном завершении (см. Время жизни: Singleton, Scoped, Transient)
string.Create, интерполяция в DefaultInterpolatedStringHandlerменьше промежуточных строк
Избежание замыканий/боксинга/LINQ в горячих путяхнет скрытых аллокаций
static лямбдынет захвата контекста
FrozenDictionary/SearchValuesбыстрые неизменяемые структуры
// Разбор строки без Substring
static int SumCsv(ReadOnlySpan<char> line)
{
    int sum = 0;
    foreach (var range in line.Split(','))       // .NET 9: MemoryExtensions.Split
        sum += int.Parse(line[range]);
    return sum;
}
 
var buffer = ArrayPool<byte>.Shared.Rent(64 * 1024);
try { /* работа */ } finally { ArrayPool<byte>.Shared.Return(buffer); }

Нюансы и подводные камни

  • Оптимизация оправдана только по профилю: читаемость дороже.
  • Массив из пула может быть больше запрошенного и не очищен.
  • Span<T> нельзя хранить в поле класса и использовать в async.
  • Возврат буфера в пул дважды или использование после возврата приводит к порче данных.
  • Большие объекты (≥85 КБ) попадают в LOH и собираются только при gen2.

Практика

  1. Уберите аллокации в парсере строк через Span и сравните в BenchmarkDotNet.
  2. Замените выделение буферов на ArrayPool.
  3. Найдите скрытые аллокации через dotnet-trace/allocation profiler.

Вопросы с ответами

Связанные темы