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

NH-3952: removing usages of NHibernate.Linq.EnumerableHelper #557

Merged
merged 8 commits into from
Mar 1, 2017

Conversation

fredericDelaporte
Copy link
Member

@fredericDelaporte fredericDelaporte commented Feb 24, 2017

NHibernate.Linq.EnumerableHelper has following comment:

// TODO rename / remove - reflection helper above is better

Done with this pull request.
It includes some additional test cases for classes impacted by this change which were not already thoroughly tested by existing test cases.
An explicit test for choosing which reflection implementation to choose from the other patterns found in NHibernate has been added too. (Unsurprisingly, caching as a static variable the thread safe MethodInfo prove to be the fastest by a wide margin. As it does not look to me as an added code complexity, pattern used in this pull request on each class affected by the removal of EnumerableHelper.)

Tracked under Jira task NH-3952.

EnumerableHelper is not removed but just marked as Obsolete, since it is publicly exposed and may be directly referenced by NHibernate users.

@hazzik
Copy link
Member

hazzik commented Feb 24, 2017

There is slightly more performant version than "Cached method definition + make gen" exists.

        public static MethodInfo GetMethod2<T, TResult>(Func<T, TResult> func, T arg1)
        {
            return func.Method;
        }

The idea that you extract a method from a bare "Method group delegate".

@fredericDelaporte
Copy link
Member Author

fredericDelaporte commented Feb 24, 2017

Thanks for the suggestion,

The case "cached method definition + make gen" should not be used if we can simply use a "cached method". I have done so in this pull request commits.
But when generic type parameter actual type varies at runtime, we cannot use directly a "cached method", and I think we cannot use the GetMethod2<T, TResult> alone either. We would have with it to get the definition and make generic at runtime too.

Now, actually ReflectionHelper use another technique for getting a MethodInfo, like:

public static MethodInfo GetMethod<TSource>(Expression<Action<TSource>> method)
{
	return ((MethodCallExpression)method.Body).Method;
}

Should we change this in favor of your GetMethod2?

About the quite "blunt" performance test on the other current patterns I have found in Nhibernate.Linq implementation, here is a sample result from my laptop, including your suggested method (they can vary considerably, but the ranking remains the same):

Blunt perf timings:
Cached method: 00:00:00.0000031
Cached method definition + make gen: 00:00:00.0017098
Hazzik GetMethod: 00:00:00.0013696
Hazzik GetMethodDefinition + make gen: 00:00:00.0047166
ReflectionHelper.GetMethod: 00:00:00.0029977
ReflectionHelper.GetMethodDefinition + make gen: 00:00:00.0052582
EnumerableHelper.GetMethod(generic overload): 00:00:00.0120367

So your method looks actually better than the one in place in ReflectionHelper.

Currently, on code I have modified, I use in order of priority whenever possible:

  1. Cached method (initialized with ReflectionHelper.GetMethod; for method, generic or not, for which we know the argument types at compile time)
  2. Cached method definition + make gen (initialized with ReflectionHelper.GetMethodDefinition; for generic method for which we do not know all the argument types at compile time)
  3. Cached method + overload search for non-generic method for which we do not know all the argument types at compile time. (NH-3850 only, WIP with commits which may get rebased.)

And that is all.

Replacing GetMethod/GetMethodDefinition by your GetMethod2/GetMethodDefinition2 would impact more than 200 calls. It should no be hard to do, but maybe would be a "too big change" for perf optimization done in the wild (without actually measuring the overall gain on some complete use case like declaring & executing a query, first time & with query plan cache hit).

I may instead introduce those two methods on ReflectionHelper, add some code comment asking for favoring them, and use them in the files this pull request impacts.

What do you think of this please?

@fredericDelaporte
Copy link
Member Author

fredericDelaporte commented Feb 27, 2017

And well, my point on "performances" was more on choosing to which pattern already in use I should stick for cleaning up usages of EnumerableHelper as the existing comment was suggesting, than introducing yet a new one. Maybe should we do that with another task.
(By the way, the pattern in use with ReflectionHelper is slightly slower than yours, but has the advantage of not allowing at compile time to try using a code block as a 'function', while yours will fails at runtime with a code block. When runtime performances are not critical, I tend to favor robustness.)


// TODO rename / remove - reflection helper above is better
[Obsolete("ReflectionHelper is better")]
Copy link
Member

Choose a reason for hiding this comment

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

Please change to [Obsolete("Please use ReflectionHelper instead")]

