RM-9585 Rewrite LINQ queries using the span version of Contains - #1454
RM-9585 Rewrite LINQ queries using the span version of Contains#1454a-ctor wants to merge 2 commits into
Conversation
MichaelKetting
left a comment
There was a problem hiding this comment.
I got some simplification and a possible re-design of where the code should go. I hven't tried it. Please test.
| /// While the usage get rewritten, it is also necessary to disable re-linqs evaluation as | ||
| /// it tries to constant fold the expression otherwise, which causes runtime exceptions. | ||
| /// </remarks> | ||
| public class ByRefLikeAwareEvaluatableExpressionFilter : EvaluatableExpressionFilterBase |
There was a problem hiding this comment.
Please create a ticket in sqlbackend. I think we should move this logic there eventually.
| /// While the usage get rewritten, it is also necessary to disable re-linqs evaluation as | ||
| /// it tries to constant fold the expression otherwise, which causes runtime exceptions. | ||
| /// </remarks> | ||
| public class ByRefLikeAwareEvaluatableExpressionFilter : EvaluatableExpressionFilterBase |
There was a problem hiding this comment.
can we get a unit test for this please?
|
|
||
| var transformerRegistry = ExpressionTransformerRegistry.CreateDefault(); | ||
| var processor = ExpressionTreeParser.CreateDefaultProcessor(transformerRegistry); | ||
| transformerRegistry.Register(new SpanContainsExpressionTransformer()); |
There was a problem hiding this comment.
Please extend LinqProviderComponentFactoryTest
| /// Transforms <see cref="MemoryExtensions"/>.<see cref="MemoryExtensions.Contains"/> calls generated by the C# 14 compiler back to their | ||
| /// <see cref="Enumerable"/>.<see cref="M:System.Linq.Enumerable.Contains``1(System.Collections.Generic.IEnumerable{``0},``0)"/> equivalent. | ||
| /// </summary> | ||
| public class SpanContainsExpressionTransformer : IExpressionTransformer<MethodCallExpression> |
There was a problem hiding this comment.
this should also go into Remotion.Linq (RMLNQ) eventually. please create a ticket
| s_memoryExtensionContainsMethod = FindContainsMethod(typeof(MemoryExtensions), typeof(ReadOnlySpan<>), false); | ||
| s_memoryExtensionContainsMethodWithEqualityComparer = FindContainsMethod(typeof(MemoryExtensions), typeof(ReadOnlySpan<>), true); | ||
|
|
||
| s_enumerableContainsMethod = FindContainsMethod(typeof(Enumerable), typeof(IEnumerable<>), false); | ||
| s_enumerableContainsMethodWithEqualityComparer = FindContainsMethod(typeof(Enumerable), typeof(IEnumerable<>), true); |
There was a problem hiding this comment.
I prefer we use this setup. It uses the existing helpers, the code is shorter and it's easier to understand for me.
... = MemberInfoFromExpressionUtility.GetGenericMethodDefinition(() => Enumerable.Contains((IEnumerable<object>)null, (object)null));
... = MemberInfoFromExpressionUtility.GetGenericMethodDefinition(() => Enumerable.Contains((IEnumerable<object>)null, (object)null, (IEqualityComparer<object>)null));
var genericParameter = Type.MakeGenericMethodParameter(0);
// MemoryExtensions.Contains(ReadOnlySpan<int>.Empty, 0));
... = Assertion.IsNotNull(
typeof(MemoryExtensions).GetMethod(
nameof(MemoryExtensions.Contains),
new[] { typeof(ReadOnlySpan<>).MakeGenericType(genericParameter), genericParameter }),
"MemoryExtensions.Contains(ReadOnlySpan<T>, T)");
... = Assertion.IsNotNull(
typeof(MemoryExtensions).GetMethod(
nameof(MemoryExtensions.Contains),
new[] { typeof(ReadOnlySpan<>).MakeGenericType(genericParameter), genericParameter, typeof(IEqualityComparer<>).MakeGenericType(genericParameter) }),
"MemoryExtensions.Contains(ReadOnlySpan<T>, T, IEqualityComparer<T>)");
Interestingly enough, we could write the MemoryExtension.Contains for the IEqualityComparer using the MemberInfoFromExpressionUtility and skip most to all of the casts, but I like the fact that we're precise. Could live without the casts but this is the easiest way to ensure correctness.
There was a problem hiding this comment.
It's likely that we only need this part of the logic and the ResultOperator-registration in the other file and no Transformer. Please check
|
|
||
| // Pre-filter for the relevant MemoryExtensions.Contains overloads | ||
| if (expression.Method.DeclaringType != typeof(MemoryExtensions) | ||
| || expression.Method.Name != "Contains" |
There was a problem hiding this comment.
| || expression.Method.Name != "Contains" | |
| || expression.Method.Name != nameof(MemoryExtensions.Contains) |
| // Pre-filter for the relevant MemoryExtensions.Contains overloads | ||
| if (expression.Method.DeclaringType != typeof(MemoryExtensions) | ||
| || expression.Method.Name != "Contains" | ||
| || !expression.Method.IsGenericMethod | ||
| || !IsEnumerableContainsMethod(expression.Method.GetGenericMethodDefinition())) | ||
| { | ||
| return expression; | ||
| } | ||
|
|
||
| var itemType = expression.Method.GetGenericArguments()[0]; | ||
| var readOnlySpanType = typeof(ReadOnlySpan<>).MakeGenericType(itemType); | ||
|
|
||
| // We need to check for and remove the implicit span cast that gets generated. | ||
| // User code might also reach this point so we don't YoloCast(tm) | ||
| var collectionArgument = expression.Arguments[0]; | ||
| if (collectionArgument is MethodCallExpression { Method.Name: "op_Implicit" } implicitConversion | ||
| && implicitConversion.Method.DeclaringType == readOnlySpanType) | ||
| { | ||
| // Contains has two relevant overloads, one with and one without equality comparer. | ||
| // While re-linq does not support the equality comparer overload, the C# compiler | ||
| // rewrites the overload to the equality comparer one and passes a null value. | ||
| // This break any Contains usage. | ||
| // As such, we rewrite null equality comparer invocations to use the overload | ||
| // without an equality comparer. | ||
| if (expression.Arguments is [_, _, not ConstantExpression { Value: null }]) | ||
| { | ||
| return Expression.Call( | ||
| s_enumerableContainsMethodWithEqualityComparer.MakeGenericMethod(itemType), | ||
| implicitConversion.Arguments[0], | ||
| expression.Arguments[1], | ||
| expression.Arguments[2]); | ||
| } | ||
|
|
||
| return Expression.Call( | ||
| s_enumerableContainsMethod.MakeGenericMethod(itemType), | ||
| implicitConversion.Arguments[0], | ||
| expression.Arguments[1]); |
There was a problem hiding this comment.
I'm not sure about the performance implications of this code. from the StopWatch, I think you took measurements.
| private static bool IsEnumerableContainsMethod (MethodInfo methodInfo) | ||
| { | ||
| return methodInfo == s_memoryExtensionContainsMethod | ||
| || methodInfo == s_memoryExtensionContainsMethodWithEqualityComparer; | ||
| } | ||
|
|
There was a problem hiding this comment.
I believe the name is incorrect. It should be IsMemoryExtenstionsContainsMethod
| protected virtual MethodInfoBasedNodeTypeRegistry CreateCustomNodeTypeProvider () | ||
| { | ||
| var customNodeTypeRegistry = new MethodInfoBasedNodeTypeRegistry(); | ||
|
|
||
| customNodeTypeRegistry.Register( | ||
| new[] { MemberInfoFromExpressionUtility.GetMethod((DomainObjectCollection obj) => obj.ContainsObject(null!)) }, | ||
| typeof(ContainsExpressionNode)); |
There was a problem hiding this comment.
Please check, I think we can plop the logic in here and simply register the ContainsExpressioNode for the two overloads of MemoryExtensions.Contains. I haven't read through the entire Transform-logic. If my suggestion works, I don't have to.
In particular, I haven't parsed why we also check for the two Contains-overloads on Enumerable. If these two also need wireing up for ContainsExpressionNode because re-linq isn't doing it on it's own, that's okay.
There was a problem hiding this comment.
Okay, running the tests, I understand we got op_Implicit in play, too and this is causing issues. Wonderful.
f0126e4 to
5b1974f
Compare
No description provided.