From: Steve Harter Date: Thu, 12 Sep 2019 20:47:56 +0000 (-0500) Subject: Improve deserialization perf with changes to property name lookup (dotnet/corefx... X-Git-Tag: submit/tizen/20210909.063632~11031^2~473 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=64e7991a1263df77440292f733b04510ed663b78;p=platform%2Fupstream%2Fdotnet%2Fruntime.git Improve deserialization perf with changes to property name lookup (dotnet/corefx#40998) Commit migrated from https://github.com/dotnet/corefx/commit/d3c6628c2594b496ebc17526576cb164fc5fc0bf --- diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs index 2fa525b..ba52359 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs @@ -6,6 +6,7 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Converters; @@ -16,7 +17,9 @@ namespace System.Text.Json internal sealed partial class JsonClassInfo { // The length of the property name embedded in the key (in bytes). - private const int PropertyNameKeyLength = 6; + // 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; // The limit to how many property names from the JSON are cached in _propertyRefsSorted before using PropertyCache. private const int PropertyNameCountCacheThreshold = 64; @@ -257,6 +260,8 @@ namespace System.Text.Json return property; } + // AggressiveInlining used although a large method it is only called from one location and is on a hot path. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public JsonPropertyInfo GetProperty(ReadOnlySpan propertyName, ref ReadStackFrame frame) { JsonPropertyInfo info = null; @@ -264,11 +269,11 @@ namespace System.Text.Json // Keep a local copy of the cache in case it changes by another thread. PropertyRef[] localPropertyRefsSorted = _propertyRefsSorted; + ulong key = GetKey(propertyName); + // If there is an existing cache, then use it. if (localPropertyRefsSorted != null) { - ulong key = GetKey(propertyName); - // Start with the current property index, and then go forwards\backwards. int propertyIndex = frame.PropertyIndex; @@ -276,66 +281,90 @@ namespace System.Text.Json int iForward = Math.Min(propertyIndex, count); int iBackward = iForward - 1; - while (iForward < count || iBackward >= 0) + while (true) { if (iForward < count) { - if (TryIsPropertyRefEqual(localPropertyRefsSorted[iForward], propertyName, key, ref info)) + PropertyRef propertyRef = localPropertyRefsSorted[iForward]; + if (TryIsPropertyRefEqual(propertyRef, propertyName, key, ref info)) { return info; } + ++iForward; - } - if (iBackward >= 0) + if (iBackward >= 0) + { + propertyRef = localPropertyRefsSorted[iBackward]; + if (TryIsPropertyRefEqual(propertyRef, propertyName, key, ref info)) + { + return info; + } + + --iBackward; + } + } + else if (iBackward >= 0) { - if (TryIsPropertyRefEqual(localPropertyRefsSorted[iBackward], propertyName, key, ref info)) + PropertyRef propertyRef = localPropertyRefsSorted[iBackward]; + if (TryIsPropertyRefEqual(propertyRef, propertyName, key, ref info)) { return info; } + --iBackward; } + else + { + // Property was not found. + break; + } } } // No cached item was found. Try the main list which has all of the properties. string stringPropertyName = JsonHelpers.Utf8GetString(propertyName); - if (PropertyCache.TryGetValue(stringPropertyName, out info)) + if (!PropertyCache.TryGetValue(stringPropertyName, out info)) { - // Check if we should add this to the cache. - // Only cache up to a threshold length and then just use the dictionary when an item is not found in the cache. - int count; - if (localPropertyRefsSorted != null) - { - count = localPropertyRefsSorted.Length; - } - else + info = JsonPropertyInfo.s_missingProperty; + } + + Debug.Assert(info != null); + + // Three code paths to get here: + // 1) info == s_missingProperty. Property not found. + // 2) key == info.PropertyNameKey. Exact match found. + // 3) key != info.PropertyNameKey. Match found due to case insensitivity. + Debug.Assert(info == JsonPropertyInfo.s_missingProperty || key == info.PropertyNameKey || Options.PropertyNameCaseInsensitive); + + // Check if we should add this to the cache. + // Only cache up to a threshold length and then just use the dictionary when an item is not found in the cache. + int cacheCount = 0; + if (localPropertyRefsSorted != null) + { + cacheCount = localPropertyRefsSorted.Length; + } + + // Do a quick check for the stable (after warm-up) case. + if (cacheCount < PropertyNameCountCacheThreshold) + { + // Do a slower check for the warm-up case. + if (frame.PropertyRefCache != null) { - count = 0; + cacheCount += frame.PropertyRefCache.Count; } - // Do a quick check for the stable (after warm-up) case. - if (count < PropertyNameCountCacheThreshold) + // Check again to append the cache up to the threshold. + if (cacheCount < PropertyNameCountCacheThreshold) { - // Do a slower check for the warm-up case. - if (frame.PropertyRefCache != null) + if (frame.PropertyRefCache == null) { - count += frame.PropertyRefCache.Count; + frame.PropertyRefCache = new List(); } - // Check again to append the cache up to the threshold. - if (count < PropertyNameCountCacheThreshold) - { - if (frame.PropertyRefCache == null) - { - frame.PropertyRefCache = new List(); - } - - ulong key = info.PropertyNameKey; - PropertyRef propertyRef = new PropertyRef(key, info); - frame.PropertyRefCache.Add(propertyRef); - } + PropertyRef propertyRef = new PropertyRef(key, info); + frame.PropertyRefCache.Add(propertyRef); } } @@ -360,12 +389,13 @@ namespace System.Text.Json public JsonPropertyInfo PolicyProperty { get; private set; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryIsPropertyRefEqual(in PropertyRef propertyRef, ReadOnlySpan propertyName, ulong key, ref JsonPropertyInfo info) { if (key == propertyRef.Key) { + // We compare the whole name, although we could skip the first 7 bytes (but it's not any faster) if (propertyName.Length <= PropertyNameKeyLength || - // We compare the whole name, although we could skip the first 6 bytes (but it's likely not any faster) propertyName.SequenceEqual(propertyRef.Info.Name)) { info = propertyRef.Info; @@ -376,49 +406,70 @@ namespace System.Text.Json return false; } - private static bool IsPropertyRefEqual(ref PropertyRef propertyRef, PropertyRef other) - { - if (propertyRef.Key == other.Key) - { - if (propertyRef.Info.Name.Length <= PropertyNameKeyLength || - propertyRef.Info.Name.AsSpan().SequenceEqual(other.Info.Name.AsSpan())) - { - return true; - } - } - - return false; - } - + /// + /// Get a key from the property name. + /// The key consists of the first 7 bytes of the property name and then the length. + /// public static ulong GetKey(ReadOnlySpan propertyName) { + const int BitsInByte = 8; ulong key; int length = propertyName.Length; - // Embed the propertyName in the first 6 bytes of the key. - if (length > 3) + if (length > 7) + { + key = MemoryMarshal.Read(propertyName); + + // Max out the length byte. + // The comparison logic tests for equality against the full contents instead of just + // the key if the property name length is >7. + key |= 0xFF00000000000000; + } + else if (length > 3) { key = MemoryMarshal.Read(propertyName); - if (length > 4) + + if (length == 7) + { + key |= (ulong) propertyName[6] << (6 * BitsInByte) + | (ulong) propertyName[5] << (5 * BitsInByte) + | (ulong) propertyName[4] << (4 * BitsInByte) + | (ulong) 7 << (7 * BitsInByte); + } + else if (length == 6) + { + key |= (ulong) propertyName[5] << (5 * BitsInByte) + | (ulong) propertyName[4] << (4 * BitsInByte) + | (ulong) 6 << (7 * BitsInByte); + } + else if (length == 5) { - key |= (ulong)propertyName[4] << 32; + key |= (ulong) propertyName[4] << (4 * BitsInByte) + | (ulong) 5 << (7 * BitsInByte); } - if (length > 5) + else { - key |= (ulong)propertyName[5] << 40; + key |= (ulong) 4 << (7 * BitsInByte); } } else if (length > 1) { key = MemoryMarshal.Read(propertyName); - if (length > 2) + + if (length == 3) { - key |= (ulong)propertyName[2] << 16; + key |= (ulong) propertyName[2] << (2 * BitsInByte) + | (ulong) 3 << (7 * BitsInByte); + } + else + { + key |= (ulong) 2 << (7 * BitsInByte); } } else if (length == 1) { - key = propertyName[0]; + key = propertyName[0] + | (ulong) 1 << (7 * BitsInByte); } else { @@ -426,8 +477,16 @@ namespace System.Text.Json key = 0; } - // Embed the propertyName length in the last two bytes. - key |= (ulong)propertyName.Length << 48; + // Verify key contains the embedded bytes as expected. + Debug.Assert( + (length < 1 || propertyName[0] == ((key & ((ulong)0xFF << 8 * 0)) >> 8 * 0)) && + (length < 2 || propertyName[1] == ((key & ((ulong)0xFF << 8 * 1)) >> 8 * 1)) && + (length < 3 || propertyName[2] == ((key & ((ulong)0xFF << 8 * 2)) >> 8 * 2)) && + (length < 4 || propertyName[3] == ((key & ((ulong)0xFF << 8 * 3)) >> 8 * 3)) && + (length < 5 || propertyName[4] == ((key & ((ulong)0xFF << 8 * 4)) >> 8 * 4)) && + (length < 6 || propertyName[5] == ((key & ((ulong)0xFF << 8 * 5)) >> 8 * 5)) && + (length < 7 || propertyName[6] == ((key & ((ulong)0xFF << 8 * 6)) >> 8 * 6))); + return key; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs index ced83b4..92cf2b8 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs @@ -5,11 +5,14 @@ using System.Buffers; using System.Collections; using System.Diagnostics; +using System.Runtime.CompilerServices; namespace System.Text.Json { public static partial class JsonSerializer { + // AggressiveInlining used although a large method it is only called from one locations and is on a hot path. + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void HandlePropertyName( JsonSerializerOptions options, ref Utf8JsonReader reader, @@ -52,7 +55,7 @@ namespace System.Text.Json } JsonPropertyInfo jsonPropertyInfo = state.Current.JsonClassInfo.GetProperty(propertyName, ref state.Current); - if (jsonPropertyInfo == null) + if (jsonPropertyInfo == JsonPropertyInfo.s_missingProperty) { JsonPropertyInfo dataExtProperty = state.Current.JsonClassInfo.DataExtensionProperty; if (dataExtProperty == null) @@ -94,9 +97,10 @@ namespace System.Text.Json state.Current.JsonPropertyInfo.JsonPropertyName = propertyNameArray; } } - - state.Current.PropertyIndex++; } + + // Increment the PropertyIndex so JsonClassInfo.GetProperty() starts with the next property. + state.Current.PropertyIndex++; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleValue.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleValue.cs index c674348..fb15f4e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleValue.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleValue.cs @@ -2,15 +2,19 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Runtime.CompilerServices; + namespace System.Text.Json { public static partial class JsonSerializer { - private static bool HandleValue(JsonTokenType tokenType, JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state) + // AggressiveInlining used although a large method it is only called from two locations and is on a hot path. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void HandleValue(JsonTokenType tokenType, JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state) { if (state.Current.SkipProperty) { - return false; + return; } JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo; @@ -23,10 +27,7 @@ namespace System.Text.Json jsonPropertyInfo = state.Current.JsonClassInfo.CreatePolymorphicProperty(jsonPropertyInfo, typeof(object), options); } - bool lastCall = (!state.Current.IsProcessingEnumerableOrDictionary && state.Current.ReturnValue == null); - jsonPropertyInfo.Read(tokenType, ref state, ref reader); - return lastCall; } } } diff --git a/src/libraries/System.Text.Json/tests/Serialization/PropertyNameTests.cs b/src/libraries/System.Text.Json/tests/Serialization/PropertyNameTests.cs index e039ffd..c5feea3 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/PropertyNameTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/PropertyNameTests.cs @@ -293,6 +293,143 @@ namespace System.Text.Json.Serialization.Tests Assert.False(obj.MyOverflow.ContainsKey("key1")); Assert.Equal(1, obj.MyOverflow["Key1"].GetInt32()); } + + private class ClassWithPropertyNamePermutations + { + public int a { get; set; } + public int aa { get; set; } + public int aaa { get; set; } + public int aaaa { get; set; } + public int aaaaa { get; set; } + public int aaaaaa { get; set; } + + // 7 characters - caching code only keys up to 7. + public int aaaaaaa { get; set; } + public int aaaaaab { get; set; } + + // 8 characters. + public int aaaaaaaa { get; set; } + public int aaaaaaab { get; set; } + + // 9 characters. + public int aaaaaaaaa { get; set; } + public int aaaaaaaab { get; set; } + + public int \u0467 { get; set; } + public int \u0467\u0467 { get; set; } + public int \u0467\u0467a { get; set; } + public int \u0467\u0467b { get; set; } + public int \u0467\u0467\u0467 { get; set; } + public int \u0467\u0467\u0467a { get; set; } + public int \u0467\u0467\u0467b { get; set; } + public int \u0467\u0467\u0467\u0467 { get; set; } + public int \u0467\u0467\u0467\u0467a { get; set; } + public int \u0467\u0467\u0467\u0467b { get; set; } + } + + [Fact] + public static void CachingKeys() + { + ClassWithPropertyNamePermutations obj; + + void Verify() + { + Assert.Equal(1, obj.a); + Assert.Equal(2, obj.aa); + Assert.Equal(3, obj.aaa); + Assert.Equal(4, obj.aaaa); + Assert.Equal(5, obj.aaaaa); + Assert.Equal(6, obj.aaaaaa); + Assert.Equal(7, obj.aaaaaaa); + Assert.Equal(7, obj.aaaaaab); + Assert.Equal(8, obj.aaaaaaaa); + Assert.Equal(8, obj.aaaaaaab); + Assert.Equal(9, obj.aaaaaaaaa); + Assert.Equal(9, obj.aaaaaaaab); + + Assert.Equal(2, obj.\u0467); + Assert.Equal(4, obj.\u0467\u0467); + Assert.Equal(5, obj.\u0467\u0467a); + Assert.Equal(5, obj.\u0467\u0467b); + Assert.Equal(6, obj.\u0467\u0467\u0467); + Assert.Equal(7, obj.\u0467\u0467\u0467a); + Assert.Equal(7, obj.\u0467\u0467\u0467b); + Assert.Equal(8, obj.\u0467\u0467\u0467\u0467); + Assert.Equal(9, obj.\u0467\u0467\u0467\u0467a); + Assert.Equal(9, obj.\u0467\u0467\u0467\u0467b); + } + + obj = new ClassWithPropertyNamePermutations + { + a = 1, + aa = 2, + aaa = 3, + aaaa = 4, + aaaaa = 5, + aaaaaa = 6, + aaaaaaa = 7, + aaaaaab = 7, + aaaaaaaa = 8, + aaaaaaab = 8, + aaaaaaaaa = 9, + aaaaaaaab = 9, + \u0467 = 2, + \u0467\u0467 = 4, + \u0467\u0467a = 5, + \u0467\u0467b = 5, + \u0467\u0467\u0467 = 6, + \u0467\u0467\u0467a = 7, + \u0467\u0467\u0467b = 7, + \u0467\u0467\u0467\u0467 = 8, + \u0467\u0467\u0467\u0467a = 9, + \u0467\u0467\u0467\u0467b = 9, + }; + + // Verify baseline. + Verify(); + + string json = JsonSerializer.Serialize(obj); + + // Verify the length is consistent with a verified value. + Assert.Equal(354, json.Length); + + obj = JsonSerializer.Deserialize(json); + + // Verify round-tripped object. + Verify(); + } + + [Theory] + [InlineData(0x1, 'v')] + [InlineData(0x1, '\u0467')] + [InlineData(0x10, 'v')] + [InlineData(0x10, '\u0467')] + [InlineData(0x100, 'v')] + [InlineData(0x100, '\u0467')] + [InlineData(0x1000, 'v')] + [InlineData(0x1000, '\u0467')] + [InlineData(0x10000, 'v')] + [InlineData(0x10000, '\u0467')] + public static void LongPropertyNames(int propertyLength, char ch) + { + // Although the CLR may limit member length to 1023 bytes, the serializer doesn't have a hard limit. + + string val = new string(ch, propertyLength); + string json = @"{""" + val + @""":1}"; + + EmptyClassWithExtensionProperty obj = JsonSerializer.Deserialize(json); + + Assert.True(obj.MyOverflow.ContainsKey(val)); + + var options = new JsonSerializerOptions + { + // Avoid escaping '\u0467'. + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + string jsonRoundTripped = JsonSerializer.Serialize(obj, options); + Assert.Equal(json, jsonRoundTripped); + } } public class OverridePropertyNameDesignTime_TestClass