Стек и куча, boxing и unboxing

↑ 2.2 .NET runtime: CLR, память, GC · ← Предыдущая · → Следующая

Обязательно~6 мин чтенияУровень: middle

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

boxing — скрытый источник аллокаций и нагрузки на GC. Вопросы «что такое boxing, где он происходит неявно» и «где хранится value type внутри класса» задают почти всегда.

Объяснение

Стек и управляемая куча (повтор с углублением)

  • Стек потока: кадры методов, локальные value types, ссылки. Освобождается при выходе из метода.
  • Управляемая куча (managed heap): объекты ссылочных типов. Каждый объект имеет заголовок (sync block index, 8 байт на x64) и указатель на таблицу методов (MethodTable, 8 байт) — минимум 24 байта на объект.
  • Value type внутри объекта (поле класса, элемент массива) хранится в куче в составе этого объекта.
  • Локальная переменная, захваченная замыканием или живущая через await, переносится в объект в куче (display class или state machine).

Boxing и unboxing

Boxing — упаковка value type в объект в куче, когда его используют как object или интерфейс. Unboxing — извлечение значения обратно с проверкой типа.

int x = 42;
object o = x;            // boxing: аллокация объекта в куче + копирование значения
int y = (int)o;          // unboxing: проверка типа + копирование
long z = (long)o;        // InvalidCastException! распаковывать можно только в точный тип

Где boxing происходит неявно

// 1. Необобщённые коллекции (legacy)
var list = new ArrayList(); list.Add(1);                   // boxing
 
// 2. Приведение struct к интерфейсу
IComparable c = 5;                                         // boxing
void Print(IFormattable f) {} Print(DateTime.Now);        // boxing
 
// 3. params object[] и форматирование (до .NET 6)
string.Format("{0} {1}", id, count);                       // boxing int-ов
Console.WriteLine("{0}", 42);
_logger.LogInformation("Count {Count}", count);           // boxing в params object?[] → LoggerMessage решает
 
// 4. Вызов методов object у struct без переопределения
myStruct.GetHashCode();  myStruct.Equals(other);           // ValueType.Equals — рефлексия и boxing
myStruct.ToString();                                        // если не переопределён
 
// 5. Enum в некоторых API
someEnum.HasFlag(Flags.A);                                  // до .NET Core 2.1 — boxing, теперь интринсик
Enum.GetName(...)
 
// 6. dynamic, рефлексия Invoke, сравнение generic без ограничений через ==(object)

Как избегать

  • Generic-коллекции и методы (List<int>, where T : IComparable<T>) — без боксинга.
  • Переопределять Equals, GetHashCode, ToString у struct или использовать record struct, реализовать IEquatable<T>.
  • Интерполяция строк (.NET 6+) без боксинга (handlers). [LoggerMessage] для логов.
  • Не приводить struct к интерфейсу в горячем коде, использовать generic-ограничения.

Как увидеть

  • IL-инструкция box в декомпиляторе (sharplab.io).
  • BenchmarkDotNet [MemoryDiagnoser] — неожиданные аллокации.
  • Анализаторы (Roslynator, HeapAllocationAnalyzer, ErrorProne.NET).

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

  • Изменение упакованной struct через интерфейс меняет копию в куче, а не исходную переменную.
  • Boxing сам по себе — наносекунды, но в цикле на миллион элементов это миллион мусорных объектов и лишние сборки GC.
  • Dictionary<MyStruct, T> без IEquatable<MyStruct> боксит ключ при каждом сравнении.
  • Nullable: object o = (int?)null → o == null (null не боксится). (int?)5 боксится как int, а не как Nullable<int>.

Тестирование

[MemoryDiagnoser]
public class BoxingBench
{
    private readonly int[] _data = Enumerable.Range(0, 1000).ToArray();
    [Benchmark] public int WithBoxing() { var l = new ArrayList(); foreach (var x in _data) l.Add(x); return l.Count; }
    [Benchmark] public int Generic()    { var l = new List<int>();  foreach (var x in _data) l.Add(x); return l.Count; }
}
// в отчёте колонка Allocated покажет разницу

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

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