From 4a5da0ecd69bfab70881c25cb9c390ca1f073751 Mon Sep 17 00:00:00 2001 From: Steve Harter Date: Mon, 16 Sep 2019 11:20:43 -0500 Subject: [PATCH] Serialization performance improvements (dotnet/corefx#41098) Commit migrated from https://github.com/dotnet/corefx/commit/017a038abaabedcbbc61c85c9f7405f588ca3b46 --- .../Text/Json/Serialization/JsonClassInfo.cs | 21 ++++++++- .../Text/Json/Serialization/JsonPropertyInfo.cs | 3 +- .../JsonSerializer.Write.HandleObject.cs | 55 ++++++++++------------ .../Serialization/JsonSerializer.Write.Helpers.cs | 20 +++----- .../Serialization/JsonSerializer.Write.String.cs | 8 +--- .../Json/Serialization/JsonSerializer.Write.cs | 11 ++--- .../Text/Json/Serialization/WriteStackFrame.cs | 19 ++++++-- 7 files changed, 72 insertions(+), 65 deletions(-) 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 ba52359..1923347 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 @@ -24,9 +24,13 @@ namespace System.Text.Json // The limit to how many property names from the JSON are cached in _propertyRefsSorted before using PropertyCache. private const int PropertyNameCountCacheThreshold = 64; - // All of the serializable properties on a POCO keyed on property name. + // All of the serializable properties on a POCO (except the optional extension property) keyed on property name. public volatile Dictionary PropertyCache; + // All of the serializable properties on a POCO including the optional extension property. + // Used for performance during serialization instead of 'PropertyCache' above. + public volatile JsonPropertyInfo[] PropertyCacheArray; + // Fast cache of properties by first JSON ordering; may not contain all properties. Accessed before PropertyCache. // Use an array (instead of List) for highest performance. private volatile PropertyRef[] _propertyRefsSorted; @@ -159,14 +163,26 @@ namespace System.Text.Json } } + JsonPropertyInfo[] cacheArray; if (DetermineExtensionDataProperty(cache)) { // Remove from cache since it is handled independently. cache.Remove(DataExtensionProperty.NameAsString); + + cacheArray = new JsonPropertyInfo[cache.Count + 1]; + + // Set the last element to the extension property. + cacheArray[cache.Count] = DataExtensionProperty; + } + else + { + cacheArray = new JsonPropertyInfo[cache.Count]; } - // Set as a unit to avoid concurrency issues. + // Set fields when finished to avoid concurrency issues. PropertyCache = cache; + cache.Values.CopyTo(cacheArray, 0); + PropertyCacheArray = cacheArray; } break; case ClassType.Enumerable: @@ -213,6 +229,7 @@ namespace System.Text.Json // Add a single property that maps to the class type so we can have policies applied. AddPolicyProperty(type, options); PropertyCache = new Dictionary(); + PropertyCacheArray = Array.Empty(); break; default: Debug.Fail($"Unexpected class type: {ClassType}"); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs index b61647f..63672b6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs @@ -237,7 +237,8 @@ namespace System.Text.Json public JsonDictionaryConverter DictionaryConverter { get; private set; } // The escaped name passed to the writer. - public JsonEncodedText? EscapedName { get; private set; } + // Use a field here (not a property) to avoid value semantics. + public JsonEncodedText? EscapedName; public static TAttribute GetAttribute(PropertyInfo propertyInfo) where TAttribute : Attribute { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleObject.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleObject.cs index 4395899..2ec63b8 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleObject.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleObject.cs @@ -2,13 +2,15 @@ // 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.Collections.Generic; 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 location and is on a hot path. + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool WriteObject( JsonSerializerOptions options, Utf8JsonWriter writer, @@ -26,32 +28,25 @@ namespace System.Text.Json } state.Current.WriteObjectOrArrayStart(ClassType.Object, writer, options); - state.Current.PropertyEnumerator = state.Current.JsonClassInfo.PropertyCache.GetEnumerator(); state.Current.PropertyEnumeratorActive = true; - state.Current.NextProperty(); + state.Current.MoveToNextProperty = true; } - else if (state.Current.MoveToNextProperty) + + if (state.Current.MoveToNextProperty) { state.Current.NextProperty(); } // Determine if we are done enumerating properties. - // If the ClassType is unknown, there will be a policy property applied - JsonClassInfo classInfo = state.Current.JsonClassInfo; - if (classInfo.ClassType != ClassType.Unknown && state.Current.PropertyEnumeratorActive) + if (state.Current.PropertyEnumeratorActive) { - HandleObject(state.Current.PropertyEnumerator.Current.Value, options, writer, ref state); - return false; - } + // If ClassType.Unknown at this point, we are typeof(object) which should not have any properties. + Debug.Assert(state.Current.JsonClassInfo.ClassType != ClassType.Unknown); - if (state.Current.ExtensionDataStatus == Serialization.ExtensionDataWriteStatus.Writing) - { - JsonPropertyInfo jsonPropertyInfo = state.Current.JsonClassInfo.DataExtensionProperty; - if (jsonPropertyInfo != null) - { - HandleObject(jsonPropertyInfo, options, writer, ref state); - return false; - } + JsonPropertyInfo jsonPropertyInfo = state.Current.JsonClassInfo.PropertyCacheArray[state.Current.PropertyEnumeratorIndex - 1]; + HandleObject(jsonPropertyInfo, options, writer, ref state); + + return false; } writer.WriteEndObject(); @@ -72,11 +67,13 @@ namespace System.Text.Json return true; } - private static bool HandleObject( - JsonPropertyInfo jsonPropertyInfo, - JsonSerializerOptions options, - Utf8JsonWriter writer, - ref WriteStack state) + // AggressiveInlining used although a large method it is only called from one location and is on a hot path. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void HandleObject( + JsonPropertyInfo jsonPropertyInfo, + JsonSerializerOptions options, + Utf8JsonWriter writer, + ref WriteStack state) { Debug.Assert( state.Current.JsonClassInfo.ClassType == ClassType.Object || @@ -85,7 +82,7 @@ namespace System.Text.Json if (!jsonPropertyInfo.ShouldSerialize) { state.Current.MoveToNextProperty = true; - return true; + return; } bool obtainedValue = false; @@ -105,7 +102,7 @@ namespace System.Text.Json { jsonPropertyInfo.Write(ref state, writer); state.Current.MoveToNextProperty = true; - return true; + return; } // A property that returns an enumerator keeps the same stack frame. @@ -117,7 +114,7 @@ namespace System.Text.Json state.Current.MoveToNextProperty = true; } - return endOfEnumerable; + return; } // A property that returns a dictionary keeps the same stack frame. @@ -129,7 +126,7 @@ namespace System.Text.Json state.Current.MoveToNextProperty = true; } - return endOfEnumerable; + return; } // A property that returns a type that is deserialized by passing an @@ -144,7 +141,7 @@ namespace System.Text.Json state.Current.MoveToNextProperty = true; } - return endOfEnumerable; + return; } // A property that returns an object. @@ -174,8 +171,6 @@ namespace System.Text.Json state.Current.MoveToNextProperty = true; } - - return true; } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs index 52552d8..88acff6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs @@ -90,31 +90,25 @@ namespace System.Text.Json return result; } - private static string WriteValueCore(Utf8JsonWriter writer, object value, Type type, JsonSerializerOptions options) + private static void WriteValueCore(Utf8JsonWriter writer, object value, Type type, JsonSerializerOptions options) { if (options == null) { options = JsonSerializerOptions.s_defaultOptions; } - string result; - - using (var output = new PooledByteBufferWriter(options.DefaultBufferSize)) - { - WriteCore(writer, output, value, type, options); - result = JsonReaderHelper.TranscodeHelper(output.WrittenMemory.Span); - } - - return result; + WriteCore(writer, value, type, options); } private static void WriteCore(PooledByteBufferWriter output, object value, Type type, JsonSerializerOptions options) { - using var writer = new Utf8JsonWriter(output, options.GetWriterOptions()); - WriteCore(writer, output, value, type, options); + using (var writer = new Utf8JsonWriter(output, options.GetWriterOptions())) + { + WriteCore(writer, value, type, options); + } } - private static void WriteCore(Utf8JsonWriter writer, PooledByteBufferWriter output, object value, Type type, JsonSerializerOptions options) + private static void WriteCore(Utf8JsonWriter writer, object value, Type type, JsonSerializerOptions options) { Debug.Assert(type != null || value == null); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.String.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.String.cs index 1e5a661..8937e2c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.String.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.String.cs @@ -18,7 +18,7 @@ namespace System.Text.Json /// public static string Serialize(TValue value, JsonSerializerOptions options = null) { - return ToStringInternal(value, typeof(TValue), options); + return WriteCoreString(value, typeof(TValue), options); } /// @@ -35,12 +35,6 @@ namespace System.Text.Json public static string Serialize(object value, Type inputType, JsonSerializerOptions options = null) { VerifyValueAndType(value, inputType); - - return ToStringInternal(value, inputType, options); - } - - private static string ToStringInternal(object value, Type inputType, JsonSerializerOptions options) - { return WriteCoreString(value, inputType, options); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.cs index a133d5a..5d40d26 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.cs @@ -36,24 +36,21 @@ namespace System.Text.Json current.JsonPropertyInfo.Write(ref state, writer); finishedSerializing = true; break; - case ClassType.Object: - finishedSerializing = WriteObject(options, writer, ref state); - break; case ClassType.Dictionary: case ClassType.IDictionaryConstructible: finishedSerializing = HandleDictionary(current.JsonClassInfo.ElementClassInfo, options, writer, ref state); break; default: - Debug.Assert(state.Current.JsonClassInfo.ClassType == ClassType.Unknown); + Debug.Assert(state.Current.JsonClassInfo.ClassType == ClassType.Object || + state.Current.JsonClassInfo.ClassType == ClassType.Unknown); - // Treat typeof(object) as an empty object. finishedSerializing = WriteObject(options, writer, ref state); break; } if (finishedSerializing) { - if (writer.CurrentDepth == 0 || writer.CurrentDepth == originalWriterDepth) + if (writer.CurrentDepth == originalWriterDepth) { break; } @@ -63,7 +60,7 @@ namespace System.Text.Json ThrowHelper.ThrowInvalidOperationException_SerializerCycleDetected(options.MaxDepth); } - // If serialization is not yet end and we surpass beyond flush threshold return false and flush stream. + // If serialization is not finished and we surpass flush threshold then return false which will flush stream. if (flushThreshold >= 0 && writer.BytesPending > flushThreshold) { return false; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs index a409b14..f2b7913 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs @@ -3,8 +3,8 @@ // See the LICENSE file in the project root for more information. using System.Collections; -using System.Collections.Generic; using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Text.Json.Serialization; namespace System.Text.Json @@ -32,8 +32,8 @@ namespace System.Text.Json // The current property. public bool PropertyEnumeratorActive; + public int PropertyEnumeratorIndex; public ExtensionDataWriteStatus ExtensionDataStatus; - public Dictionary.Enumerator PropertyEnumerator; public JsonPropertyInfo JsonPropertyInfo; public void Initialize(Type type, JsonSerializerOptions options) @@ -111,7 +111,7 @@ namespace System.Text.Json ExtensionDataStatus = ExtensionDataWriteStatus.NotStarted; IsIDictionaryConstructible = false; JsonClassInfo = null; - PropertyEnumerator = default; + PropertyEnumeratorIndex = 0; PropertyEnumeratorActive = false; PopStackOnEndCollection = false; PopStackOnEndObject = false; @@ -119,6 +119,7 @@ namespace System.Text.Json EndProperty(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void EndProperty() { IsIDictionaryConstructibleProperty = false; @@ -139,20 +140,28 @@ namespace System.Text.Json PopStackOnEndCollection = false; } + // AggressiveInlining used although a large method it is only called from one location and is on a hot path. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void NextProperty() { EndProperty(); if (PropertyEnumeratorActive) { - if (PropertyEnumerator.MoveNext()) + int len = JsonClassInfo.PropertyCacheArray.Length; + if (PropertyEnumeratorIndex < len) { + if ((PropertyEnumeratorIndex == len - 1) && JsonClassInfo.DataExtensionProperty != null) + { + ExtensionDataStatus = ExtensionDataWriteStatus.Writing; + } + + PropertyEnumeratorIndex++; PropertyEnumeratorActive = true; } else { PropertyEnumeratorActive = false; - ExtensionDataStatus = ExtensionDataWriteStatus.Writing; } } else -- 2.7.4