Уважаемые читатели, в этой статье я хочу разобраться, сколько времени добавляет PropertyNameCaseInsensitive, и представить свои выводы.
Фронт присылает camelCase, свойства в C# названы с большой буквы. Принять такой JSON можно двумя способами:
-
PropertyNameCaseInsensitive = true — сравнивать имена ключей с именами свойств без учёта регистра;
-
PropertyNamingPolicy = JsonNamingPolicy.CamelCase— привести имена свойств к camelCase, чтобы они совпали с ключами.
По времени они одинаковые: 0,92–1,09 на четырёх машинах, трёх рантаймах и трёх размерах входного JSON. Разницу до ×4,3 даёт другое: этот же JsonSerializerOptions уже читал те же ключи в другом регистре. По четырём машинам 3,18–4,28.
Будет 5 историй:
-
сколько добавляет флаг, если ключи приходят в одном регистре;
-
что меняется, когда в этих настройках появляется второй регистр тех же ключей;
-
сколько таких регистров нужно, чтобы разрыв стал заметен, и на каком месте он окажется;
-
почему на объекте из 64 свойств кеш заканчивается уже на втором регистре;
-
влияет ли на это длина имён.
Код — в репозитории JsonKeyProof. Четыре машины:
|
№ |
CPU |
Ядра/потоки |
Частота |
ОС |
|
№1 |
AMD Ryzen 9 5950X |
16 / 32 |
3,4 ГГц, до 4,9 в турбо |
Windows 10 1809 |
|
№2 |
Intel Core i9-10900KF |
10 / 20 |
3,7 ГГц, до 5,3 в турбо |
Windows 10 22H2 |
|
№3 |
2 × Intel Xeon Silver 4314 |
2×16 / 64 |
2,4 ГГц, до 3,4 в турбо |
Windows Server 2022 |
|
№4 |
Intel Xeon W-2255 |
10 / 20 |
3,7 ГГц, до 4,5 в турбо |
Windows Server 2022 |
BenchmarkDotNet 0.15.8, Release, .NET 8, 9 и 10. Таблицы по .NET 10, отличия на восьмёрке и девятке разбираю в третьей и четвёртой историях. Полные отчёты в репозитории.
История 1. Что даёт флаг, если весь JSON приходит в одном регистре
Объект из восьми свойств типа int, имена ключей по 2–4 байта. Наборы JSON отличаются только регистром ключей, а по длине в байтах совпадают — это проверяет сверка перед прогоном.
Каждый регистр читается своим экземпляром JsonSerializerOptions. Одним тут не обойтись: метаданные типа лежат не в самом экземпляре, а в общем кеше. Ключ к этому кешу — настройки, поэтому два экземпляра с одинаковыми полями получат один и тот же JsonTypeInfo.
// dotnet/runtime, System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs// сокращено/// Context can be shared across multiple equivalent options instances.internal CachingContext CacheContextpublic static CachingContext GetOrCreate(JsonSerializerOptions options){ int hashCode = s_optionsComparer.GetHashCode(options); // ... поиск готового контекста по этому коду}public bool Equals(JsonSerializerOptions? left, JsonSerializerOptions? right){ return left._jsonPropertyNamingPolicy == right._jsonPropertyNamingPolicy && left._propertyNameCaseInsensitive == right._propertyNameCaseInsensitive && left._typeInfoResolver == right._typeInfoResolver && // ... остальные поля}
Резолвер метаданных сравнивается по ссылке, поэтому свой DefaultJsonTypeInfoResolver у каждого экземпляра делает их разными. Сверка перед прогоном это проверяет, иначе замер не запустится.
// JsonKeyProof, Subjects.cspublic static readonly JsonSerializerOptions OwnCamel = new(){ PropertyNameCaseInsensitive = true, TypeInfoResolver = new DefaultJsonTypeInfoResolver()};
|
Что меряем |
№1 Ryzen |
№2 i9 |
№3 Xeon Silver |
№4 Xeon W |
|
точный регистр, флаг выключен |
1,00 |
1,00 |
1,00 |
1,00 |
|
точный регистр, флаг включён |
1,00 |
1,00 |
0,97 |
1,08 |
|
ЗАГЛАВНЫЕ, флаг включён |
1,00 |
1,00 |
1,03 |
1,02 |
|
camelCase, флаг включён |
0,99 |
0,99 |
0,98 |
1,06 |
|
camelCase, политика именования |
1,00 |
0,99 |
1,00 |
1,07 |
Отношение к первой строке, 1 000 объектов, .NET 10. На каждый регистр ключей — свой экземпляр настроек
Из таблицы можно сделать выводы:
-
флаг ничего не добавляет, когда регистр ключей совпал: 0,97–1,08;
-
когда не совпал — тоже: 1,00–1,03 на ЗАГЛАВНЫХ;
-
на camelCase то же самое: 0,98–1,06;
-
политика именования даёт 0,99–1,07.
На машине №4 разница между строками доходит до 8%, но до 1,07 там дотягивает и строка с политикой именования, где флага нет — это разброс машины. На остальных трёх разница не больше 3%. На всех трёх рантаймах и всех размерах входного JSON любая из четырёх строк укладывается в 0,92–1,09.
Свойство по имени ищется в два захода: сначала по массиву, где под каждый вариант ключа есть отдельная запись, и только при промахе — по словарю имён.
// dotnet/runtime, System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs// сокращеноprivate PropertyRef[] _utf8PropertyCache = [];internal JsonPropertyInfo? GetProperty(ReadOnlySpan<byte> propertyName, ref ReadStackFrame frame, out byte[] utf8PropertyName){ // The logic can be broken up into roughly three stages: // 1. Look up the UTF-8 property cache for potential exact matches in the encoding. // 2. If no match is found, decode to UTF-16 and look up the primary dictionary. // 3. Store the new result for potential inclusion to the UTF-8 cache once deserialization is complete. PropertyRef[] utf8PropertyCache = _utf8PropertyCache; ReadOnlySpan<PropertyRef> utf8PropertyCacheSpan = utf8PropertyCache; ulong key = PropertyRef.GetKey(propertyName); // ... проход по массиву записей // No cached item was found. Try the main dictionary which has all of the properties. if (PropertyIndex.TryLookupUtf8Key(propertyName, out JsonPropertyInfo? info) && (!Options.PropertyNameCaseInsensitive || propertyName.SequenceEqual(info.NameAsUtf8Bytes))) { // We have an exact match in UTF8 encoding. utf8PropertyName = info.NameAsUtf8Bytes; }}
До массива флаг не доходит. Он работает в двух местах, и оба не на каждом ключе: сверка SequenceEqual на второй ветке и выбор компаратора для словаря. Компаратор выбирается один раз на тип, когда собираются метаданные.
// dotnet/runtime, System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs// сокращеноinternal void ConfigureProperties(){ // ... сбор свойств типа StringComparer comparer = Options.PropertyNameCaseInsensitive ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; Dictionary<string, JsonPropertyInfo> propertyIndex = new(properties.Count, comparer);}
Политика именования применяется ещё раньше — при создании JsonPropertyInfo, то есть один раз на свойство типа.
// dotnet/runtime,// System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs// сокращеноprivate static void DeterminePropertyName(JsonPropertyInfo propertyInfo, MemberInfo memberInfo){ JsonPropertyNameAttribute? nameAttribute = memberInfo.GetCustomAttribute<JsonPropertyNameAttribute>(inherit: false); string? name; if (nameAttribute != null) { name = nameAttribute.Name; } else if (propertyInfo.Options.PropertyNamingPolicy != null) { name = propertyInfo.Options.PropertyNamingPolicy.ConvertName(memberInfo.Name); } else { name = memberInfo.Name; } propertyInfo.Name = name;}
В документации есть предупреждение про накладные расходы, но не отмечено, когда они появятся. Раздел Remarks на странице PropertyNameCaseInsensitive:
// learn.microsoft.com, JsonSerializerOptions.PropertyNameCaseInsensitiveThere is a performance cost associated with case-insensitive comparison
Предупреждение верное — речь про вторую ветку. Условие есть в обращении dotnet/runtime #35848, где этот поиск переводили на массив записей: если входной JSON уже совпадает с тем, что даёт политика camelCase, прироста от правки не будет — промахов нет. Обращение старое, времён .NET 5, но с тех пор поиск только дорабатывали: все листинги в статье взяты из ветки release/10.0. Обратный случай в документации не описан.
Пока в настройки приходит один регистр ключей, флаг на время не влияет.
ASP.NET работает в режиме Web, а там уже включены и флаг, и политика:
// dotnet/runtime, System/Text/Json/Serialization/JsonSerializerOptions.cs// сокращеноpublic JsonSerializerOptions(JsonSerializerDefaults defaults) : this(){ if (defaults == JsonSerializerDefaults.Web) { _propertyNameCaseInsensitive = true; _jsonPropertyNamingPolicy = JsonNamingPolicy.CamelCase; _numberHandling = JsonNumberHandling.AllowReadingFromString; }}
Свойства становятся camelCase, входной camelCase с ними совпадает, и в кеш попадает один регистр. Поэтому на веб-настройках ничего не заметно — пока не придёт второй.
Остаётся проверить генератор исходного кода.
|
Что меряем |
№1 Ryzen |
№2 i9 |
№3 Xeon Silver |
№4 Xeon W |
|
короткие имена, рефлексия |
1,00 |
1,00 |
1,00 |
1,00 |
|
короткие имена, генератор |
0,91 |
0,99 |
0,93 |
0,97 |
|
длинные имена, рефлексия |
1,04 |
1,06 |
1,02 |
1,10 |
|
длинные имена, генератор |
1,00 |
1,04 |
1,05 |
1,05 |
|
ЗАГЛАВНЫЕ с флагом, рефлексия |
1,00 |
1,02 |
0,97 |
1,03 |
|
ЗАГЛАВНЫЕ с флагом, генератор |
0,98 |
0,96 |
0,93 |
0,94 |
Отношение к первой строке, 1 000 объектов, .NET 10
Разбор через генератор исходного кода занимает 0,91–1,03 от разбора через рефлексию на тех же данных. Поиск свойства по имени написан в общей части сериализатора, генератор его не подменяет — дальше всё одинаково для обоих.
История 2. Что меняется, когда приходит второй регистр
Всё то же самое, только все три регистра читает один экземпляр настроек. Служба принимает JSON от нескольких источников, а настройки одни на всех.
|
Что меряем |
№1 Ryzen |
№2 i9 |
№3 Xeon Silver |
№4 Xeon W |
|
точный регистр, флаг выключен |
1,00 |
1,00 |
1,00 |
1,00 |
|
точный регистр, флаг включён |
1,01 |
1,01 |
1,00 |
1,03 |
|
ЗАГЛАВНЫЕ, флаг включён |
1,17 |
1,37 |
1,47 |
1,34 |
|
camelCase, флаг включён |
1,45 |
1,57 |
1,80 |
1,76 |
|
camelCase, политика именования |
1,05 |
1,00 |
0,99 |
1,01 |
Отношение к первой строке, 1 000 объектов, .NET 10. Один экземпляр настроек на все три регистра
Из таблицы можно сделать выводы:
-
флаг при совпадающем регистре ничего не добавляет: 1,00–1,03;
-
ЗАГЛАВНЫЕ попали в настройки вторыми — 1,17–1,47;
-
camelCase попал третьим — 1,45–1,80;
-
политика именования даёт 0,99–1,05 — как разбор без настроек.
Строка с camelCase — тот же вызов на тех же данных, что и в первой таблице, где он дал 0,98–1,06. Настройки совпадают по всем полям, флаг и объект те же. Отличается только то, что эти настройки читали раньше. Значит дело не в регистре.
Что успело накопиться, показывает отдельный отчёт из репозитория:
// JsonKeyProof, вывод dotnet run -c Release -f net10.0 -- cache=== Восемь свойств, короткие имена: записей в кеше ===Настройки Что через них прошло ЗаписейPlain точный регистр, без флага 8OwnExact точный регистр 8OwnUpper ЗАГЛАВНЫЕ 8OwnCamel camelCase 8Policy camelCase, политика именования 8Shared все три регистра 24
24 записи вместо 8 — это три регистра по восемь ключей. Сравниваются они побайтово, поэтому Id, ID и id занимают в массиве три разные записи:
// dotnet/runtime, System/Text/Json/Serialization/Metadata/PropertyRef.cs// сокращено/// PropertyRefs use byte sequence equality, so equal JSON strings with alternate encodings or casings are not equal.// The key is a ulong (8 bytes) containing the first 7 bytes of the property name// followed by a byte representing the length.private const int PropertyNameKeyLength = 7;public bool Equals(ReadOnlySpan<byte> propertyName, ulong key){ // If the property name is less than 8 bytes, it is embedded in the key so no further comparison is necessary. return key == Key && (propertyName.Length <= PropertyNameKeyLength || propertyName.SequenceEqual(Utf8PropertyName));}
Обход массива стартует не с нуля, а с номера свойства в текущем объекте, и дальше идёт в обе стороны:
// dotnet/runtime, System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs// сокращено// Start with the current property index, and then go forwards\backwards.int propertyIndex = frame.PropertyIndex;int count = utf8PropertyCacheSpan.Length;int iForward = Math.Min(propertyIndex, count);int iBackward = iForward - 1;
Что такое frame.PropertyIndex, видно на вызывающей стороне: это счётчик свойств внутри текущего объекта, и растёт он на каждом разобранном ключе.
// dotnet/runtime,// System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs// сокращеноJsonPropertyInfo? jsonPropertyInfo = jsonTypeInfo.GetProperty( unescapedPropertyName, ref state.Current, out byte[] utf8PropertyName);// Increment PropertyIndex so GetProperty() checks the next property first when called again.state.Current.PropertyIndex++;
// dotnet/runtime,// System/Text/Json/Serialization/Metadata/PropertyRefCacheBuilder.cs// сокращеноpublic PropertyRef[] ToArray() => [.. OriginalCache, .. _propertyRefs];public void TryAdd(PropertyRef propertyRef){ if (_added.Add(propertyRef)) { _propertyRefs.Add(propertyRef); }}
Новые записи дописываются в конец, старые остаются на своих местах. Отсюда и порядок в кеше после трёх регистров:
-
первые 8 записей — точный регистр;
-
следующие 8 — ЗАГЛАВНЫЕ;
-
последние 8 — camelCase.
Пока регистр один, поиск попадает в свойство с первого шага: третье свойство объекта стоит в кеше третьим. Регистр, попавший третьим, отодвинут на 16 позиций — это 16 шагов. Тот, что попал вторым, — 8 шагов, и разрыв меньше: 1,17–1,47 против 1,45–1,80.
Остаётся понять, много это или мало на фоне всего разбора.
|
Что меряем |
№1 Ryzen |
№2 i9 |
№3 Xeon Silver |
№4 Xeon W |
|
проход по токенам, короткие имена |
1,00 |
1,00 |
1,00 |
1,00 |
|
проход по токенам, длинные имена |
1,02 |
1,02 |
1,05 |
0,95 |
|
проход со сравнением одного имени |
1,06 |
1,05 |
1,01 |
1,08 |
|
полный разбор, короткие имена |
2,28 |
2,46 |
2,06 |
2,21 |
|
полный разбор, длинные имена |
2,31 |
2,69 |
2,37 |
2,40 |
Отношение к первой строке, 1 000 объектов, .NET 10. Проход по токенам идёт через Utf8JsonReader, без привязки к типу
Полный разбор занимает в 2,06–2,46 раза больше, чем проход по токенам с таким же набором. Значит 51–59% времени уходит не на чтение байт, а на всё остальное, включая обход кеша имён.
Разрыв даёт не регистр ключей, а то, каким по счёту он попал в кеш.
На восьми int разбор значений почти ничего не занимает. На объекте со строками, датой и дробным числом разрыв меньше. Значения у каждого объекта свои, их генерирует Bogus с зафиксированным сидом, поэтому набор повторяется от прогона к прогону.
|
Что меряем |
№1 Ryzen |
№2 i9 |
№3 Xeon Silver |
№4 Xeon W |
|
точный регистр, флаг выключен |
1,00 |
1,00 |
1,00 |
1,00 |
|
camelCase, свой экземпляр настроек |
1,02 |
1,00 |
0,99 |
1,00 |
|
camelCase, общий экземпляр настроек |
1,24 |
1,25 |
1,25 |
1,28 |
|
camelCase, политика именования |
1,03 |
1,01 |
1,06 |
1,01 |
Отношение к первой строке, 1 000 объектов, .NET 10. Восемь свойств: два числа, две строки, дата, дробное число и признак
1,24–1,28 вместо 1,45–1,80. Направление то же, разрыв меньше в 1,9–3,2 раза.
История 3. Сколько регистров в кеше и на каком месте нужный
Восемь свойств, имена ровно по 7 байт. Длина выбрана не случайно. Из семи букв выходит 128 вариантов регистра, а точным, ЗАГЛАВНЫМИ и camelCase до предела кеша не дойти. И 7 байт помещаются в восьмибайтовый ключ записи, поэтому он сравнивается как число, без побайтовой сверки имени.
Вариант номер k получается так: переворачиваем регистр тех букв, чей бит стоит в числе k. Первый экземпляр настроек читает один вариант, второй — два, дальше три, восемь и девять. Читаем всякий раз тем вариантом, который попал в кеш последним. Полая точка на графике — тот же экземпляр, но читаем первым.
Из графика можно сделать выводы:
-
один вариант от разбора без флага не отличается: 0,98–1,02;
-
второй — 1,31–1,52;
-
третий, прочитанный первым, — 0,98–1,07;
-
он же, прочитанный третьим, — 1,49–1,91;
-
восьмой — 2,94–4,25;
-
девятый — 3,96–6,96.
Третий и четвёртый пункт сняты одним экземпляром настроек. Записей в кеше поровну, 24, наборы отличаются только регистром букв и совпадают по длине в байтах. Читаем первым вариантом — 0,98–1,07, третьим — 1,49–1,91. Размер кеша тут ни при чём, работает только позиция.
Одна точка выпадает: на машине №1 восьмой вариант дал 4,25 при 2,74 на десяти объектах и 2,75 на ста тысячах — та же машина, тот же рантайм. На .NET 8 и .NET 9 эта же машина даёт 2,77. Из тридцати шести сочетаний рантайма, размера и машины 8-й вариант оказался медленнее 9-го только тут. Верхняя граница диапазона 2,94–4,25 взята с этой точки — по остальным трём машинам он даёт 2,94–3,96.
С 9-го варианта к разрыву добавляются аллокации: 71,38 КБ во всех точках, кроме последней, и 321,38 КБ в ней. Кеш вмещает 64 записи:
// dotnet/runtime,// System/Text/Json/Serialization/Metadata/PropertyRefCacheBuilder.cs// сокращеноpublic const int MaxCapacity = 64;
Дойдя до предела, сериализатор перестаёт пополнять кеш:
// dotnet/runtime, System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs// сокращено// Assuming there is capacity, store the new result for potential// inclusion to the UTF-8 cache once deserialization is complete.ref PropertyRefCacheBuilder? cacheBuilder = ref frame.PropertyRefCacheBuilder;if ((cacheBuilder?.TotalCount ?? utf8PropertyCache.Length) < PropertyRefCacheBuilder.MaxCapacity){ (cacheBuilder ??= new(utf8PropertyCache)).TryAdd(new(key, info, utf8PropertyName));}
Восемь вариантов по восемь ключей — ровно 64 записи. Для 9-го места уже нет: любой ключ в любом объекте проходит весь кеш впустую, уходит в словарь, а имя копируется в новый массив.
// dotnet/runtime, System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs// сокращеноelse{ // Make a copy of the original Span. utf8PropertyName = propertyName.ToArray();}
8000 ключей на 1000 объектов, 250 КБ — 32 байта на ключ. Столько и занимает в куче массив из семи байт на x64: 16 байт заголовка, 8 на длину, 7 данных, округление вверх до восьми.
На .NET 8 и .NET 9 та же таблица: третий вариант — 1,44–1,67 и 1,42–1,88, девятый — 3,78–5,36 и 4,07–6,00.
Внутри библиотеки этот кеш переписывали от версии к версии, но набирает он одинаково: отчёт даёт те же 8, 16, 24, 64 и 64 записи на всех трёх рантаймах и на всех четырёх машинах.
Разрыв растёт вместе с числом вариантов, прошедших через настройки, а после 64-й записи добавляются аллокации.
История 4. Объект из 64 свойств
Предел один — 64 записи, сколько бы свойств у типа ни было. На восьми свойствах это восемь регистров, на 64 — один.
|
Что меряем |
№1 Ryzen |
№2 i9 |
№3 Xeon Silver |
№4 Xeon W |
|
точный регистр, флаг выключен |
1,00 |
1,00 |
1,00 |
1,00 |
|
точный регистр, флаг включён |
1,01 |
1,05 |
0,98 |
1,02 |
|
ЗАГЛАВНЫЕ, свой экземпляр настроек |
1,03 |
1,01 |
0,99 |
1,04 |
|
ЗАГЛАВНЫЕ после точного регистра |
3,18 |
3,38 |
4,28 |
3,37 |
Отношение к первой строке, 1 000 объектов, .NET 10. Объект из 64 свойств типа int с короткими именами
Из таблицы можно сделать выводы:
-
на объекте из 64 свойств флаг ничего не добавляет: 0,98–1,05;
-
другой регистр, если он единственный, — 0,99–1,04;
-
второй регистр через те же настройки — 3,18–4,28.
Аллокации: 290,13 КБ на первых трёх строках и 2290,13 КБ на последней. Два мегабайта — это копии имён: 64 ключа в каждом из 1000 объектов, 64 000 копий по 32 байта. Те же 32 байта, что и в третьей истории, только теперь на другом типе и другом количестве ключей. Первый вариант занял все 64 записи кеша, а для второго места нет.
На .NET 8 и .NET 9 последняя строка — 3,31–4,22 и 3,61–5,84.
Чем больше у типа свойств, тем меньше вариантов помещается в кеш. На 64 свойствах туда попадает только первый.
История 5. Длина имён ключей
Имя до 7 байт помещается в ключ записи целиком, вместе с длиной, и ключи сравниваются как числа. Длинные идут на побайтовую проверку.
// dotnet/runtime, System/Text/Json/Serialization/Metadata/PropertyRef.cs// сокращеноpublic static ulong GetKey(ReadOnlySpan<byte> name){ int length = name.Length; ulong key = (ulong)(byte)length << 56; key |= length switch { 0 => 0, 1 => name[0], // ... длины со второй по шестую собираются так же, по частям 7 => MemoryMarshal.Read<uint>(name) | ((ulong)MemoryMarshal.Read<ushort>(name.Slice(4, 2)) << 32) | ((ulong)name[6] << 48), _ => MemoryMarshal.Read<ulong>(name) & 0x00ffffffffffffffUL }; return key;}
Старший байт ключа — длина имени, младшие семь — его первые семь байт. Имена от восьми байт берутся одним обращением к памяти и обрезаются маской; короткие собираются по частям: восемь байт из имени длиной шесть уже не прочитать.
Отсюда и граница. Если имя не длиннее семи байт, ключ содержит его целиком: совпали ключи — совпали и имена, сверять байты незачем. А что на длинных именах сериализатор доходит до байтов, видно в PropertyRef.Equals из второй истории.
Из графика можно сделать выводы:
-
весь ряд от 2 до 64 байт держится в 0,97–1,14;
-
между 7 и 8 байтами разницы нет: 0,97–1,05 против 1,01–1,08.
Рядом два случая, которые из этого ряда выпадают: имя, заданное атрибутом, и кириллица.
|
Что меряем |
№1 Ryzen |
№2 i9 |
№3 Xeon Silver |
№4 Xeon W |
|
2–4 байта |
1,00 |
1,00 |
1,00 |
1,00 |
|
2–4 байта, имя из атрибута |
1,02 |
1,01 |
1,00 |
0,98 |
|
кириллица |
0,91 |
1,05 |
1,08 |
1,01 |
Отношение к первой строке, 1 000 объектов, .NET 10. Набор второй строки совпадает с первой до байта, отличается только объявление типа
Атрибут не добавляет ничего: 0,98–1,02 на том же наборе байт. Кириллица — 0,91–1,08, хотя буква там занимает два байта и имя из четырёх букв в ключ записи уже не помещается.
Длина сама по себе не даёт ничего. Остаётся проверить её вместе с другим регистром.
|
Что меряем |
№1 Ryzen |
№2 i9 |
№3 Xeon Silver |
№4 Xeon W |
|
короткие имена, точный регистр |
1,00 |
1,00 |
1,00 |
1,00 |
|
короткие имена, ЗАГЛАВНЫЕ |
1,01 |
0,99 |
0,99 |
0,99 |
|
длинные имена, точный регистр |
1,04 |
1,07 |
1,10 |
1,06 |
|
длинные имена, ЗАГЛАВНЫЕ |
1,04 |
1,07 |
1,14 |
1,04 |
Отношение к первой строке, 1 000 объектов, .NET 10. Флаг включён во всех строках, у каждого регистра свой экземпляр настроек
Другой регистр на коротких именах — 0,99–1,01, на длинных — 0,98–1,03. Длинные имена добавляют 1,04–1,10, и регистр тут ни при чём. Разрыв из третьей истории даёт только позиция в кеше.
Выводы
По цифрам:
-
PropertyNameCaseInsensitiveне влияет на время, пока ключи приходят в одном регистре: 0,92–1,09; -
JsonNamingPolicy.CamelCase там же — 0,95–1,07;
-
второй регистр тех же ключей через тот же экземпляр настроек — 1,17–1,47, третий — 1,45–1,80;
-
на именах по 7 байт то же самое: второй — 1,31–1,52, третий — 1,49–1,91, восьмой — 2,94–4,25, девятый — 3,96–6,96 и лишние 250 КБ на 1000 объектов;
-
тот же третий регистр, попавший в кеш первым, — 0,98–1,07;
-
на объекте из 64 свойств второй регистр даёт 3,18–4,28 и лишние 2 МБ;
-
на объекте со строками, датой и дробным числом разрыв 1,24–1,28 вместо 1,45–1,80;
-
длина имён при другом регистре ничего не добавляет: 0,98–1,03;
-
генератор исходного кода ничего не меняет: 0,91–1,03.
Что делать на практике:
-
под каждый регистр ключей — свой JsonSerializerOptions, тогда те самые 1,31–1,91 не появятся;
-
одного new мало: настройки с одинаковыми полями делят метаданные, а с ними и кеш имён. Свой резолвер каждому — и они не совпадут;
-
если приходит только camelCase, берите JsonNamingPolicy.CamelCase: на время не влияет, а в кеше остаётся один регистр;
-
считайте запас заранее: 64 записи — это 8 регистров по 8 ключей или один регистр на 64 свойства, и чем больше свойств у типа, тем меньше регистров туда влезет;
-
аллокации на разборе выросли — значит появился источник с другим регистром ключей, и ему нужен отдельный
JsonSerializerOptions.
Код из статьи
-
JsonKeyProof — бенчмарки, сверки и прогоны на четырёх машинах
Ссылки
Всем удачи и до новых встреч!
ссылка на оригинал статьи https://habr.com/ru/articles/1065092/