diff --git a/.gitignore b/.gitignore
index 9997611..ec89885 100644
--- a/.gitignore
+++ b/.gitignore
@@ -109,6 +109,7 @@ stylecop.*
~$*
*.dbmdl
Generated_Code #added for RIA/Silverlight projects
+BenchmarkDotNet.Artifacts/
# Backup & report files from converting an old project file to a newer
# Visual Studio version. Backup files are not needed, because we have git ;-)
diff --git a/DynamicExpresso.sln b/DynamicExpresso.sln
index 52825f2..1a4e450 100644
--- a/DynamicExpresso.sln
+++ b/DynamicExpresso.sln
@@ -12,24 +12,63 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{4088369E-A
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DynamicExpresso.Core", "src\DynamicExpresso.Core\DynamicExpresso.Core.csproj", "{C6B7C0D2-B84A-4307-9C61-D95613DB564D}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmark", "benchmark", "{09EED85C-BE3C-7566-DC0E-2E8E43466740}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DynamicExpresso.Benchmarks", "benchmark\DynamicExpresso.Benchmarks\DynamicExpresso.Benchmarks.csproj", "{394496C4-B878-4F0C-8471-2417F3205FC8}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Debug|x64.Build.0 = Debug|Any CPU
+ {33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Debug|x86.Build.0 = Debug|Any CPU
{33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Release|x64.ActiveCfg = Release|Any CPU
+ {33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Release|x64.Build.0 = Release|Any CPU
+ {33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Release|x86.ActiveCfg = Release|Any CPU
+ {33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Release|x86.Build.0 = Release|Any CPU
{C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Debug|x64.Build.0 = Debug|Any CPU
+ {C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Debug|x86.Build.0 = Debug|Any CPU
{C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Release|x64.ActiveCfg = Release|Any CPU
+ {C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Release|x64.Build.0 = Release|Any CPU
+ {C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Release|x86.ActiveCfg = Release|Any CPU
+ {C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Release|x86.Build.0 = Release|Any CPU
+ {394496C4-B878-4F0C-8471-2417F3205FC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {394496C4-B878-4F0C-8471-2417F3205FC8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {394496C4-B878-4F0C-8471-2417F3205FC8}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {394496C4-B878-4F0C-8471-2417F3205FC8}.Debug|x64.Build.0 = Debug|Any CPU
+ {394496C4-B878-4F0C-8471-2417F3205FC8}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {394496C4-B878-4F0C-8471-2417F3205FC8}.Debug|x86.Build.0 = Debug|Any CPU
+ {394496C4-B878-4F0C-8471-2417F3205FC8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {394496C4-B878-4F0C-8471-2417F3205FC8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {394496C4-B878-4F0C-8471-2417F3205FC8}.Release|x64.ActiveCfg = Release|Any CPU
+ {394496C4-B878-4F0C-8471-2417F3205FC8}.Release|x64.Build.0 = Release|Any CPU
+ {394496C4-B878-4F0C-8471-2417F3205FC8}.Release|x86.ActiveCfg = Release|Any CPU
+ {394496C4-B878-4F0C-8471-2417F3205FC8}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {394496C4-B878-4F0C-8471-2417F3205FC8} = {09EED85C-BE3C-7566-DC0E-2E8E43466740}
+ EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A36C3463-448E-4051-AE87-A2994E36C1EC}
EndGlobalSection
diff --git a/README.md b/README.md
index 1533f6c..ba05cac 100644
--- a/README.md
+++ b/README.md
@@ -560,6 +560,12 @@ or run unit tests for a specific project with a specific framework:
Add `--logger:trx` to generate test results for VSTS.
+## Benchmarks
+
+This repository includes a BenchmarkDotNet project under `benchmark/DynamicExpresso.Benchmarks` to measure interpreter hot-paths.
+
+ dotnet run -c Release --project benchmark/DynamicExpresso.Benchmarks
+
## Release notes
See [releases page](https://github.com/dynamicexpresso/DynamicExpresso/releases).
diff --git a/benchmark/DynamicExpresso.Benchmarks/DynamicExpresso.Benchmarks.csproj b/benchmark/DynamicExpresso.Benchmarks/DynamicExpresso.Benchmarks.csproj
new file mode 100644
index 0000000..c2afd82
--- /dev/null
+++ b/benchmark/DynamicExpresso.Benchmarks/DynamicExpresso.Benchmarks.csproj
@@ -0,0 +1,12 @@
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
diff --git a/benchmark/DynamicExpresso.Benchmarks/LambdaBenchmarks.cs b/benchmark/DynamicExpresso.Benchmarks/LambdaBenchmarks.cs
new file mode 100644
index 0000000..ab4e079
--- /dev/null
+++ b/benchmark/DynamicExpresso.Benchmarks/LambdaBenchmarks.cs
@@ -0,0 +1,94 @@
+using BenchmarkDotNet.Attributes;
+
+namespace DynamicExpresso.Benchmarks;
+
+[MemoryDiagnoser]
+public class LambdaBenchmarks
+{
+ private const string ExpressionText =
+ "((a + b) * c - (double)d / (e + f + 1)) + Math.Max(a, b)";
+
+ private Interpreter _interpreter = null!;
+
+ private Lambda _lambda = null!;
+
+ private object[] _args = null!;
+ private Parameter[] _declared = null!;
+ private Parameter[] _parameterValues = null!;
+
+ private const int A = 1;
+ private const int B = 2;
+ private const int C = 3;
+ private const int D = 4;
+ private const int E = 5;
+ private const int F = 6;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ _interpreter = new Interpreter();
+
+ _declared = new[]
+ {
+ new Parameter("a", typeof(int)),
+ new Parameter("b", typeof(int)),
+ new Parameter("c", typeof(int)),
+ new Parameter("d", typeof(int)),
+ new Parameter("e", typeof(int)),
+ new Parameter("f", typeof(int))
+ };
+
+ _lambda = _interpreter.Parse(
+ ExpressionText,
+ typeof(double),
+ _declared);
+
+ _args = new object[] { A, B, C, D, E, F };
+
+ _parameterValues = new[]
+ {
+ new Parameter("a", typeof(int), A),
+ new Parameter("b", typeof(int), B),
+ new Parameter("c", typeof(int), C),
+ new Parameter("d", typeof(int), D),
+ new Parameter("e", typeof(int), E),
+ new Parameter("f", typeof(int), F)
+ };
+ }
+
+ [Benchmark(Description = "Invoke cached lambda (object[])")]
+ public double Invoke_ObjectArray()
+ {
+ double sum = 0;
+ for (var i = 0; i < 100_000; i++)
+ {
+ sum += (double)_lambda.Invoke(_args);
+ }
+
+ return sum;
+ }
+
+ [Benchmark(Description = "Invoke cached lambda (IEnumerable)")]
+ public double Invoke_ParametersEnumerable()
+ {
+ double sum = 0;
+ for (var i = 0; i < 100_000; i++)
+ {
+ sum += (double)_lambda.Invoke(_parameterValues);
+ }
+
+ return sum;
+ }
+
+ [Benchmark(Description = "Eval (IEnumerable)")]
+ public double Eval_ParametersEnumerable()
+ {
+ double sum = 0;
+ for (var i = 0; i < 100_000; i++)
+ {
+ sum += _interpreter.Eval(ExpressionText, _parameterValues);
+ }
+
+ return sum;
+ }
+}
diff --git a/benchmark/DynamicExpresso.Benchmarks/Program.cs b/benchmark/DynamicExpresso.Benchmarks/Program.cs
new file mode 100644
index 0000000..c9a0467
--- /dev/null
+++ b/benchmark/DynamicExpresso.Benchmarks/Program.cs
@@ -0,0 +1,3 @@
+using BenchmarkDotNet.Running;
+
+BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
diff --git a/src/DynamicExpresso.Core/Interpreter.cs b/src/DynamicExpresso.Core/Interpreter.cs
index ce84fc0..f7f49f0 100644
--- a/src/DynamicExpresso.Core/Interpreter.cs
+++ b/src/DynamicExpresso.Core/Interpreter.cs
@@ -523,7 +523,9 @@ public T Eval(string expressionText, params Parameter[] parameters)
///
public object Eval(string expressionText, Type expressionType, params Parameter[] parameters)
{
- return Parse(expressionText, expressionType, parameters).Invoke(parameters);
+ // Eval is intended for one-off expressions: prefer interpretation to avoid IL generation cost.
+ var lambda = ParseAsLambda(expressionText, expressionType, parameters, preferInterpretation: true);
+ return lambda.Invoke(parameters);
}
#endregion
@@ -548,7 +550,7 @@ public IdentifiersInfo DetectIdentifiers(string expression, DetectorOptions opti
#region Private methods
- private Lambda ParseAsLambda(string expressionText, Type expressionType, Parameter[] parameters)
+ private Lambda ParseAsLambda(string expressionText, Type expressionType, Parameter[] parameters, bool preferInterpretation = false)
{
var arguments = new ParserArguments(
expressionText,
@@ -561,7 +563,7 @@ private Lambda ParseAsLambda(string expressionText, Type expressionType, Paramet
foreach (var visitor in Visitors)
expression = visitor.Visit(expression);
- var lambda = new Lambda(expression, arguments);
+ var lambda = new Lambda(expression, arguments, preferInterpretation);
#if TEST_DetectIdentifiers
AssertDetectIdentifiers(lambda);
diff --git a/src/DynamicExpresso.Core/Lambda.cs b/src/DynamicExpresso.Core/Lambda.cs
index 23b8f91..4f7c3a7 100644
--- a/src/DynamicExpresso.Core/Lambda.cs
+++ b/src/DynamicExpresso.Core/Lambda.cs
@@ -16,16 +16,21 @@ public class Lambda
{
private readonly Expression _expression;
private readonly ParserArguments _parserArguments;
- private readonly Lazy _delegate;
+ private readonly InvocationContext _invocation;
- internal Lambda(Expression expression, ParserArguments parserArguments)
+ internal Lambda(Expression expression, ParserArguments parserArguments, bool preferInterpretation = false)
{
_expression = expression ?? throw new ArgumentNullException(nameof(expression));
_parserArguments = parserArguments ?? throw new ArgumentNullException(nameof(parserArguments));
- // Note: I always lazy compile the generic lambda. Maybe in the future this can be a setting because if I generate a typed delegate this compilation is not required.
- _delegate = new Lazy(() =>
- Expression.Lambda(_expression, _parserArguments.UsedParameters.Select(p => p.Expression).ToArray()).Compile());
+ var settings = _parserArguments.Settings;
+ _invocation = new InvocationContext(
+ expression,
+ _parserArguments.DeclaredParameters.ToArray(),
+ _parserArguments.UsedParameters.ToArray(),
+ settings.KeyComparison,
+ settings.KeyComparer,
+ preferInterpretation);
}
public Expression Expression { get { return _expression; } }
@@ -38,25 +43,26 @@ internal Lambda(Expression expression, ParserArguments parserArguments)
///
/// The used parameters.
[Obsolete("Use UsedParameters or DeclaredParameters")]
- public IEnumerable Parameters { get { return _parserArguments.UsedParameters; } }
+ public IEnumerable Parameters { get { return _invocation.UsedParameters; } }
///
/// Gets the parameters actually used in the expression parsed.
///
/// The used parameters.
- public IEnumerable UsedParameters { get { return _parserArguments.UsedParameters; } }
+ public IEnumerable UsedParameters { get { return _invocation.UsedParameters; } }
+
///
/// Gets the parameters declared when parsing the expression.
///
/// The declared parameters.
- public IEnumerable DeclaredParameters { get { return _parserArguments.DeclaredParameters; } }
+ public IEnumerable DeclaredParameters { get { return _invocation.DeclaredParameters; } }
public IEnumerable Types { get { return _parserArguments.UsedTypes; } }
public IEnumerable Identifiers { get { return _parserArguments.UsedIdentifiers; } }
public object Invoke()
{
- return InvokeWithUsedParameters(new object[0]);
+ return _invocation.InvokeNoArgs();
}
public object Invoke(params Parameter[] parameters)
@@ -64,59 +70,24 @@ public object Invoke(params Parameter[] parameters)
return Invoke((IEnumerable)parameters);
}
+ ///
+ /// Invoke the expression with the given named parameters.
+ /// Parameters are matched by name against the parameters actually used in the expression.
+ ///
public object Invoke(IEnumerable parameters)
{
- var args = (from usedParameter in UsedParameters
- from actualParameter in parameters
- where usedParameter.Name.Equals(actualParameter.Name, _parserArguments.Settings.KeyComparison)
- select actualParameter.Value)
- .ToArray();
-
- return InvokeWithUsedParameters(args);
+ return _invocation.InvokeFromNamed(parameters);
}
///
- /// Invoke the expression with the given parameters values.
+ /// Invoke the expression with the given parameter values.
+ /// The values are in the same order as the parameters declared when parsing (DeclaredParameters).
+ /// Only the parameters actually used in the expression are passed to the underlying delegate.
///
- /// Order of parameters must be the same of the parameters used during parse (DeclaredParameters).
- ///
+ /// Values for declared parameters, in declared order.
public object Invoke(params object[] args)
{
- var parameters = new List();
- var declaredParameters = DeclaredParameters.ToArray();
-
- if (args != null)
- {
- if (declaredParameters.Length != args.Length)
- throw new InvalidOperationException(ErrorMessages.ArgumentCountMismatch);
-
- for (var i = 0; i < args.Length; i++)
- {
- var parameter = new Parameter(
- declaredParameters[i].Name,
- declaredParameters[i].Type,
- args[i]);
-
- parameters.Add(parameter);
- }
- }
-
- return Invoke(parameters);
- }
-
- private object InvokeWithUsedParameters(object[] orderedArgs)
- {
- try
- {
- return _delegate.Value.DynamicInvoke(orderedArgs);
- }
- catch (TargetInvocationException exc)
- {
- if (exc.InnerException != null)
- ExceptionDispatchInfo.Capture(exc.InnerException).Throw();
-
- throw;
- }
+ return _invocation.InvokeFromDeclared(args);
}
public override string ToString()
@@ -127,7 +98,10 @@ public override string ToString()
///
/// Generate the given delegate by compiling the lambda expression.
///
- /// The delegate to generate. Delegate parameters must match the one defined when creating the expression, see UsedParameters.
+ ///
+ /// The delegate to generate. Delegate parameters must match the ones defined
+ /// when creating the expression, see DeclaredParameters.
+ ///
public TDelegate Compile()
{
var lambdaExpression = LambdaExpression();
@@ -145,24 +119,179 @@ public TDelegate Compile(IEnumerable parameters)
/// Generate a lambda expression.
///
/// The lambda expression.
- /// The delegate to generate. Delegate parameters must match the one defined when creating the expression, see UsedParameters.
+ ///
+ /// The delegate to generate. Delegate parameters must match the ones defined
+ /// when creating the expression, see DeclaredParameters.
+ ///
public Expression LambdaExpression()
{
- return Expression.Lambda(_expression, DeclaredParameters.Select(p => p.Expression).ToArray());
+ return Expression.Lambda(_expression, _invocation.DeclaredParameterExpressions);
}
internal LambdaExpression LambdaExpression(Type delegateType)
{
- var parameterExpressions = DeclaredParameters.Select(p => p.Expression).ToArray();
+ var parameterExpressions = _invocation.DeclaredParameterExpressions;
var types = delegateType.GetGenericArguments();
// return type
- if (delegateType.GetGenericTypeDefinition() == ReflectionExtensions.GetFuncType(parameterExpressions.Length))
+ var genericType = delegateType.GetGenericTypeDefinition();
+ if (genericType == ReflectionExtensions.GetFuncType(parameterExpressions.Length))
types[types.Length - 1] = _expression.Type;
- var genericType = delegateType.GetGenericTypeDefinition();
var inferredDelegateType = genericType.MakeGenericType(types);
return Expression.Lambda(inferredDelegateType, _expression, parameterExpressions);
}
+
+ private sealed class InvocationContext
+ {
+ private readonly Expression _expression;
+ private readonly Parameter[] _declaredParameters;
+ private readonly Parameter[] _usedParameters;
+ private readonly StringComparison _keyComparison;
+ private readonly IEqualityComparer _keyComparer;
+ private readonly bool _preferInterpretation;
+
+ // used index -> declared index
+ private readonly int[] _usedToDeclaredIndex;
+
+ // Delegate with parameters in UsedParameters order.
+ private readonly Lazy _delegate;
+
+ public InvocationContext(
+ Expression expression,
+ Parameter[] declaredParameters,
+ Parameter[] usedParameters,
+ StringComparison keyComparison,
+ IEqualityComparer keyComparer,
+ bool preferInterpretation)
+ {
+ _expression = expression ?? throw new ArgumentNullException(nameof(expression));
+ _declaredParameters = declaredParameters ?? Array.Empty();
+ _usedParameters = usedParameters ?? Array.Empty();
+ _keyComparison = keyComparison;
+ _keyComparer = keyComparer ?? StringComparer.InvariantCulture;
+ _preferInterpretation = preferInterpretation;
+
+ DeclaredParameterExpressions = _declaredParameters.Select(p => p.Expression).ToArray();
+
+ _delegate = new Lazy(() =>
+ Expression.Lambda(_expression, _usedParameters.Select(p => p.Expression).ToArray())
+ .Compile(_preferInterpretation));
+
+ _usedToDeclaredIndex = BuildUsedToDeclaredIndex(_declaredParameters, _usedParameters, _keyComparer);
+ }
+
+ public Parameter[] DeclaredParameters => _declaredParameters;
+ public Parameter[] UsedParameters => _usedParameters;
+ public ParameterExpression[] DeclaredParameterExpressions { get; }
+
+ public object InvokeNoArgs()
+ {
+ return InvokeWithUsedParameters(Array.Empty