Skip to content

Comments

Apply a variant of the visitor pattern for reflection-based generic type instantiation.#72373

Closed
eiriktsarpalis wants to merge 2 commits intodotnet:mainfrom
eiriktsarpalis:type-visitor
Closed

Apply a variant of the visitor pattern for reflection-based generic type instantiation.#72373
eiriktsarpalis wants to merge 2 commits intodotnet:mainfrom
eiriktsarpalis:type-visitor

Conversation

@eiriktsarpalis
Copy link
Member

The System.Text.Json default metadata resolver has been using reflection to instantiate some of its generic metadata types such as JsonTypeInfo<T> and JsonPropertyInfo<T>. This of course is necessary since the value of T cannot be statically determined in most cases.

This PR updates some of the existing reflection-based generic type instantiation logic so that type parameter reification concerns are delegated to a "type witness", and any domain-specific metadata instantiation is performed via a generic visitor that is accepted by the witness. This approach has a few benefits:

  1. Makes the metadata layer more amenable to refactoring: constructors are not invoked via reflection and any parameter mismatches are caught at compile time.
  2. Avoids TargetInvocationException being surfaced to the user.
  3. Records nice performance gains compared to System.Activator or direct MethodInfo invocation on the micro level.

cc @stephentoub @GrabYourPitchforks @jkotas for feedback.

@eiriktsarpalis eiriktsarpalis requested a review from krwq July 18, 2022 13:21
@eiriktsarpalis eiriktsarpalis self-assigned this Jul 18, 2022
@eiriktsarpalis eiriktsarpalis added this to the 7.0.0 milestone Jul 18, 2022
@ghost
Copy link

ghost commented Jul 18, 2022

Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis
See info in area-owners.md if you want to be subscribed.

Issue Details

The System.Text.Json default metadata resolver has been using reflection to instantiate some of its generic metadata types such as JsonTypeInfo<T> and JsonPropertyInfo<T>. This of course is necessary since the value of T cannot be statically determined in most cases.

This PR updates some of the existing reflection-based generic type instantiation logic so that type parameter reification concerns are delegated to a "type witness", and any domain-specific metadata instantiation is performed via a generic visitor that is accepted by the witness. This approach has a few benefits:

  1. Makes the metadata layer more amenable to refactoring: constructors are not invoked via reflection and any parameter mismatches are caught at compile time.
  2. Avoids TargetInvocationException being surfaced to the user.
  3. Records nice performance gains compared to System.Activator or direct MethodInfo invocation on the micro level.

cc @stephentoub @GrabYourPitchforks @jkotas for feedback.

Author: eiriktsarpalis
Assignees: eiriktsarpalis
Labels:

area-System.Text.Json

Milestone: -

{
return (JsonTypeInfo)methodInfo.MakeGenericMethod(type).Invoke(null, new[] { options })!;
}
catch (TargetInvocationException ex)
Copy link
Contributor

Choose a reason for hiding this comment

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

FYI there is BindingFlags.DoNotWrapExceptions that lets the raw exception throw.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's not available in netstandard2.0, hence this extra code. That being said, it is evident from this PR that we haven't been applying the flag consistently.

}

s_createJsonTypeInfo ??= typeof(JsonTypeInfo).GetMethod(nameof(CreateJsonTypeInfo), new Type[] { typeof(JsonSerializerOptions) })!;
return (JsonTypeInfo)s_createJsonTypeInfo.MakeGenericMethod(type)
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a note that this PR is changing the refactoring done in #67526.

In effect, replacing MethodInfo.MakeGenericMethod with Activor.CreateInstance()?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's using Type.MakeGenericType + Activator instead of MethodInfo.MakeGenericMethod, but everything is behind logic marked RUC so it shouldn't matter much.

@jkotas
Copy link
Member

jkotas commented Jul 18, 2022

Records nice performance gains

I expect that this change has performance losses in static footprint. The runtime has to load number of extra generic types for each T.

@jkotas
Copy link
Member

jkotas commented Jul 18, 2022

Records nice performance gains

I expect that this change has performance losses in static footprint. The runtime has to load number of extra generic types for each T.

Given that these operations are typically executed just once, the static footprint is going to dominate the performance impact. My guess is that this change is (startup) performance regression for typical json serialization uses.

@eiriktsarpalis
Copy link
Member Author

eiriktsarpalis commented Jul 18, 2022

Given that these operations are typically executed just once, the static footprint is going to dominate the performance impact. My guess is that this change is (startup) performance regression for typical json serialization uses.

How could I measure potential regressions in this space? We have a few cold-start serialization benchmarks in dotnet/performance but effectively they follow the same approach as the benchmark linked in this PR (creating metadata from scratch in the same process).

@jkotas
Copy link
Member

jkotas commented Jul 18, 2022

https://gist.github.com/jkotas/aaaab624fbd034265cbecd6b6d8508ae

Current win-x64 release main: ~700ms
This PR: ~2400ms (~3.4x regression)

@eiriktsarpalis
Copy link
Member Author

Nice and simple. I think I'll keep that for future checks. Thanks.

@ghost ghost locked as resolved and limited conversation to collaborators Aug 18, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants