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 reflection performance via e.g., CreateDelegate() #90

Closed
JeremyCaney opened this issue Apr 14, 2021 · 7 comments
Closed

Improve reflection performance via e.g., CreateDelegate() #90

JeremyCaney opened this issue Apr 14, 2021 · 7 comments
Assignees
Labels
Area: Mapping Relates to one of the `ITopicMappingService` interfaces or implementations. Priority: 2 Severity 2: Major Status 5: Complete Task is considered complete, and ready for deployment. Type: Improvement Improves the functionality or interface of an existing feature.
Milestone

Comments

@JeremyCaney
Copy link
Member

JeremyCaney commented Apr 14, 2021

Use Delegate.CreateDelegate() (example, example, example) to create a delegate and cache it as part of e.g., MemberDispatcher, if not TypeMemberInfoCollection and MemberInfoCollection<>. This should dramatically improve performance.

Ideally, this should be applied to Topic classes and model classes. For Topic classes, this needs to account for Get{Attribute}() methods as well as [AttributeSetter] and [ReferenceSetter] properties. For model classes, this needs to account for properties and constructors. These may be able to be handled by a single class, though they may be better served by specialized classes.

@JeremyCaney JeremyCaney added Area: Mapping Relates to one of the `ITopicMappingService` interfaces or implementations. Severity 2: Major Priority: 2 Type: Improvement Improves the functionality or interface of an existing feature. Status 2: Scheduled Planned for an upcoming release. labels Apr 14, 2021
@JeremyCaney JeremyCaney added this to the OnTopic 5.2.0 milestone Apr 14, 2021
@JeremyCaney JeremyCaney self-assigned this Apr 14, 2021
@JeremyCaney
Copy link
Member Author

Can we create a delegate for e.g., GetSetMethod() if the class type and return type aren’t known at compile time? E.g., by using:

var setter = propertyInfo.GetSetMethod();
Delegate.CreateDelegate(typeof(Action<object, object>), setter);

Or will this fail due to type mismatches with GetSetMethod()?

@JeremyCaney
Copy link
Member Author

JeremyCaney commented Apr 15, 2021

This can be done dynamically, with some creativity. The following quick-and-dirty example is adapted from Optimize C# Reflection Up to 10 Times by Using Delegates to work with GetSetMethod():

public class MethodHelper {

  private static Action<object, object> CallInnerDelegate<TClass, TParam>(Action<TClass, TParam> action)
    => (instance, value) => action((TClass)instance, (TParam)value);

  private static readonly MethodInfo CallInnerDelegateMethod = typeof(MethodHelper).GetMethod(nameof(CallInnerDelegate), BindingFlags.NonPublic | BindingFlags.Static)!;

  public void SetValue(object target, PropertyInfo propertyInfo, object value) {

    var delegateType = typeof(Action<,>).MakeGenericType(propertyInfo.DeclaringType, propertyInfo.PropertyType);
    var delegateSetter = propertyInfo.GetSetMethod().CreateDelegate(delegateType);
    var setterWithTypes = CallInnerDelegateMethod.MakeGenericMethod(propertyInfo.DeclaringType!, propertyInfo.PropertyType);
    var setter = (Action<object, object>)setterWithTypes.Invoke(null, new[] { delegateSetter })!;

    setter.Invoke(target, value);

  }
}

The delegate will be different for a get method, obviously, which will use a Func<,> instead of an Action<,>.

@JeremyCaney
Copy link
Member Author

JeremyCaney commented Apr 16, 2021

Currently, (nearly?) all calls to TypeMemberInfoCollection and MemberInfoCollection<> are routed through MemberDispatcher, which acts as a type of façade for handling the underlying complexity. Given this, we can easily swap out TypeMemberInfoCollection and MemberInfoCollection<> with e.g. a TypeAccessorCache or TypeAccessor and MemberAccessor objects without actually updating, for instance, the TrackedRecordCollection<> or TopicMappingService. That's the easiest path from a performance perspective.

That said, a number of members of the TypeMemberInfoCollection aren't really necessary anymore, or could be simplified by deeper integration with TypeAccessor and MemberAccessor. For example, the TypeMemberInfoCollection differentiates between properties and methods, whereas the MemberAccessor simply interacts with them as members; thus we could potentially consolidate e.g. SetPropertyValue() and SetMethodValue() into a single SetValue() method. Similarly, the two pieces of validation that TypeMemberInfoCollection introduces—for checking if a property has a specific attribute, and determining if value type conversion is supported—could optionally be moved to a lower level, thus preventing the need for e.g. HasSettableProperty().

Despite this, as these are internal libraries not used directly by external callers, the interface interests are a secondary priority to performance, and the MemberDispatcher façade refactoring or removal can be done independent of deploying the newly proposed TypeAccessorCache. As such, we can defer that to a separate issue if it requires further consideration.

