Аллокации, которых нет в коде: охота на скрытый боксинг в .NET 10 — 19 июня 2026 г. в 01:08:26.192
Аллокации, которых нет в коде: охота на скрытый боксинг в .NET 10 Самая дорогая аллокация в вашем сервисе та, которой нет в исходниках. Вы написали struct ради zero-allocation, прошли code review, а в проде Gen0-коллекции все равно идут косяком. Потому что между вашим кодом и машинным кодом стоит компилятор, и он молча упаковывает ваш value-тип в кучу там, где вы этого не просили — а на код-ревью этого не видно. TL;DR. Боксинг (boxing) в .NET - это не только object o = 42. Он прячется в вызовах интерфейсных методов на struct, в дефолтном ValueType.Equals, в params object[]-аргументах, в foreach по интерфейсу и в замыканиях. При этом часть “классических” примеров боксинга из старых гайдов на современном рантайме уже не аллоцирует — JIT научился их вырезать, и слепо копировать советы десятилетней давности вредно. Ниже — карта мест, где боксинг живёт и сейчас, отдельный разбор того, что рантайм уже оптимизировал, реальный мини-кейс, воспроизводимый бенчмарк на BenchmarkDotNet с MemoryDiagnoser, способ ловить упаковку через DOTNET_JitDisasm и dotnet-gcdump, и паттерны лечения без потери читаемости. О версиях и числах. Всё прверялось на .NET 10 (текущий LTS) и C# 13/14-уровне компилятора, Release, без отладчика, BenchmarkDotNet с MemoryDiagnoser. На .NET 8/9 поведение в основном такое же, но отдельные оптимизации JIT отличаются между мажорными версиями — поэтому главный принцип статьи: не верьте на слово (в том числе мне), гоняйте MemoryDiagnoser на своей версии рантайма. Числа в таблицах ниже - иллюстративные, порядок величины, а не точные замеры с вашего железа. Пролог: “у нас же всё на struct, откуда Gen0?” Сервис на горячем пути считает метрики: миллионы маленьких readonly struct-значений в секунду, никакого new, никаких классов в hot path. По задумке — ноль аллокаций. На дашборде — стабильный поток Gen0-коллекций раз в несколько секунд под нагрузкой. Профайлер показывает аллокации, но стек ведёт в метод, где в коде нет ни одного new. Там цикл по интерфейсу, пара вызовов .Equals(), передача значения в params-метод лога. Глазами — чисто. В машинном коде — box-инструкции на каждой итерации. Это и есть скрытый боксинг: компилятор C# и JIT упаковывают ваш struct в объект на куче, потому что в конкретной точке кода value-тип нужно представить как ссылочный. Симптом — Gen0-коллекции “из ниоткуда”, и его не видно ни в code review, ни в дампе, пока не посмотришь на IL или дизасм. Что такое боксинг и почему он стоит дорого Боксинг — это упаковка value-типа (struct, enum, примитив) в объект на управляемой куче. Рантайму нужно выделить заголовок объекта, скопировать туда значение и вернуть ссылку. Анбоксинг - обратная операция с проверкой типа. Цена не в самой инструкции, а в последствиях: каждая упаковка - это аллокация в Gen0. Много мелких аллокаций на горячем пути означают частые Gen0-коллекции, паузы (пусть и короткие), вытеснение полезных данных из кэша и общий рост CPU на ровном месте. На сервисе с SLA по p99 это бьёт по хвосту латентности так же, как и любая другая лишняя аллокация. В IL боксинг виден явно - инструкция box. Именно её мы и будем искать. Читать дальше: https://habr.com/ru/articles/1049236/

