Skip to content

Commit 1a69d23

Browse files
authored
Fix to #35206 - Query/Perf: don't use Invoke in value comparer lambdas when constructing shaper with PopulateCollection (#35207)
In EF9 we changed the way we generate shapers in preparation for AOT scenarios. We no longer can embed arbitrary objects into the shaper, instead we need to provide a way to construct that object in code, or simulate the functionality it used to provide. One of the examples was use of ValueComparers in PopulateIncludeCollection. Now instead of passing list of ValueComparer objects to use (which we can't reliably generate in code), we pass the delegate which is used to compare two values: ``` (left, right) => left == null ? right == null : right != null && Invoke((v1, v2) => v1 == v2, (int)left, (int)right) ``` This incurs a performance hit on some scenarios with collections, but can be improved by simplifying the delegate we use. Instead of having nested lambdas and using Invoke, we can inline the body of the nested lambda directly into the outer lambda, like so: ``` (left, right) => left == null ? right == null : right != null && (int)left == (int)right ``` This one change yields significant improvement in the affected scenarios (reducing both time spent and allocations): ef 9 before the Invoke fix | Method | Async | Mean | Error | StdDev | Op/s | Gen0 | Gen1 | Allocated | |-------------------------- |------ |---------:|--------:|--------:|------:|-----------:|----------:|----------:| | PredicateMultipleIncludes | False | 322.6 ms | 0.97 ms | 0.86 ms | 3.099 | 13000.0000 | 6000.0000 | 79.48 MB | | PredicateMultipleIncludes | True | 344.9 ms | 6.79 ms | 6.67 ms | 2.899 | 14000.0000 | 7000.0000 | 87.72 MB | ef 9 after the invoke fix | Method | Async | Mean | Error | StdDev | Op/s | Gen0 | Gen1 | Allocated | |-------------------------- |------ |---------:|--------:|--------:|------:|-----------:|----------:|----------:| | PredicateMultipleIncludes | False | 242.8 ms | 2.39 ms | 2.12 ms | 4.119 | 8000.0000 | 5000.0000 | 51.69 MB | | PredicateMultipleIncludes | True | 263.4 ms | 2.21 ms | 2.06 ms | 3.797 | 10000.0000 | 9000.0000 | 59.93 MB |
1 parent 3695bca commit 1a69d23

File tree

1 file changed

+35
-12
lines changed

1 file changed

+35
-12
lines changed

src/EFCore/ChangeTracking/ValueComparer`.cs

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ public class ValueComparer
4444
private static readonly PropertyInfo StructuralComparisonsStructuralEqualityComparerProperty =
4545
typeof(StructuralComparisons).GetProperty(nameof(StructuralComparisons.StructuralEqualityComparer))!;
4646

47+
private static readonly bool UseOldBehavior35206 =
48+
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35206", out var enabled35206) && enabled35206;
49+
4750
/// <summary>
4851
/// Creates a new <see cref="ValueComparer{T}" /> with a default comparison
4952
/// expression and a shallow copy for the snapshot.
@@ -263,18 +266,38 @@ public override LambdaExpression ObjectEqualsExpression
263266
var left = Parameter(typeof(object), "left");
264267
var right = Parameter(typeof(object), "right");
265268

266-
_objectEqualsExpression = Lambda<Func<object?, object?, bool>>(
267-
Condition(
268-
Equal(left, Constant(null)),
269-
Equal(right, Constant(null)),
270-
AndAlso(
271-
NotEqual(right, Constant(null)),
272-
Invoke(
273-
EqualsExpression,
274-
Convert(left, typeof(T)),
275-
Convert(right, typeof(T))))),
276-
left,
277-
right);
269+
if (!UseOldBehavior35206)
270+
{
271+
var remap = ReplacingExpressionVisitor.Replace(
272+
[EqualsExpression.Parameters[0], EqualsExpression.Parameters[1]],
273+
[Convert(left, typeof(T)), Convert(right, typeof(T))],
274+
EqualsExpression.Body);
275+
276+
_objectEqualsExpression = Lambda<Func<object?, object?, bool>>(
277+
Condition(
278+
Equal(left, Constant(null)),
279+
Equal(right, Constant(null)),
280+
AndAlso(
281+
NotEqual(right, Constant(null)),
282+
remap)),
283+
left,
284+
right);
285+
}
286+
else
287+
{
288+
_objectEqualsExpression = Lambda<Func<object?, object?, bool>>(
289+
Condition(
290+
Equal(left, Constant(null)),
291+
Equal(right, Constant(null)),
292+
AndAlso(
293+
NotEqual(right, Constant(null)),
294+
Invoke(
295+
EqualsExpression,
296+
Convert(left, typeof(T)),
297+
Convert(right, typeof(T))))),
298+
left,
299+
right);
300+
}
278301
}
279302

280303
return _objectEqualsExpression;

0 commit comments

Comments
 (0)