@JeremyCaney
Copy link
Member Author

JeremyCaney commented Nov 25, 2021

Once this is fully integrated, we will be able to get rid of:

  • TypeMemberInfoCollection
  • MemberInfoCollection
  • MemberInfoCollection<>

As well as any related unit tests.

Additionally, we should be able to effectively merge the relevant methods from MemberDispatcher into TypeAccessorCache—or, better yet, into a set of TypeAccessorCache extension methods located in the OnTopic.Mapping namespace. That’s useful since the mapping library is the only location where we need to validate and convert against the AttributeValueConverter, and it would thus keep the mapping-specific logic independent from the general reflection capabilities.

Note: Acknowledging that some mapping limits remain implicit in the MemberAccessor design, such as parameter constraints on methods.

As part of this, we can optionally collapse the following:

  • HasGettableProperty(), HasGettableMethod()
  • GetPropertyValue(), GetMethodValue()
  • HasSettableProperty(), HasSettableMethod()
  • SetPropertyValue(), SetMethodValue()

Into:

  • HasGetter()
  • HasSetter()
  • GetValue()
  • SetValue()

It may remain useful to test these independently, however.

@JeremyCaney
Copy link
Member Author

One potential issue: TypeAccessor doesn’t directly contain MemberInfo objects, but rather MemberAccessor objects, which are more expensive to construct due to the compiled member accessor. This is more efficient when using reflection to read or write values, but isn’t necessary for e.g. BindingModelValidator which just requires metadata.

One alternative is to maintain a separate MemberInfoCollection for light weight metadata retrieval. And, if it‘s important, we can also add a AsTypeAccessor() method for translating to a TypeAccessor. Otherwise, the MemberInfoCollection becomes a lightweight wrapper for reflection, with no integration with MemberInfoAccessor or TypeAccessor.

@JeremyCaney
Copy link
Member Author

MemberAccessor tests should focus on evaluating IsValid, properties, and data type exceptions. Standard SetValue() and GetValue() tests should go through TypeAccessorCacheExtentions, to also evaluate its validation and conversion capabilities. This can largely be based on the existing MemberDispatchTests, just as TypeAccessorCacheExtentions should be based on MemberDispatcher.

JeremyCaney added a commit that referenced this issue Dec 8, 2021
Major refactoring of the internal `OnTopic.Internal.Reflection` classes. Previously, these classes relied on the .NET Reflection libraries to dynamically create delegate signatures for properties and methods before getting or setting values. Now, the delegate signature is created by the new `MemberAccessor` class and cached as part of a `TypeAccessorCache`. This promises to improve performance.

As part of this, I significantly refactored the organization and approach to the code. `TypeMemberInfoCollection` has been replaced by `TypeAccessorCache`, `MemberInfoCollection<T>` has been replaced by `TypeAccessor`, and `MemberDispatcher` has been replaced by `MemberAccessor`. This takes a more intuitive top-down approach, where as previously `MemberDispatcher` maintained its own `TypeMemberInfoCollection` cache which complicated the relationship between objects.

I also improved the validation of accessors, thus helping prevent unnecessary conversions, or attempts to set members that can't be set (e.g., by setting `null` to a non-nullable property).

In theory, this is supposed to have a major performance benefit—and, indeed, according to my initial testing directly against the new and old methods, it did. In real-world testing, however, we're not seeing that much benefit. That said, this also isn't any slower, and while it adds some complexity when calculating the delegate signatures, it's also much better organized.

This resolves Issue #90.
@JeremyCaney
Copy link
Member Author

Not only was the original optimization made, but all of the proposed refactoring from the comments has also been introduced. This was merged into develop as part of a82f7e6. This will be released with OnTopic 5.2.0.

@JeremyCaney JeremyCaney added Status 5: Complete Task is considered complete, and ready for deployment. and removed Status 2: Scheduled Planned for an upcoming release. labels Dec 8, 2021
JeremyCaney added a commit that referenced this issue Jun 26, 2022
OnTopic 5.2.0 introduces support for response caching (#89), a new `ErrorController` for HTTP error codes (#91), and improvements to `ITopicMappingService` performance via optional `AttributeDictionary` constructors (#99), updates to the internal reflection libraries (#90), and caching of compatible properties (#102). In addition, it also includes a number of internal code improvements, such as adoption of new C# 10 capabilities (#96), including global using directives (#93).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area: Mapping Relates to one of the `ITopicMappingService` interfaces or implementations. Priority: 2 Severity 2: Major Status 5: Complete Task is considered complete, and ready for deployment. Type: Improvement Improves the functionality or interface of an existing feature.
Projects
None yet
Development

No branches or pull requests

1 participant