Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve serializer performance #57327

Merged
merged 7 commits into from
Aug 15, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace System.Text.Json.Serialization.Converters
{
internal class FSharpTypeConverterFactory : JsonConverterFactory
internal sealed class FSharpTypeConverterFactory : JsonConverterFactory
{
// Temporary solution to account for not implemented support for type-level attributes
// TODO remove once addressed https://github.com/mono/linker/issues/1742#issuecomment-875036480
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ internal class ObjectDefaultConverter<T> : JsonObjectConverter<T> where T : notn
{
internal override bool CanHaveIdMetadata => true;

#if NET6_0_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
Copy link
Member

@eiriktsarpalis eiriktsarpalis Aug 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, what are some of the optimizations that were being missed without this attribute? I had tried using it a few months back on the JsonConverter<T>.TryWrite method (which is also fairly large) and did not receive any perf improvements.

Copy link
Member Author

@steveharter steveharter Aug 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure how to find this out, with tiered jitting and all (e.g. hit a breakpoint after running a 1,000 times to inspect the native code?). Also, I did not run with crossgen2 yet, but typically that has the same results once the warm-up phase is done.

UPDATE: the assembly can be viewed by a checked build and using DOTNET_JitDisasm=methodName.

The results are small when there are a couple properties but should be greater when there are more properties. I had to run benchmarks a few times to make sure noise isn't a factor. I welcome investigation from others on this.

Here's some results with and without AggressiveOptimization. Again, I assume the more properties, the more savings. Also I tend to focus on the Min column more than Mean.

With AO and without

Using a class with 2 string properties; I ran 5 times and took the fastest.

With AO:

Method Mean Error StdDev Gen 0 Allocated
Serialize 197.1 ns 0.34 ns 0.27 ns - -
Deserialize 330.1 ns 0.42 ns 0.38 ns 0.0161 104 B

Without AO:

Method Mean Error StdDev Gen 0 Allocated
Serialize 198.8 ns 0.10 ns 0.09 ns - -
Deserialize 333.1 ns 1.14 ns 1.01 ns 0.0161 104 B

Running ReadJson<LargeStructWithProperties> and WriteJson<LargeStructWithProperties> since it has 10 properties. I ran the benchmarks 3 times and took the fastest. The "Stream" cases didn't get any faster -- not sure why, although that does have a different code path in the object converter.

With AO:

Method Mean Error StdDev Median Min Max Gen 0 Allocated
SerializeToString 650.1 ns 5.49 ns 4.87 ns 648.0 ns 643.8 ns 656.6 ns 0.0765 488 B
SerializeToUtf8Bytes 618.7 ns 7.81 ns 7.31 ns 615.9 ns 609.7 ns 633.3 ns 0.0577 376 B
SerializeToStream 766.6 ns 10.86 ns 9.63 ns 766.1 ns 754.1 ns 783.0 ns 0.0368 232 B
SerializeObjectProperty 831.6 ns 13.94 ns 11.64 ns 828.7 ns 819.1 ns 862.5 ns 0.1347 848 B

Without AO:

Method Mean Error StdDev Median Min Max Gen 0 Allocated
SerializeToString 667.1 ns 2.42 ns 2.15 ns 667.5 ns 661.5 ns 669.4 ns 0.0754 488 B
SerializeToUtf8Bytes 620.1 ns 3.00 ns 2.66 ns 620.0 ns 615.0 ns 624.4 ns 0.0598 376 B
SerializeToStream 760.7 ns 0.80 ns 0.67 ns 760.6 ns 759.8 ns 762.1 ns 0.0370 232 B
SerializeObjectProperty 839.5 ns 8.38 ns 7.84 ns 838.3 ns 826.0 ns 851.7 ns 0.1340 848 B

With AO:

Method Mean Error StdDev Median Min Max Gen 0 Allocated
DeserializeFromString 1.114 us 0.0083 us 0.0070 us 1.114 us 1.104 us 1.131 us 0.0318 200 B
DeserializeFromUtf8Bytes 1.042 us 0.0118 us 0.0111 us 1.040 us 1.028 us 1.065 us 0.0289 200 B
DeserializeFromStream 1.519 us 0.0189 us 0.0176 us 1.516 us 1.493 us 1.556 us 0.0478 328 B

Without AO:

Method Mean Error StdDev Median Min Max Gen 0 Allocated
DeserializeFromString 1.151 us 0.0046 us 0.0041 us 1.152 us 1.143 us 1.156 us 0.0276 200 B
DeserializeFromUtf8Bytes 1.063 us 0.0141 us 0.0132 us 1.065 us 1.043 us 1.081 us 0.0295 200 B
DeserializeFromStream 1.497 us 0.0053 us 0.0047 us 1.496 us 1.489 us 1.507 us 0.0479 328 B

Copy link
Member Author

@steveharter steveharter Aug 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the end, after running thousands of times, the jitted (native) code may be more or less the same with or without the [AO]. But [AO] should produce faster code sooner.

Copy link
Member

@EgorBo EgorBo Aug 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please avoid [AO]?
Methods with [AO] aren't instrumented so won't be able to benefit from PGO (devirtualize calls, move cold parts of methods further from hot ones, etc) - e.g. run your benchmark again with DOTNET_ReadyToRun=0, DOTNET_TC_QuickJitForLoops=1, DOTNET_TieredPGO=1 with and without [AO]

Also, when a method is promoted to tier1 naturally (without [AO]`) we can:

  1. Get rid of static initializations in codegen
  2. Convert static readonly fields to constants
  3. Inliner is able to resolve call tokens and produce better results

/cc @AndyAyersMS

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[AO] should be avoided. It causes methods to be optimized prematurely. We use it only in a handful of places where we feel like we can't afford to ever run unoptimized code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I'll re-run with those settings.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #58209 for reverting the 4 usages of [AO]. Note I was going to keep the 2 in JsonConverter<T> since there does seem to benefits at least sometimes, but for V7 I think it makes sense to remove them based upon offline discussion with @EgorBo and "DynamicPGO" scenarios. I don't plan on porting that PR to V6.

#endif
internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, [MaybeNullWhen(false)] out T value)
{
JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo;
Expand Down Expand Up @@ -246,6 +249,9 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,
return true;
}

#if NET6_0_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
#endif
internal sealed override bool OnTryWrite(
Utf8JsonWriter writer,
T value,
Expand All @@ -272,7 +278,7 @@ internal sealed override bool OnTryWrite(
onSerializing.OnSerializing();
}

List<KeyValuePair<string, JsonPropertyInfo?>> properties = state.Current.JsonTypeInfo.PropertyCache!.List;
List<KeyValuePair<string, JsonPropertyInfo?>> properties = jsonTypeInfo.PropertyCache!.List;
for (int i = 0; i < properties.Count; i++)
{
JsonPropertyInfo jsonPropertyInfo = properties[i].Value!;
Expand All @@ -291,7 +297,7 @@ internal sealed override bool OnTryWrite(
}

// Write extension data after the normal properties.
JsonPropertyInfo? dataExtensionProperty = state.Current.JsonTypeInfo.DataExtensionProperty;
JsonPropertyInfo? dataExtensionProperty = jsonTypeInfo.DataExtensionProperty;
if (dataExtensionProperty?.ShouldSerialize == true)
{
// Remember the current property for JsonPath support if an exception is thrown.
Expand Down Expand Up @@ -327,7 +333,7 @@ internal sealed override bool OnTryWrite(
state.Current.ProcessedStartToken = true;
}

List<KeyValuePair<string, JsonPropertyInfo?>>? propertyList = state.Current.JsonTypeInfo.PropertyCache!.List!;
List<KeyValuePair<string, JsonPropertyInfo?>>? propertyList = jsonTypeInfo.PropertyCache!.List!;
while (state.Current.EnumeratorIndex < propertyList.Count)
{
JsonPropertyInfo? jsonPropertyInfo = propertyList![state.Current.EnumeratorIndex].Value;
Expand Down Expand Up @@ -362,7 +368,7 @@ internal sealed override bool OnTryWrite(
// Write extension data after the normal properties.
if (state.Current.EnumeratorIndex == propertyList.Count)
{
JsonPropertyInfo? dataExtensionProperty = state.Current.JsonTypeInfo.DataExtensionProperty;
JsonPropertyInfo? dataExtensionProperty = jsonTypeInfo.DataExtensionProperty;
if (dataExtensionProperty?.ShouldSerialize == true)
{
// Remember the current property for JsonPath support if an exception is thrown.
Expand Down Expand Up @@ -407,7 +413,7 @@ internal sealed override bool OnTryWrite(

// AggressiveInlining since this method is only called from two locations and is on a hot path.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void ReadPropertyValue(
protected static void ReadPropertyValue(
object obj,
ref ReadStack state,
ref Utf8JsonReader reader,
Expand Down Expand Up @@ -438,7 +444,7 @@ protected void ReadPropertyValue(
state.Current.EndProperty();
}

protected bool ReadAheadPropertyValue(ref ReadStack state, ref Utf8JsonReader reader, JsonPropertyInfo jsonPropertyInfo)
protected static bool ReadAheadPropertyValue(ref ReadStack state, ref Utf8JsonReader reader, JsonPropertyInfo jsonPropertyInfo)
{
// Returning false below will cause the read-ahead functionality to finish the read.
state.Current.PropertyState = StackFramePropertyState.ReadValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,5 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, T? v
_converter.WriteNumberWithCustomHandling(writer, value.Value, handling);
}
}

internal override bool IsNull(in T? value) => !value.HasValue;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization.Metadata;

namespace System.Text.Json.Serialization
Expand Down Expand Up @@ -143,6 +144,9 @@ internal virtual bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, J
/// <remarks>Note that the value of <seealso cref="HandleNull"/> determines if the converter handles null JSON tokens.</remarks>
public abstract T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options);

#if NET6_0_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
#endif
internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out T? value)
{
if (ConverterStrategy == ConverterStrategy.Value)
Expand Down Expand Up @@ -310,10 +314,13 @@ internal override sealed bool TryReadAsObject(ref Utf8JsonReader reader, JsonSer
}

/// <summary>
/// Overridden by the nullable converter to prevent boxing of values by the JIT.
/// Performance optimization. The 'in' modifier in TryWrite(in T Value) will cause boxing, so this helper method avoids that.
steveharter marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
internal virtual bool IsNull(in T value) => value == null;
internal bool IsNull(T value) => value is null;
steveharter marked this conversation as resolved.
Show resolved Hide resolved

#if NET6_0_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
Copy link
Member

@eiriktsarpalis eiriktsarpalis Aug 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was not able to squeeze out any performance gains when applying aggressive optimizations to this method in the past.

#endif
internal bool TryWrite(Utf8JsonWriter writer, in T value, JsonSerializerOptions options, ref WriteStack state)
{
if (writer.CurrentDepth >= options.EffectiveMaxDepth)
Expand All @@ -333,10 +340,14 @@ internal bool TryWrite(Utf8JsonWriter writer, in T value, JsonSerializerOptions

if (
#if NET5_0_OR_GREATER
!typeof(T).IsValueType && // treated as a constant by recent versions of the JIT.
// Short-circuit the check against "is not null"; treated as a constant by recent versions of the JIT.
!typeof(T).IsValueType &&
#else
!IsValueType &&
#endif
// Since we may have checked for a null value above we may have a redundant check here,
// but this seems to be better than trying to cache that value when considering all permutations:
// int?, int?(null value), int, object, object(null value)
value is not null)
{

Expand Down Expand Up @@ -433,11 +444,12 @@ internal bool TryWrite(Utf8JsonWriter writer, in T value, JsonSerializerOptions

if (
#if NET5_0_OR_GREATER
!typeof(T).IsValueType && // treated as a constant by recent versions of the JIT.
// Short-circuit the check against ignoreCyclesPopReference; treated as a constant by recent versions of the JIT.
!typeof(T).IsValueType &&
#endif
ignoreCyclesPopReference)
{
// should only be entered if we're serializing instances
// Should only be entered if we're serializing instances
// of type object using the internal object converter.
Debug.Assert(value?.GetType() == typeof(object) && IsInternalConverter);
state.ReferenceResolver.PopReferenceForCycleDetection();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ public static partial class JsonSerializer
internal const string SerializationUnreferencedCodeMessage = "JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.";

[RequiresUnreferencedCode(SerializationUnreferencedCodeMessage)]
private static JsonTypeInfo GetTypeInfo(Type runtimeType, JsonSerializerOptions? options)
private static JsonTypeInfo GetTypeInfo(JsonSerializerOptions? options, Type runtimeType)
{
options ??= JsonSerializerOptions.s_defaultOptions;
options.RootBuiltInConvertersAndTypeInfoCreator();
if (!options.IsInitializedForReflectionSerializer)
{
options.InitializeForReflectionSerializer();
}

return options.GetOrAddClassForRootType(runtimeType);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ public static partial class JsonSerializer
throw new ArgumentNullException(nameof(document));
}

return ReadUsingOptions<TValue>(document, typeof(TValue), options);
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, typeof(TValue));
return ReadDocument<TValue>(document, jsonTypeInfo);
}

/// <summary>
Expand Down Expand Up @@ -66,7 +67,8 @@ public static partial class JsonSerializer
throw new ArgumentNullException(nameof(returnType));
}

return ReadUsingOptions<object?>(document, returnType, options);
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, returnType);
return ReadDocument<object?>(document, jsonTypeInfo);
}

/// <summary>
Expand Down Expand Up @@ -101,7 +103,7 @@ public static partial class JsonSerializer
throw new ArgumentNullException(nameof(jsonTypeInfo));
}

return ReadUsingMetadata<TValue>(document, jsonTypeInfo);
return ReadDocument<TValue>(document, jsonTypeInfo);
}

/// <summary>
Expand Down Expand Up @@ -157,17 +159,14 @@ public static partial class JsonSerializer
throw new ArgumentNullException(nameof(context));
}

return ReadUsingMetadata<object?>(document, GetTypeInfo(context, returnType));
JsonTypeInfo jsonTypeInfo = GetTypeInfo(context, returnType);
return ReadDocument<object?>(document, jsonTypeInfo);
}

[RequiresUnreferencedCode(SerializationUnreferencedCodeMessage)]
private static TValue? ReadUsingOptions<TValue>(JsonDocument document, Type returnType, JsonSerializerOptions? options) =>
ReadUsingMetadata<TValue>(document, GetTypeInfo(returnType, options));

private static TValue? ReadUsingMetadata<TValue>(JsonDocument document, JsonTypeInfo jsonTypeInfo)
private static TValue? ReadDocument<TValue>(JsonDocument document, JsonTypeInfo jsonTypeInfo)
{
ReadOnlySpan<byte> utf8Json = document.GetRootRawValue().Span;
return ReadUsingMetadata<TValue>(utf8Json, jsonTypeInfo);
return ReadSpan<TValue>(utf8Json, jsonTypeInfo);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ public static partial class JsonSerializer
/// for <typeparamref name="TValue"/> or its serializable members.
/// </exception>
[RequiresUnreferencedCode(SerializationUnreferencedCodeMessage)]
public static TValue? Deserialize<TValue>(this JsonElement element, JsonSerializerOptions? options = null) =>
ReadUsingOptions<TValue>(element, typeof(TValue), options);
public static TValue? Deserialize<TValue>(this JsonElement element, JsonSerializerOptions? options = null)
{
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, typeof(TValue));
return ReadUsingMetadata<TValue>(element, jsonTypeInfo);
}

/// <summary>
/// Converts the <see cref="JsonElement"/> representing a single JSON value into a <paramref name="returnType"/>.
Expand All @@ -51,7 +54,8 @@ public static partial class JsonSerializer
throw new ArgumentNullException(nameof(returnType));
}

return ReadUsingOptions<object?>(element, returnType, options);
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, returnType);
return ReadUsingMetadata<object?>(element, jsonTypeInfo);
}

/// <summary>
Expand Down Expand Up @@ -124,17 +128,14 @@ public static partial class JsonSerializer
throw new ArgumentNullException(nameof(context));
}

return ReadUsingMetadata<object?>(element, GetTypeInfo(context, returnType));
JsonTypeInfo jsonTypeInfo = GetTypeInfo(context, returnType);
return ReadUsingMetadata<object?>(element, jsonTypeInfo);
}

[RequiresUnreferencedCode(SerializationUnreferencedCodeMessage)]
private static TValue? ReadUsingOptions<TValue>(JsonElement element, Type returnType, JsonSerializerOptions? options) =>
ReadUsingMetadata<TValue>(element, GetTypeInfo(returnType, options));

private static TValue? ReadUsingMetadata<TValue>(JsonElement element, JsonTypeInfo jsonTypeInfo)
{
ReadOnlySpan<byte> utf8Json = element.GetRawValue().Span;
return ReadUsingMetadata<TValue>(utf8Json, jsonTypeInfo);
return ReadSpan<TValue>(utf8Json, jsonTypeInfo);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ public static partial class JsonSerializer
// The non-generic API was called or we have a polymorphic case where TValue is not equal to the T in JsonConverter<T>.
object? value = jsonConverter.ReadCoreAsObject(ref reader, options, ref state);
Debug.Assert(value == null || value is TValue);
return (TValue)value!;
return (TValue?)value;
}

private static TValue? ReadUsingMetadata<TValue>(ReadOnlySpan<byte> utf8Json, JsonTypeInfo jsonTypeInfo, int? actualByteCount = null)
private static TValue? ReadSpan<TValue>(ReadOnlySpan<byte> utf8Json, JsonTypeInfo jsonTypeInfo, int? actualByteCount = null)
{
JsonSerializerOptions options = jsonTypeInfo.Options;

Expand All @@ -33,7 +33,22 @@ public static partial class JsonSerializer
ReadStack state = default;
state.Initialize(jsonTypeInfo);

TValue? value = ReadCore<TValue>(jsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase, ref reader, options, ref state);
TValue? value;
JsonConverter jsonConverter = jsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase;

// For performance, the code below is a lifted ReadCore() above.
if (jsonConverter is JsonConverter<TValue> converter)
{
// Call the strongly-typed ReadCore that will not box structs.
value = converter.ReadCore(ref reader, options, ref state);
}
else
{
// The non-generic API was called or we have a polymorphic case where TValue is not equal to the T in JsonConverter<T>.
object? objValue = jsonConverter.ReadCoreAsObject(ref reader, options, ref state);
Debug.Assert(objValue == null || objValue is TValue);
value = (TValue?)objValue;
}

// The reader should have thrown if we have remaining bytes.
Debug.Assert(reader.BytesConsumed == (actualByteCount ?? utf8Json.Length));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ public static partial class JsonSerializer
[RequiresUnreferencedCode(SerializationUnreferencedCodeMessage)]
public static TValue? Deserialize<TValue>(this JsonNode? node, JsonSerializerOptions? options = null)
{
return ReadUsingOptions<TValue>(node, typeof(TValue), options);
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, typeof(TValue));
return ReadNode<TValue>(node, jsonTypeInfo);
}

/// <summary>
Expand All @@ -52,7 +53,8 @@ public static partial class JsonSerializer
throw new ArgumentNullException(nameof(returnType));
}

return ReadUsingOptions<object?>(node, returnType, options);
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, returnType);
return ReadNode<object?>(node, jsonTypeInfo);
}

/// <summary>
Expand All @@ -78,7 +80,7 @@ public static partial class JsonSerializer
throw new ArgumentNullException(nameof(jsonTypeInfo));
}

return ReadUsingMetadata<TValue>(node, jsonTypeInfo);
return ReadNode<TValue>(node, jsonTypeInfo);
}

/// <summary>
Expand Down Expand Up @@ -125,17 +127,13 @@ public static partial class JsonSerializer
throw new ArgumentNullException(nameof(context));
}

return ReadUsingMetadata<object?>(node, GetTypeInfo(context, returnType));
JsonTypeInfo jsonTypeInfo = GetTypeInfo(context, returnType);
return ReadNode<object?>(node, jsonTypeInfo);
}

[RequiresUnreferencedCode(SerializationUnreferencedCodeMessage)]
private static TValue? ReadUsingOptions<TValue>(JsonNode? node, Type returnType, JsonSerializerOptions? options) =>
ReadUsingMetadata<TValue>(node, GetTypeInfo(returnType, options));

private static TValue? ReadUsingMetadata<TValue>(JsonNode? node, JsonTypeInfo jsonTypeInfo)
private static TValue? ReadNode<TValue>(JsonNode? node, JsonTypeInfo jsonTypeInfo)
{
JsonSerializerOptions options = jsonTypeInfo.Options;
Debug.Assert(options != null);

// For performance, share the same buffer across serialization and deserialization.
using var output = new PooledByteBufferWriter(options.DefaultBufferSize);
Expand All @@ -151,7 +149,7 @@ public static partial class JsonSerializer
}
}

return ReadUsingMetadata<TValue>(output.WrittenMemory.Span, jsonTypeInfo);
return ReadSpan<TValue>(output.WrittenMemory.Span, jsonTypeInfo);
}
}
}
Loading