Improve deserialization perf with changes to property name lookup (dotnet/corefx...
authorSteve Harter <steveharter@users.noreply.github.com>
Thu, 12 Sep 2019 20:47:56 +0000 (15:47 -0500)
committerGitHub <noreply@github.com>
Thu, 12 Sep 2019 20:47:56 +0000 (15:47 -0500)
Commit migrated from https://github.com/dotnet/corefx/commit/d3c6628c2594b496ebc17526576cb164fc5fc0bf

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleValue.cs
src/libraries/System.Text.Json/tests/Serialization/PropertyNameTests.cs

index 2fa525b..ba52359 100644 (file)
@@ -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<byte> 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<PropertyRef>();
                     }
 
-                    // Check again to append the cache up to the threshold.
-                    if (count < PropertyNameCountCacheThreshold)
-                    {
-                        if (frame.PropertyRefCache == null)
-                        {
-                            frame.PropertyRefCache = new List<PropertyRef>();
-                        }
-
-                        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<byte> 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;
-        }
-
+        /// <summary>
+        /// Get a key from the property name.
+        /// The key consists of the first 7 bytes of the property name and then the length.
+        /// </summary>
         public static ulong GetKey(ReadOnlySpan<byte> 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<ulong>(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<uint>(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<ushort>(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;
         }
 
index ced83b4..92cf2b8 100644 (file)
@@ -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++;
             }
         }
 
index c674348..fb15f4e 100644 (file)
@@ -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;
         }
     }
 }
index e039ffd..c5feea3 100644 (file)
@@ -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<ClassWithPropertyNamePermutations>(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<EmptyClassWithExtensionProperty>(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