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 deserialization perf for case-insensitive and missing-property cases #35848

Merged
merged 2 commits into from
May 11, 2020

Conversation

steveharter
Copy link
Member

@steveharter steveharter commented May 5, 2020

~1.75x faster deserialization of a simple POCO when:

  • JsonSerializerOptions.PropertyNameCaseInsensitive is true AND
  • Property names in JSON do not match property name (specified by either the CLR property name, the value specified by [JsonPropertyName], or the property naming policy) AND
  • There is at least one property name > 7 bytes in length

In addition, similar perf results for property-misses cases (property names in JSON do not match any property).

Also there are less cache memory allocations for property names. This also has a startup CPU improvement - a simple POCO resulted in a .5ms startup improvements on first deserialization (~23.5ms to ~23.0ms). Private bytes savings were also verified using Process Explorer (very long property names were used).

Fixes #30789 and the increased startup perf and reduced memory consumption helps with 5.0 serialization goals per dotnet/designs#113.

In addition, there are less stack-based allocations for case-insensitive and missing-property scenarios (shown below in the benchmarks).

This may help with ASP.NET deserialization scenarios since they enable case-insensitive by default (cc @pranavkm). However, since ASP.NET also uses a camel-casing property naming policy, the benefit will not occur if the incoming JSON exactly matches what was produced by the camel-casing policy, as would be the case if all serialization occurs with System.Text.Json.

No measurable perf regressions on existing deserialization benchmarks (within +- 3%). Todo: add a case-insensitive and missing-property benchmark.

The main CPU benefit comes from avoiding the dictionary-based lookup for case-insensitive and missing-property scenarios and instead use the array-backed cache. The previous code would also unnecessarily grow and max out that array (64 elements) in certain cases before using the dictionary.

During testing, it was found that "missing properties" in JSON that are added to extension data did not properly support JsonPath semantics (used when a JsonException is thrown to report the invalid property). That was fixed and a test added.

Below are benchmarks for a simple 4-property POCO test class that has property names > 7 bytes.

3.1 performance
|                            Method |       Mean |   Error |  StdDev |     Median |        Min |        Max |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|---------------------------------- |-----------:|--------:|--------:|-----------:|-----------:|-----------:|-------:|------:|------:|----------:|
| CaseSensitive_Matching            |   844.2 ns | 4.25 ns | 3.55 ns |   844.2 ns |   838.6 ns |   850.6 ns | 0.0342 |     - |     - |     224 B |
| CaseInsensitive_Matching          |   833.3 ns | 3.84 ns | 3.40 ns |   832.6 ns |   829.4 ns |   841.1 ns | 0.0504 |     - |     - |     328 B |
| CaseSensitive_NotMatching(Missing)| 1,007.7 ns | 9.40 ns | 8.79 ns | 1,005.1 ns |   997.3 ns | 1,023.3 ns | 0.0722 |     - |     - |     464 B |
| CaseInsensitive_NotMatching       | 1,405.6 ns | 8.35 ns | 7.40 ns | 1,405.1 ns | 1,397.1 ns | 1,423.6 ns | 0.0626 |     - |     - |     408 B |

5.0 performance (BEFORE)
|                            Method |       Mean |    Error |   StdDev |     Median |        Min |        Max |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|---------------------------------- |-----------:|---------:|---------:|-----------:|-----------:|-----------:|-------:|------:|------:|----------:|
| CaseSensitive_Matching            |   826.0 ns |  6.41 ns |  5.99 ns |   827.1 ns |   815.7 ns |   836.1 ns | 0.0981 |     - |     - |     632 B |
| CaseInsensitive_Matching          |   844.7 ns |  2.95 ns |  2.46 ns |   845.1 ns |   841.2 ns |   848.5 ns | 0.1158 |     - |     - |     736 B |
| CaseSensitive_NotMatching(Missing)|   899.7 ns |  3.77 ns |  3.14 ns |   899.4 ns |   895.5 ns |   906.6 ns | 0.0182 |     - |     - |     120 B |
| CaseInsensitive_NotMatching       | 1,419.6 ns | 24.86 ns | 20.76 ns | 1,424.9 ns | 1,374.7 ns | 1,446.9 ns | 0.1263 |     - |     - |     816 B |

5.0 performance (AFTER)
|                            Method |     Mean |   Error |  StdDev |   Median |      Min |      Max |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|---------------------------------- |---------:|--------:|--------:|---------:|---------:|---------:|-------:|------:|------:|----------:|
| CaseSensitive_Matching            | 799.2 ns | 4.59 ns | 4.29 ns | 801.0 ns | 790.5 ns | 803.9 ns | 0.0985 |     - |     - |     632 B |
| CaseInsensitive_Matching          | 789.2 ns | 6.62 ns | 5.53 ns | 790.3 ns | 776.0 ns | 794.4 ns | 0.1004 |     - |     - |     632 B |
| CaseSensitive_NotMatching(Missing)| 479.9 ns | 0.75 ns | 0.59 ns | 479.8 ns | 479.1 ns | 481.0 ns | 0.0059 |     - |     - |      40 B |
| CaseInsensitive_NotMatching       | 783.5 ns | 3.26 ns | 2.89 ns | 783.5 ns | 779.0 ns | 789.2 ns | 0.1004 |     - |     - |     632 B |

@steveharter steveharter added this to the 5.0 milestone May 5, 2020
@steveharter steveharter requested review from layomia and jozkee May 5, 2020 17:37
@steveharter steveharter self-assigned this May 5, 2020
@ghost
Copy link

ghost commented May 5, 2020

Tagging subscribers to this area: @jozkee
Notify danmosemsft if you want to be subscribed.

Copy link
Contributor

@pranavkm pranavkm left a comment

Choose a reason for hiding this comment

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

Nice!

_preserveHandlingOnSerialize = preserveHandlingOnSerialize;
_preserveHandlingOnDeserialize = preserveHandlingOnDeserialize;
_shouldReadPreservedReferences = preserveHandlingOnDeserialize == PreserveReferencesHandling.All;
_shouldWritePreservedReferences = preserveHandlingOnSerialize == PreserveReferencesHandling.All;
}

internal bool ShouldReadPreservedReferences()
Copy link
Contributor

Choose a reason for hiding this comment

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

Could these be internal properties instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes; didn't change. cc @jozkee

@@ -245,7 +251,7 @@ private void InitializeConstructorParameters(ConstructorInfo constructorInfo)

// One object property cannot map to multiple constructor
// parameters (ConvertName above can't return multiple strings).
parameterCache.Add(jsonParameterInfo.NameAsString, jsonParameterInfo);
parameterCache.Add(jsonPropertyInfo.NameAsString!, jsonParameterInfo);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: we should assert that jsonPropertyInfo.NameAsString and jsonParameterInfo.NameAsString are equal (I can do this in a follow up)

@steveharter
Copy link
Member Author

Test failures in runtime Libraries Windows_NT x64 Debug due to infrastructure:
Restore) Cannot create a file when that file already exists.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Normalizing casing for property names in JsonException.Path helps deserialization performance
4 participants