Skip to content

Commit

Permalink
Optimize cachedExpressionCompiler by changing cache key to remove Exp…
Browse files Browse the repository at this point in the history
…ression.Lambda() calls
  • Loading branch information
myk0la999 committed Nov 22, 2024
1 parent b9afb45 commit 1a30e70
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 16 deletions.
46 changes: 35 additions & 11 deletions ExpressionUtils/Evaluating/CachedExpressionCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ namespace MiaPlaza.ExpressionUtils.Evaluating {
/// the result via a closure and a delegate capturing the actual parameters of the original expression is returned.
/// </remarks>
public class CachedExpressionCompiler : IExpressionEvaluator {
static ConcurrentDictionary<LambdaExpression, ParameterListDelegate> delegates = new ConcurrentDictionary<LambdaExpression, ParameterListDelegate>(new ExpressionComparing.StructuralComparer(ignoreConstantsValues: true));
static ConcurrentDictionary<LambdaParts, ParameterListDelegate> delegates = new ConcurrentDictionary<LambdaParts, ParameterListDelegate>(new LambdaExpressionComparer());

public static readonly CachedExpressionCompiler Instance = new CachedExpressionCompiler();

private CachedExpressionCompiler() { }

VariadicArrayParametersDelegate IExpressionEvaluator.EvaluateLambda(LambdaExpression lambdaExpression) => CachedCompileLambda(lambdaExpression);
public VariadicArrayParametersDelegate CachedCompileLambda(LambdaExpression lambda) {
public VariadicArrayParametersDelegate EvaluateLambda(LambdaExpression lambdaExpression) => CachedCompileLambda(lambdaExpression);

public VariadicArrayParametersDelegate CachedCompileLambda(LambdaParts lambda) {
IReadOnlyList<object> constants;

ParameterListDelegate compiled;
Expand All @@ -51,23 +53,23 @@ public VariadicArrayParametersDelegate CachedCompileLambda(LambdaExpression lamb
return args => compiled(constants.Concat(args).ToArray());
}

object IExpressionEvaluator.Evaluate(Expression unparametrizedExpression) => CachedCompile(unparametrizedExpression);
public object CachedCompile(Expression unparametrizedExpression) => CachedCompileLambda(Expression.Lambda(unparametrizedExpression))();
public object Evaluate(Expression unparametrizedExpression) => CachedCompile(unparametrizedExpression);
public object CachedCompile(Expression unparametrizedExpression) => CachedCompileLambda(new LambdaParts { Body = unparametrizedExpression, Parameters = Array.Empty<ParameterExpression>() })();

DELEGATE IExpressionEvaluator.EvaluateTypedLambda<DELEGATE>(Expression<DELEGATE> expression) => CachedCompileTypedLambda(expression);
public DELEGATE EvaluateTypedLambda<DELEGATE>(Expression<DELEGATE> expression) where DELEGATE : class => CachedCompileTypedLambda(expression);
public DELEGATE CachedCompileTypedLambda<DELEGATE>(Expression<DELEGATE> expression) where DELEGATE : class => CachedCompileLambda(expression).WrapDelegate<DELEGATE>();


/// <summary>
/// A closure free expression tree that can be used as a caching key. Can be used with the <see cref="ExpressionComparing.StructuralComparer" /> to compare
/// to the original lambda expression.
/// Creates a closure-free key for caching, represented as a tuple of the expression body and parameter collection.
/// Can be used with the <see cref="LambdaExpressionComparer" /> to compare to the original lambda expression.
/// </summary>
private LambdaExpression getClosureFreeKeyForCaching(ConstantExtractor.ExtractionResult extractionResult, IReadOnlyCollection<ParameterExpression> parameterExpressions) {
private LambdaParts getClosureFreeKeyForCaching(ConstantExtractor.ExtractionResult extractionResult, IReadOnlyCollection<ParameterExpression> parameterExpressions) {
var e = SimpleParameterSubstituter.SubstituteParameter(extractionResult.ConstantfreeExpression,
extractionResult.ConstantfreeExpression.Parameters.Select(
p => (Expression) Expression.Constant(getDefaultValue(p.Type), p.Type)));
return Expression.Lambda(e, parameterExpressions);

return new LambdaParts { Body = e, Parameters = parameterExpressions };
}

private static object getDefaultValue(Type t) {
Expand All @@ -84,5 +86,27 @@ private static object getDefaultValue(Type t) {
internal bool IsCached(LambdaExpression lambda) {
return delegates.ContainsKey(lambda);
}

/// <summary>
/// Previously as a key for <see cref="delegates"/> we were using <see cref="LambdaExpression"/> objects and <see cref="ExpressionComparing.StructuralComparer"/> as a comparer.
/// But that required us to make calls to <see cref="Expression.Lambda(Expression, ParameterExpression[])"/> which contains global lock inside, and that started to become a problem.
/// So instead we now use <see cref="LambdaParts"/> as a key and pass body and parameters separately, to reduce number of calls to <see cref="Expression.Lambda(Expression, ParameterExpression[])"/>.
/// </summary>
private class LambdaExpressionComparer : IEqualityComparer<LambdaParts> {
private IEqualityComparer<Expression> expressionComparer = new ExpressionComparing.StructuralComparer(ignoreConstantsValues: true);

public bool Equals(LambdaParts x, LambdaParts y) {
return x.Body.Type == y.Body.Type
&& x.Parameters.SequenceEqualOrBothNull(y.Parameters, expressionComparer.Equals)
&& expressionComparer.Equals(x.Body, y.Body);
}

public int GetHashCode(LambdaParts obj) {
var hash = Hashing.FnvOffset;
Hashing.Hash(ref hash, obj.Body.Type.GetHashCode());
Hashing.Hash(ref hash, expressionComparer.GetHashCode(obj.Body));
return hash;
}
}
}
}
2 changes: 2 additions & 0 deletions ExpressionUtils/ExpressionUtils.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/Miaplaza/expression-utils</PackageProjectUrl>
<PackageTags>Expression;Expressions;Linq.Expressions;Evaluation;Compile;Compiler;Execute;Execution;Compare;Comparison</PackageTags>
<DebugType>embedded</DebugType>
<EmbedAllSources>true</EmbedAllSources>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CSharp" Version="4.6.0" />
Expand Down
22 changes: 22 additions & 0 deletions ExpressionUtils/LambdaParts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace MiaPlaza.ExpressionUtils {

/// <summary>
/// Sometimes we don't have actual <see cref="LambdaExpression"/> object, but instead <see cref="LambdaExpression.Body"/> and <see cref="LambdaExpression.Parameters"/> that represent lambda.
/// We could call <see cref="Expression.Lambda(Expression, ParameterExpression[])"/> to get it, but such calls can be expensive due to locking that is used inside.
/// So this object plays a role of a container that holds the data that is needed to create lambda expression.
/// </summary>
/// <remarks>
/// Before <see cref="LambdaExpression"/> objects were used to fullfill that role. This was added to replace those.
/// </remarks>
public class LambdaParts {
public Expression Body { get; set; }
public IReadOnlyCollection<ParameterExpression> Parameters { get; set; }

public static implicit operator LambdaParts(LambdaExpression lambda)
=> lambda is null ? null : new LambdaParts { Body = lambda.Body, Parameters = lambda.Parameters };
}
}
7 changes: 2 additions & 5 deletions ExpressionUtils/ParameterSubstituter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,8 @@ public class ParameterSubstituter : SimpleParameterSubstituter {
public static new Expression SubstituteParameter(LambdaExpression expression, params Expression[] replacements)
=> SubstituteParameter(expression, replacements as IReadOnlyCollection<Expression>);

public static new Expression SubstituteParameter(LambdaExpression expression, IEnumerable<Expression> replacements)
=> SubstituteParameter(expression, replacements.ToList());

public static new Expression SubstituteParameter(LambdaExpression expression, IReadOnlyCollection<Expression> replacements) {
if (expression == null) {
public static Expression SubstituteParameter(LambdaParts expression, IReadOnlyCollection<Expression> replacements) {
if (expression == null || expression.Body == null || expression.Parameters == null) {
throw new ArgumentNullException(nameof(expression));
}

Expand Down

0 comments on commit 1a30e70

Please sign in to comment.