@@ -16,7 +16,18 @@ namespace NHibernate.Linq.NestedSelects
{
static class NestedSelectRewriter
{
private static readonly MethodInfo CastMethod = EnumerableHelper.GetMethod("Cast", new[] { typeof (IEnumerable) }, new[] { typeof (object[]) });
private static readonly MethodInfo CastMethod = ReflectionHelper.GetMethod(
Copy link
Member

Choose a reason for hiding this comment

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

I would love these fields to be concentrated in EnumerableHelper class (as internal static readonly fields), or other class, if we are making EnumerableHelper obsolete.

Copy link
Member Author

Choose a reason for hiding this comment

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

I have to check if a sound naming convention would do. With overloads and methods having the same name but belonging to other types, putting them all on ReflectionHelper could end up a bit messy.
Maybe should we use some ReflectionHelper internal subclass for each commonly reflected type to hold their cached method definition. I will test that soon.

@fredericDelaporte
Copy link
Member Author

fredericDelaporte commented Feb 28, 2017

The Obsolete reason is rewritten.

I have added a ReflectionCache class along ReflectionHelper, holding a subclass EnumerableMethods with MethodInfo fields. All those additions are internal.
Currently there is only one subclass, but I expect more to come, by example if we do the same caching for Queryable methods used in ProcessFirst, ProcessSingle, ExpressionParameterVisitor.

To avoid ambiguity with overloads, I have put in place a naming convention. It is not really simple. Here is how I have documented it in code:

// When adding a method to this cache, please follow the naming convention of those subclasses and fields:
//  - Add your method to a subclass named according to the type holding the method, and suffixed with "Methods".
//  - Name the field according to the method name.
//  - If the method has overloads, suffix it with "With" followed by its parameter names. Do not list parameters
//    common to all overloads.
//  - If the method is a generic method definition, add "Definition" as final suffix.
//  - If the method is generic, suffix it with "On" followed by its generic parameter type names.
// Avoid caching here narrow cases, such as those using specific types and unlikely to be used by many classes.
// Cache them instead in classes using them.

I have considered the NestedSelectRewriter reflection cases depending on type NHibernate.Linq.NestedSelects.Tuple to be too narrow for this method cache. So they stay defined in this NestedSelectRewriter class.
Same for CreateQueryMethodDefinition in DefaultQueryProvider. And I have not considered moving cached MethodInfofound in classes outside of the NHibernate.Linq namespace, such as those in ProxyFactory.

The above naming will not be enough for disambiguation of overloads having same parameter names but different parameter types without being generic. We should probably use type names instead of parameter names for those cases. I still favor parameter names when possible because I find it more readable and generally more concise than type names. (Imagine type name of a resultSelector...)

var selectMethod = EnumerableHelper.GetMethod("Select",
new[] { typeof (IEnumerable<>), typeof (Func<,>) },
new[] { typeof (Tuple), elementType });
var selectMethod = SelectMethodDefinition.MakeGenericMethod(new[] { typeof(Tuple), elementType });
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this use ReflectionCache.EnumerableMethods.SelectMethodDefinition?

Copy link
Member Author

Choose a reason for hiding this comment

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

Indeed, missed it.


internal static readonly MethodInfo CastDefinition =
ReflectionHelper.GetMethodDefinition(() => Enumerable.Cast<object>(null));
internal static readonly MethodInfo CastOnObjectArray =
Copy link
Member

Choose a reason for hiding this comment

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

I think this needs to be moved back to NestedSelectRewriter

Copy link
Member Author

Choose a reason for hiding this comment

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

Was unsure about that one, agreed.

@@ -94,7 +93,46 @@ internal static System.Type GetPropertyOrFieldType(this MemberInfo memberInfo)
}
}

// TODO rename / remove - reflection helper above is better
internal static class ReflectionCache
Copy link
Member

Choose a reason for hiding this comment

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

Please move this to a separate class in NHibernate.Util namespace.

Copy link
Member Author

Choose a reason for hiding this comment

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

Way more cleaner.

@fredericDelaporte
Copy link
Member Author

Changes done. Should I rebase for merging commits/cleaning up history? (I prefer to wait for review rather than doing it at once, as this would not help reviewer to review only last changes.)

@hazzik
Copy link
Member

hazzik commented Mar 1, 2017

Should I rebase for merging commits/cleaning up history?

Yes, please squash all your changes down to one commit.

@hazzik hazzik merged commit 585d83e into nhibernate:master Mar 1, 2017
@hazzik
Copy link
Member

hazzik commented Mar 1, 2017

Don't worry, I squashed myself.

@hazzik hazzik modified the milestone: 5.0.0 Mar 1, 2017
@fredericDelaporte
Copy link
Member Author

Ok, thanks.

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

Successfully merging this pull request may close these issues.

2 participants