Технологии4 мин чтения
Рефлексия в 36 раз медленнее: как достать приватное поле за 0,25 нс
Чтение приватного поля через рефлексию занимает 8,96 наносекунды и выделяет 24 байта в куче. Тот же доступ без рефлексии — 0,25 наносекунды и ноль байт: разница в 35,8 раза, а на .NET 8 она доходила до 108. Способ называется UnsafeAccessor, появился в .NET 8, работает без генерации кода и применяется внутри самой библиотеки классов.

Как это выглядит в коде
Ситуация обычная: есть класс из чужой библиотеки, пакета или самой платформы, исходник которого править нельзя, а до приватного поля добраться надо. Привычный путь — получить описание поля через GetField с флагами для непубличных членов и вызвать GetValue.
Способ из .NET 8 выглядит иначе. Метод объявляется как extern static, тела у него нет, его подставляет рантайм: атрибут UnsafeAccessor с видом Field и именем поля, а возвращает метод ссылку на поле. Благодаря возврату по ссылке одним объявлением закрываются сразу чтение и запись.
Такие приёмы попадают в код после профилирования, а не до него. Мы просим показать замер, где доступ к полю действительно виден в профиле. Обычно оказывается, что время уходит в запросы к базе, а приватное поле тут ни при чём.
Что показал замер
Автор прогнал сравнение на четырёх машинах: Intel Core i9-10900KF, AMD Ryzen 9 5950X, Intel Xeon W-2255 и двухпроцессорной системе на Xeon Silver 4314. Все x64, рантаймы .NET 8, 9 и 10 в одном запуске BenchmarkDotNet. Сравнивались шесть способов, и всё, что можно было подготовить заранее, было подготовлено: описание поля найдено один раз, дерево выражений скомпилировано, метод из инструкций IL собран.
Результат: UnsafeAccessor работает за то же время, что прямое чтение поля внутри самого класса. Рефлексия с уже готовым описанием поля медленнее в 2,9–7,8 раза, а рефлексия с поиском поля на каждом обращении — в 14,8–40,2 раза. Между ними расположились дерево выражений и метод, собранный из инструкций IL.

Почему так быстро
Рефлексия работает в рантайме. Описание поля хранит смещение, тип и права доступа, а вызов GetValue каждый раз проверяет, разрешено ли обращение, и приводит результат к object — отсюда и время, и те самые 24 байта в куче на упаковку.
UnsafeAccessor работает на этапе компиляции метода. Рантайм ищет поле один раз, когда впервые компилирует метод доступа, и подставляет в него обычное чтение по смещению. Дальше выполняется тот же машинный код, что и при прямом обращении к полю, поэтому разницы во времени просто нет.
Где легко ошибиться
Плата за скорость — потеря части проверок, и три ловушки стоит знать заранее.
- Имя поля задаётся строкой, и опечатку компилятор не увидит: сборка пройдёт, а первый вызов упадёт с MissingFieldException. Рефлексия в той же ситуации вернёт null из GetField, и это можно проверить заранее.
- Поиск идёт только в том типе, который указан первым аргументом: по базовым типам рантайм не проходит, и поле, объявленное в родителе, таким способом не достать.
- У структуры первый аргумент нужно объявлять по ссылке. Иначе метод получит копию, и запись в поле потеряется без всякой ошибки.
Когда это вообще нужно
Честный ответ: реже, чем хочется. Если поле приватное, автор библиотеки обычно имел на это причину, и обращение к нему — это принятие на себя риска, что следующая версия сломает ваш код молча.
Осмысленные случаи — узкие: сериализация, работа фреймворков с чужими объектами, тесты, которым нужно заглянуть во внутреннее состояние, и горячий код, где рефлексия уже показала себя в профиле. Во всех остальных ситуациях сначала стоит поискать публичный API, а разница в наносекунду имеет значение только на миллионах обращений.
Итог
UnsafeAccessor — редкий случай, когда более быстрый способ ещё и проще: один атрибут вместо кеширования описаний полей и генерации кода. Но он убирает именно те проверки, которые делали рефлексию безопасной, поэтому применять его стоит там, где стоимость доступа реально видна в измерениях.
Источники
Подписаться на журнал
Новые разборы о сайтах, SEO и ИИ выходят в журнале QIO. Подпишитесь в Google, по RSS или в Telegram, чтобы получать их первыми.


