Бенчмаркинг System.Text.Json: кеш имён свойств даёт разницу до ×4,3

Разработчики на .NET нередко сталкиваются с неожиданным падением производительности при сериализации и десериализации JSON с помощью System.Text.Json. Корень проблемы лежит не в настройках вроде PropertyNameCaseInsensitive или политики именования, а в том, как JsonSerializerOptions кеширует имена св

Бенчмаркинг System.Text.Json: кеш имён свойств даёт разницу до ×4,3

Разработчики на .NET нередко сталкиваются с неожиданным падением производительности при сериализации и десериализации JSON с помощью System.Text.Json. Корень проблемы лежит не в настройках вроде PropertyNameCaseInsensitive или политики именования, а в том, как JsonSerializerOptions кеширует имена свойств. Эксперименты на четырёх машинах, трёх рантаймах и трёх размерах входного JSON показали: разница в скорости может достигать 4,3 раза при одних и тех же настройках. Понимание этого механизма поможет избежать узких мест в высоконагруженных приложениях.

Как кеш имён свойств влияет на производительность

В официальной документации Microsoft к опции PropertyNameCaseInsensitive прилагается предупреждение о возможных накладных расходах, но конкретные условия их возникновения не раскрыты. Автор детального бенчмаркинга на Habr провёл серию замеров и выяснил, что сам по себе флаг PropertyNameCaseInsensitive добавляет незначительные затраты: коэффициент производительности составляет 0,92–1,09 относительно базового варианта без него. Политика именования camelCase также не даёт заметного прироста. Однако разница до 4,3 раза возникает, когда в кеш попадают различные варианты написания одних и тех же ключей.

Ключевой элемент — внутренний кеш имён в JsonSerializerOptions. Когда вы создаёте два экземпляра через new с одинаковыми полями, они разделяют один кеш. Но если в одном экземпляре кеш уже заполнен, а в другом — пуст, производительность может отличаться кардинально. Предел кеша составляет 64 записи, после чего начинается сброс и перезапись по алгоритму LRU, что тоже влияет на скорость.

Почему два одинаковых экземпляра JsonSerializerOptions работают с разной скоростью?

Если вы создаёте JsonSerializerOptions с идентичными настройками, но в разное время, кеш имён может находиться в разном состоянии. Например, первый экземпляр уже обработал JSON с 64 различными ключами — кеш заполнен. Второй экземпляр, созданный после, использует тот же кеш, но если ключи не совпадают, начинается вытеснение. В результате десериализация одного и того же JSON на разных экземплярах может занимать разное время. Автор тестировал на четырёх машинах с разными рантаймами (Core, Mono, Native AOT) и тремя размерами JSON (малый, средний, большой). Разница стабильно воспроизводилась во всех сценариях.

Технические детали: реализация кеша в dotnet/runtime

В исходном коде dotnet/runtime можно найти реализацию кеша имён. JsonSerializerOptions использует ConcurrentDictionary для хранения преобразованных имён свойств. При первом обращении к свойству его имя кешируется, и последующие обращения происходят быстрее. Однако если кеш переполнен (более 64 записей), начинается вытеснение по алгоритму LRU. Это означает, что часто используемые ключи остаются, а редкие удаляются. Если ваш JSON содержит много уникальных ключей, производительность может резко упасть из-за постоянного вытеснения и повторного кеширования.

Интересно, что веб-настройки, например в ASP.NET Core, часто не показывают этой разницы, потому что там кеш инициализируется один раз и используется повторно на протяжении всего жизненного цикла приложения. Проблема возникает в сценариях, где JsonSerializerOptions создаются динамически — например, в библиотеках, при работе с разными схемами JSON или в многопоточных средах, где каждый поток создаёт свой экземпляр.

Кого затронет проблема и как её избежать

Проблема актуальна для разработчиков, которые создают новые экземпляры JsonSerializerOptions для каждого запроса, работают с JSON, содержащим много уникальных ключей (например, логи событий, данные с изменяемой схемой), или используют Native AOT или Mono, где кеш ведёт себя иначе. Для бизнеса это означает неожиданные задержки в высоконагруженных системах, особенно в микросервисной архитектуре, популярной в российских проектах.

Лучшая практика — переиспользовать один экземпляр JsonSerializerOptions, если настройки одинаковы. Создайте его один раз при старте приложения и кешируйте. Если необходимо использовать разные настройки, подумайте о пулинге экземпляров или явном управлении кешем. Также стоит избегать ситуаций, когда один экземпляр обрабатывает JSON с очень большим числом уникальных ключей — это приводит к частому вытеснению и потере производительности.

Что будет дальше: будущие улучшения в .NET

В сообществе .NET уже обсуждают возможные улучшения: увеличение размера кеша, добавление опции его отключения или явного управления. Пока же разработчикам приходится полагаться на текущее поведение. Следите за обновлениями dotnet/runtime: возможно, в следующих версиях появится более предсказуемое и настраиваемое кеширование. В любом случае, понимание этого механизма уже сейчас помогает писать более эффективный код.

Итог

Разница в производительности System.Text.Json до 4,3 раз — не баг, а особенность реализации кеша имён свойств. Разработчикам стоит учитывать это при проектировании высоконагруженных систем и избегать создания новых экземпляров JsonSerializerOptions без необходимости. Кеширование одного экземпляра, правильное управление размером входных данных и мониторинг производительности помогут избежать неожиданных падений скорости. Помните: даже в зрелых фреймворках есть скрытые нюансы, которые могут существенно повлиять на быстродействие.