Skip to content

Commit fd1508d

Browse files
authored
Reduce lambda compilation for constant evaluation (#375)
Motivation ---------- We are seeing significant, ongoing JIT compilations and allocations as a result of evaluating constants during the query generation process. Almost all queries include some reference to a local variable as part of the expression tree, such as a reference to the IQueryable<T> itself or a filter variable in a Where predicate. These local variables are captured as a closure by C# under the covers, and appear a MemberAccessExpression with a ConstantExpression as the target. Currently, such cases require the JIT compilation of a lambda for every single execution of the query. Modifications ------------- Create an EnhancedPartialEvaluatingExpressionTreeVisitor based upon the Relinq PartialEvaluatingExpressionTreeVisitor that evaluates more cases using reflection instead of compiling a lambda. This includes the mentioned member access case as well as some other common cases. Expressions which can't be evaluated in this manner fallback to the previous behavior of compiling a lambda and executing it. Results ------- Significantly less overhead for most queries when computing the constant portions of the query. Note that in this context constant means a the portion of the query that is known and evaluatable within C# without requiring data from the database. It does not necessarily mean that the values are constant for each execution of the query.
1 parent 3fa6e0d commit fd1508d

6 files changed

Lines changed: 302 additions & 2 deletions
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
// From https://github.com/dotnet/runtime/blob/5da4a9e919dcee35f831ab69b6e475baaf798875/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/CallerArgumentExpressionAttribute.cs
5+
6+
namespace System.Runtime.CompilerServices;
7+
8+
#if !NETCOREAPP3_0_OR_GREATER
9+
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
10+
internal sealed class CallerArgumentExpressionAttribute : Attribute
11+
{
12+
public CallerArgumentExpressionAttribute(string parameterName)
13+
{
14+
ParameterName = parameterName;
15+
}
16+
17+
public string ParameterName { get; }
18+
}
19+
#endif
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System.Linq.Expressions;
2+
using Couchbase.Linq.Utils;
3+
using Remotion.Linq.Parsing.ExpressionVisitors.TreeEvaluation;
4+
using Remotion.Linq.Parsing.Structure;
5+
6+
namespace Couchbase.Linq.QueryGeneration
7+
{
8+
internal sealed class EnhancedPartialEvaluatingExpressionTreeProcessor : IExpressionTreeProcessor
9+
{
10+
public IEvaluatableExpressionFilter Filter { get; }
11+
12+
public EnhancedPartialEvaluatingExpressionTreeProcessor(IEvaluatableExpressionFilter filter)
13+
{
14+
ThrowHelpers.ThrowIfNull(filter);
15+
16+
Filter = filter;
17+
}
18+
19+
public Expression? Process(Expression expressionTree)
20+
{
21+
ThrowHelpers.ThrowIfNull(expressionTree);
22+
23+
return EnhancedPartialEvaluatingExpressionVisitor.EvaluateIndependentSubtrees(expressionTree, Filter);
24+
}
25+
}
26+
}
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
using System;
2+
using System.Linq;
3+
using System.Linq.Expressions;
4+
using System.Reflection;
5+
using Couchbase.Linq.Utils;
6+
using Remotion.Linq.Clauses.Expressions;
7+
using Remotion.Linq.Parsing;
8+
using Remotion.Linq.Parsing.ExpressionVisitors.TreeEvaluation;
9+
10+
namespace Couchbase.Linq.QueryGeneration
11+
{
12+
/// <summary>
13+
/// This is an enhanced version of the PartialEvaluatingExpressionVisitor that is able to evaluate
14+
/// more complex expressions without first compiling them to delegates. This significantly reduces the CPU and
15+
/// allocation overhead caused by JIT compilation. The optimizations used are particularly targeted to cover
16+
/// the most common scenario, which is accessing a local variable within a lambda which has been captured by
17+
/// the compiler in a closure object.
18+
/// </summary>
19+
internal sealed class EnhancedPartialEvaluatingExpressionVisitor : RelinqExpressionVisitor
20+
{
21+
public static Expression? EvaluateIndependentSubtrees(Expression expressionTree, IEvaluatableExpressionFilter evaluatableExpressionFilter)
22+
{
23+
ThrowHelpers.ThrowIfNull(expressionTree);
24+
ThrowHelpers.ThrowIfNull(evaluatableExpressionFilter);
25+
26+
var partialEvaluationInfo = EvaluatableTreeFindingExpressionVisitor.Analyze(expressionTree, evaluatableExpressionFilter);
27+
28+
var visitor = new EnhancedPartialEvaluatingExpressionVisitor(partialEvaluationInfo, evaluatableExpressionFilter);
29+
return visitor.Visit(expressionTree);
30+
}
31+
32+
// _partialEvaluationInfo contains a list of the expressions that are safe to be evaluated.
33+
private readonly PartialEvaluationInfo _partialEvaluationInfo;
34+
private readonly IEvaluatableExpressionFilter _evaluatableExpressionFilter;
35+
36+
private EnhancedPartialEvaluatingExpressionVisitor(
37+
PartialEvaluationInfo partialEvaluationInfo,
38+
IEvaluatableExpressionFilter evaluatableExpressionFilter)
39+
{
40+
ThrowHelpers.ThrowIfNull(partialEvaluationInfo);
41+
ThrowHelpers.ThrowIfNull(evaluatableExpressionFilter);
42+
43+
_partialEvaluationInfo = partialEvaluationInfo;
44+
_evaluatableExpressionFilter = evaluatableExpressionFilter;
45+
}
46+
47+
public override Expression? Visit(Expression? expression)
48+
{
49+
// Only evaluate expressions which do not use any of the surrounding parameter expressions. Don't evaluate
50+
// lambda expressions (even if you could), we want to analyze those later on.
51+
if (expression is null)
52+
{
53+
return null;
54+
}
55+
56+
if (expression.NodeType == ExpressionType.Lambda || !_partialEvaluationInfo.IsEvaluatableExpression(expression))
57+
{
58+
return base.Visit(expression);
59+
}
60+
61+
Expression? evaluatedExpression;
62+
try
63+
{
64+
evaluatedExpression = EvaluateSubtree(expression);
65+
}
66+
catch (Exception ex)
67+
{
68+
// Evaluation caused an exception. Skip evaluation of this expression and proceed as if it weren't evaluable.
69+
var baseVisitedExpression = base.Visit(expression);
70+
// Then wrap the result to capture the exception for the back-end.
71+
return new PartialEvaluationExceptionExpression(ex, baseVisitedExpression);
72+
}
73+
74+
if (evaluatedExpression != expression && evaluatedExpression is not null)
75+
{
76+
return EvaluateIndependentSubtrees(evaluatedExpression, _evaluatableExpressionFilter);
77+
}
78+
79+
return evaluatedExpression;
80+
}
81+
82+
/// <summary>
83+
/// Evaluates an evaluatable <see cref="Expression"/> subtree, i.e. an independent expression tree that is compilable and executable
84+
/// without any data being passed in. The result of the evaluation is returned as a <see cref="ConstantExpression"/>; if the subtree
85+
/// is already a <see cref="ConstantExpression"/>, no evaluation is performed.
86+
/// </summary>
87+
/// <param name="subtree">The subtree to be evaluated.</param>
88+
/// <returns>A <see cref="ConstantExpression"/> holding the result of the evaluation.</returns>
89+
private Expression? EvaluateSubtree(Expression? subtree)
90+
{
91+
if (subtree is null)
92+
{
93+
return null;
94+
}
95+
96+
if (subtree is ConstantExpression constantExpression)
97+
{
98+
var valueAsIQueryable = constantExpression.Value as IQueryable;
99+
if (valueAsIQueryable != null && valueAsIQueryable.Expression != constantExpression)
100+
{
101+
return valueAsIQueryable.Expression;
102+
}
103+
104+
// It is important to return the original constant expression here or the Visit method
105+
// above will create an infinite recursion.
106+
return constantExpression;
107+
}
108+
109+
var value = EvaluateOrExecuteSubtreeValue(subtree);
110+
if (value is Expression expression)
111+
{
112+
return expression;
113+
}
114+
115+
return Expression.Constant(value, subtree.Type);
116+
}
117+
118+
119+
// May return an Expression if it can't be evaluated, otherwise the constant value.
120+
private object? EvaluateOrExecuteSubtreeValue(Expression subtree)
121+
{
122+
var (value, success) = EvaluateSubtreeValue(subtree);
123+
if (success)
124+
{
125+
return value;
126+
}
127+
128+
// Fallback to compiling a delegate
129+
Expression<Func<object>> lambdaWithoutParameters =
130+
Expression.Lambda<Func<object>>(Expression.Convert(subtree, typeof(object)));
131+
var compiledLambda = lambdaWithoutParameters.Compile();
132+
133+
return compiledLambda();
134+
}
135+
136+
// Optimizations to avoid compiling and executing delegates if possible by evaluating some
137+
// common scenarios directly.
138+
private (object? value, bool success) EvaluateSubtreeValue(Expression subtree)
139+
{
140+
if (subtree is null)
141+
{
142+
return (null, false);
143+
}
144+
145+
switch (subtree)
146+
{
147+
case ConstantExpression constantExpression:
148+
// We've reached a constant. In the most common scenario, this will be a closure object created
149+
// by the compiler to capture variables in a lambda expression. However, it could also be a parameter
150+
// to a method call or a static field or property.
151+
return (constantExpression.Value, true);
152+
153+
case UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unaryExpression
154+
when unaryExpression.Type.UnwrapNullableType() == unaryExpression.Operand.Type:
155+
// Drill into conversions to Nullable<T> from concrete T.
156+
return EvaluateSubtreeValue(unaryExpression.Operand);
157+
158+
case MemberExpression memberExpression:
159+
{
160+
// Evaluate member access expressions. This will often be accessing a local variable from a
161+
// closure object created by the compiler.
162+
163+
// Evaluate the object instance, if any. This will be null for static fields and properties.
164+
object? instanceValue = null;
165+
if (memberExpression.Expression is not null)
166+
{
167+
(instanceValue, var success) = EvaluateSubtreeValue(memberExpression.Expression);
168+
if (!success)
169+
{
170+
return (null, false);
171+
}
172+
}
173+
174+
try
175+
{
176+
switch (memberExpression.Member)
177+
{
178+
case FieldInfo fieldInfo:
179+
return (fieldInfo.GetValue(instanceValue), true);
180+
181+
case PropertyInfo propertyInfo:
182+
return (propertyInfo.GetValue(instanceValue), true);
183+
}
184+
}
185+
catch
186+
{
187+
// Fall back to the delegate compilation behavior
188+
}
189+
}
190+
break;
191+
192+
case MethodCallExpression methodCallExpression:
193+
{
194+
// Evaluate method calls such as extension methods. Note that only method calls that were
195+
// previously considered evaluatable by EvaluatableTreeFindingExpressionVisitor.Analyze will
196+
// be evaluated here, otherwise the parent expression would not be evaluatable and this code
197+
// would not be reached.
198+
199+
// Evaluate the object instance, if any. This will be null for static methods.
200+
object? instanceValue = null;
201+
if (methodCallExpression.Object is not null)
202+
{
203+
(instanceValue, var success) = EvaluateSubtreeValue(methodCallExpression.Object);
204+
if (!success)
205+
{
206+
return (null, false);
207+
}
208+
}
209+
210+
var argumentValues = methodCallExpression.Arguments.Count == 0
211+
? Array.Empty<object?>()
212+
: new object?[methodCallExpression.Arguments.Count];
213+
214+
for (var i = 0; i < methodCallExpression.Arguments.Count; i++)
215+
{
216+
var (argumentValue, success) = EvaluateSubtreeValue(methodCallExpression.Arguments[i]);
217+
if (!success)
218+
{
219+
return (null, false);
220+
}
221+
222+
argumentValues[i] = argumentValue;
223+
}
224+
225+
try
226+
{
227+
return (methodCallExpression.Method.Invoke(instanceValue, argumentValues), true);
228+
}
229+
catch
230+
{
231+
// Fall back to the delegate compilation behavior
232+
}
233+
}
234+
break;
235+
}
236+
237+
return (null, false);
238+
}
239+
}
240+
}

Src/Couchbase.Linq/QueryParserHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public static IQueryParser CreateQueryParser(ICluster cluster) =>
9898
{
9999
new TransformingExpressionTreeProcessor(_prePartialEvaluationTransformerRegistry),
100100
SerializationExpressionTreeProcessor.FromCluster(cluster),
101-
new PartialEvaluatingExpressionTreeProcessor(new ExcludeSerializationConversionEvaluatableExpressionFilter()),
101+
new EnhancedPartialEvaluatingExpressionTreeProcessor(new ExcludeSerializationConversionEvaluatableExpressionFilter()),
102102
new TransformingExpressionTreeProcessor(_transformerRegistry)
103103
})));
104104
}

Src/Couchbase.Linq/Utils/ReflectionUtils.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,7 @@ public static T UnwrapNullableConversion<T>(Expression expression, out bool wasU
4343
wasUnwrapped = false;
4444
return expression as T;
4545
}
46+
47+
public static Type? UnwrapNullableType(this Type type) => Nullable.GetUnderlyingType(type);
4648
}
4749
}
Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
11
using System;
22
using System.Diagnostics.CodeAnalysis;
3+
using System.Runtime.CompilerServices;
34

45
namespace Couchbase.Linq.Utils
56
{
67
internal static class ThrowHelpers
78
{
9+
public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
10+
{
11+
#if NET6_0_OR_GREATER
12+
ArgumentNullException.ThrowIfNull(argument, paramName);
13+
#else
14+
if (argument is null)
15+
{
16+
ThrowArgumentNullException(paramName);
17+
}
18+
#endif
19+
}
20+
821
[DoesNotReturn]
9-
public static void ThrowArgumentNullException(string paramName) =>
22+
public static void ThrowArgumentNullException(string? paramName) =>
1023
throw new ArgumentNullException(paramName);
1124
}
1225
}

0 commit comments

Comments
 (0)