From ac4e342b53571306f39a1b86c66b8ef0ff9ef408 Mon Sep 17 00:00:00 2001 From: Christian Illy Date: Thu, 1 Apr 2021 14:30:27 +0200 Subject: [PATCH] Added instance caches Reduced allocations for string tests --- src/NCalc/Domain/EvaluationVisitor.cs | 250 +++++++++----------------- src/NCalc/Domain/Function.cs | 2 + src/NCalc/EvaluateFunctionHandler.cs | 2 +- src/NCalc/EvaluationOption.cs | 7 +- src/NCalc/Expression.cs | 24 ++- src/NCalc/FunctionArgs.cs | 3 +- 6 files changed, 117 insertions(+), 171 deletions(-) diff --git a/src/NCalc/Domain/EvaluationVisitor.cs b/src/NCalc/Domain/EvaluationVisitor.cs index 6d73982..5264e05 100644 --- a/src/NCalc/Domain/EvaluationVisitor.cs +++ b/src/NCalc/Domain/EvaluationVisitor.cs @@ -280,26 +280,53 @@ namespace NCalc.Domain public override void Visit(Function function) { - var args = new FunctionArgs - { - Parameters = new Expression[function.Expressions.Length] - }; + FunctionArgs args; + + if ((_options & EvaluateOptions.ReuseInstances) == EvaluateOptions.ReuseInstances) + { + if (function.ArgumentsCache == null) + { + function.ArgumentsCache = new FunctionArgs + { + Parameters = new Expression[function.Expressions.Length] + }; + + for (int i = 0; i < function.Expressions.Length; i++ ) + { + function.ArgumentsCache.Parameters[i] = new Expression(function.Expressions[i], _options); + function.ArgumentsCache.Parameters[i].EvaluateFunction += EvaluateFunction; + function.ArgumentsCache.Parameters[i].EvaluateParameter += EvaluateParameter; + } + } + + args = function.ArgumentsCache; + } else { + args = new FunctionArgs + { + Parameters = new Expression[function.Expressions.Length] + }; + + for (int i = 0; i < function.Expressions.Length; i++ ) + { + args.Parameters[i] = new Expression(function.Expressions[i], _options); + args.Parameters[i].EvaluateFunction += EvaluateFunction; + args.Parameters[i].EvaluateParameter += EvaluateParameter; + } + } // Don't call parameters right now, instead let the function do it as needed. // Some parameters shouldn't be called, for instance, in a if(), the "not" value might be a division by zero // Evaluating every value could produce unexpected behaviour for (int i = 0; i < function.Expressions.Length; i++ ) { - args.Parameters[i] = new Expression(function.Expressions[i], _options); - args.Parameters[i].EvaluateFunction += EvaluateFunction; - args.Parameters[i].EvaluateParameter += EvaluateParameter; - // Assign the parameters of the Expression to the arguments so that custom Functions and Parameters can use them args.Parameters[i].Parameters = Parameters; } + string functionIdentifierName = function.Identifier.Name; + // Calls external implementation - OnEvaluateFunction(IgnoreCase ? function.Identifier.Name.ToLower() : function.Identifier.Name, args); + OnEvaluateFunction(functionIdentifierName, args); // If an external implementation was found get the result back if (args.HasResult) @@ -308,12 +335,10 @@ namespace NCalc.Domain return; } - switch (function.Identifier.Name.ToLower()) + if (functionIdentifierName.Equals ("abs", StringComparison.OrdinalIgnoreCase)) { #region Abs - case "abs": - - CheckCase("Abs", function.Identifier.Name); + CheckCase("Abs", functionIdentifierName); if (function.Expressions.Length != 1) throw new ArgumentException("Abs() takes exactly 1 argument"); @@ -331,170 +356,110 @@ namespace NCalc.Domain Evaluate(function.Expressions[0])) ); } - - break; - #endregion - + } else if (functionIdentifierName.Equals ("acos", StringComparison.OrdinalIgnoreCase)) { #region Acos - case "acos": - CheckCase("Acos", function.Identifier.Name); + CheckCase("Acos", functionIdentifierName); if (function.Expressions.Length != 1) throw new ArgumentException("Acos() takes exactly 1 argument"); Result = Math.Acos(Convert.ToDouble(Evaluate(function.Expressions[0]))); - - break; - #endregion - + } else if (functionIdentifierName.Equals ("asin", StringComparison.OrdinalIgnoreCase)) { #region Asin - case "asin": - - CheckCase("Asin", function.Identifier.Name); + CheckCase("Asin", functionIdentifierName); if (function.Expressions.Length != 1) throw new ArgumentException("Asin() takes exactly 1 argument"); Result = Math.Asin(Convert.ToDouble(Evaluate(function.Expressions[0]))); - - break; - #endregion - + } else if (functionIdentifierName.Equals ("atan", StringComparison.OrdinalIgnoreCase)) { #region Atan - case "atan": - - CheckCase("Atan", function.Identifier.Name); + CheckCase("Atan", functionIdentifierName); if (function.Expressions.Length != 1) throw new ArgumentException("Atan() takes exactly 1 argument"); Result = Math.Atan(Convert.ToDouble(Evaluate(function.Expressions[0]))); - - break; - #endregion - + } else if (functionIdentifierName.Equals ("ceiling", StringComparison.OrdinalIgnoreCase)) { #region Ceiling - case "ceiling": - - CheckCase("Ceiling", function.Identifier.Name); + CheckCase("Ceiling", functionIdentifierName); if (function.Expressions.Length != 1) throw new ArgumentException("Ceiling() takes exactly 1 argument"); Result = Math.Ceiling(Convert.ToDouble(Evaluate(function.Expressions[0]))); - - break; - #endregion - + } else if (functionIdentifierName.Equals ("cos", StringComparison.OrdinalIgnoreCase)) { #region Cos - - case "cos": - - CheckCase("Cos", function.Identifier.Name); + CheckCase("Cos", functionIdentifierName); if (function.Expressions.Length != 1) throw new ArgumentException("Cos() takes exactly 1 argument"); Result = Math.Cos(Convert.ToDouble(Evaluate(function.Expressions[0]))); - - break; - #endregion - + } else if (functionIdentifierName.Equals ("exp", StringComparison.OrdinalIgnoreCase)) { #region Exp - case "exp": - - CheckCase("Exp", function.Identifier.Name); + CheckCase("Exp", functionIdentifierName); if (function.Expressions.Length != 1) throw new ArgumentException("Exp() takes exactly 1 argument"); Result = Math.Exp(Convert.ToDouble(Evaluate(function.Expressions[0]))); - - break; - #endregion - + } else if (functionIdentifierName.Equals ("floor", StringComparison.OrdinalIgnoreCase)) { #region Floor - case "floor": - - CheckCase("Floor", function.Identifier.Name); + CheckCase("Floor", functionIdentifierName); if (function.Expressions.Length != 1) throw new ArgumentException("Floor() takes exactly 1 argument"); Result = Math.Floor(Convert.ToDouble(Evaluate(function.Expressions[0]))); - - break; - #endregion - + } else if (functionIdentifierName.Equals ("ieeeremainder", StringComparison.OrdinalIgnoreCase)) { #region IEEERemainder - case "ieeeremainder": - - CheckCase("IEEERemainder", function.Identifier.Name); + CheckCase("IEEERemainder", functionIdentifierName); if (function.Expressions.Length != 2) throw new ArgumentException("IEEERemainder() takes exactly 2 arguments"); Result = Math.IEEERemainder(Convert.ToDouble(Evaluate(function.Expressions[0])), Convert.ToDouble(Evaluate(function.Expressions[1]))); - - break; - #endregion - + } else if (functionIdentifierName.Equals ("log", StringComparison.OrdinalIgnoreCase)) { #region Log - case "log": - - CheckCase("Log", function.Identifier.Name); + CheckCase("Log", functionIdentifierName); if (function.Expressions.Length != 2) throw new ArgumentException("Log() takes exactly 2 arguments"); Result = Math.Log(Convert.ToDouble(Evaluate(function.Expressions[0])), Convert.ToDouble(Evaluate(function.Expressions[1]))); - - break; - #endregion - + } else if (functionIdentifierName.Equals ("log10", StringComparison.OrdinalIgnoreCase)) { #region Log10 - case "log10": - - CheckCase("Log10", function.Identifier.Name); + CheckCase("Log10", functionIdentifierName); if (function.Expressions.Length != 1) throw new ArgumentException("Log10() takes exactly 1 argument"); Result = Math.Log10(Convert.ToDouble(Evaluate(function.Expressions[0]))); - - break; - #endregion - + } else if (functionIdentifierName.Equals ("pow", StringComparison.OrdinalIgnoreCase)) { #region Pow - case "pow": - - CheckCase("Pow", function.Identifier.Name); + CheckCase("Pow", functionIdentifierName); if (function.Expressions.Length != 2) throw new ArgumentException("Pow() takes exactly 2 arguments"); Result = Math.Pow(Convert.ToDouble(Evaluate(function.Expressions[0])), Convert.ToDouble(Evaluate(function.Expressions[1]))); - - break; - #endregion - + } else if (functionIdentifierName.Equals ("round", StringComparison.OrdinalIgnoreCase)) { #region Round - case "round": - - CheckCase("Round", function.Identifier.Name); + CheckCase("Round", functionIdentifierName); if (function.Expressions.Length != 2) throw new ArgumentException("Round() takes exactly 2 arguments"); @@ -502,85 +467,55 @@ namespace NCalc.Domain MidpointRounding rounding = (_options & EvaluateOptions.RoundAwayFromZero) == EvaluateOptions.RoundAwayFromZero ? MidpointRounding.AwayFromZero : MidpointRounding.ToEven; Result = Math.Round(Convert.ToDouble(Evaluate(function.Expressions[0])), Convert.ToInt16(Evaluate(function.Expressions[1])), rounding); - - break; - #endregion - + } else if (functionIdentifierName.Equals ("sign", StringComparison.OrdinalIgnoreCase)) { #region Sign - case "sign": - - CheckCase("Sign", function.Identifier.Name); + CheckCase("Sign", functionIdentifierName); if (function.Expressions.Length != 1) throw new ArgumentException("Sign() takes exactly 1 argument"); Result = Math.Sign(Convert.ToDouble(Evaluate(function.Expressions[0]))); - - break; - #endregion - + } else if (functionIdentifierName.Equals ("sin", StringComparison.OrdinalIgnoreCase)) { #region Sin - case "sin": - - CheckCase("Sin", function.Identifier.Name); + CheckCase("Sin", functionIdentifierName); if (function.Expressions.Length != 1) throw new ArgumentException("Sin() takes exactly 1 argument"); Result = Math.Sin(Convert.ToDouble(Evaluate(function.Expressions[0]))); - - break; - #endregion - + } else if (functionIdentifierName.Equals ("sqrt", StringComparison.OrdinalIgnoreCase)) { #region Sqrt - case "sqrt": - - CheckCase("Sqrt", function.Identifier.Name); + CheckCase("Sqrt", functionIdentifierName); if (function.Expressions.Length != 1) throw new ArgumentException("Sqrt() takes exactly 1 argument"); Result = Math.Sqrt(Convert.ToDouble(Evaluate(function.Expressions[0]))); - - break; - #endregion - + } else if (functionIdentifierName.Equals ("tan", StringComparison.OrdinalIgnoreCase)) { #region Tan - case "tan": - - CheckCase("Tan", function.Identifier.Name); + CheckCase("Tan", functionIdentifierName); if (function.Expressions.Length != 1) throw new ArgumentException("Tan() takes exactly 1 argument"); Result = Math.Tan(Convert.ToDouble(Evaluate(function.Expressions[0]))); - - break; - #endregion - + } else if (functionIdentifierName.Equals ("truncate", StringComparison.OrdinalIgnoreCase)) { #region Truncate - case "truncate": - - CheckCase("Truncate", function.Identifier.Name); + CheckCase("Truncate", functionIdentifierName); if (function.Expressions.Length != 1) throw new ArgumentException("Truncate() takes exactly 1 argument"); Result = Math.Truncate(Convert.ToDouble(Evaluate(function.Expressions[0]))); - - break; - #endregion - + } else if (functionIdentifierName.Equals ("max", StringComparison.OrdinalIgnoreCase)) { #region Max - case "max": - - CheckCase("Max", function.Identifier.Name); + CheckCase("Max", functionIdentifierName); if (function.Expressions.Length != 2) throw new ArgumentException("Max() takes exactly 2 arguments"); @@ -589,14 +524,10 @@ namespace NCalc.Domain object maxright = Evaluate(function.Expressions[1]); Result = Numbers.Max(maxleft, maxright); - break; - #endregion - + } else if (functionIdentifierName.Equals ("min", StringComparison.OrdinalIgnoreCase)) { #region Min - case "min": - - CheckCase("Min", function.Identifier.Name); + CheckCase("Min", functionIdentifierName); if (function.Expressions.Length != 2) throw new ArgumentException("Min() takes exactly 2 arguments"); @@ -605,14 +536,10 @@ namespace NCalc.Domain object minright = Evaluate(function.Expressions[1]); Result = Numbers.Min(minleft, minright); - break; - #endregion - + } else if (functionIdentifierName.Equals ("if", StringComparison.OrdinalIgnoreCase)) { #region if - case "if": - - CheckCase("if", function.Identifier.Name); + CheckCase("if", functionIdentifierName); if (function.Expressions.Length != 3) throw new ArgumentException("if() takes exactly 3 arguments"); @@ -620,14 +547,10 @@ namespace NCalc.Domain bool cond = Convert.ToBoolean(Evaluate(function.Expressions[0])); Result = cond ? Evaluate(function.Expressions[1]) : Evaluate(function.Expressions[2]); - break; - #endregion - + } else if (functionIdentifierName.Equals ("in", StringComparison.OrdinalIgnoreCase)) { #region in - case "in": - - CheckCase("in", function.Identifier.Name); + CheckCase("in", functionIdentifierName); if (function.Expressions.Length < 2) throw new ArgumentException("in() takes at least 2 arguments"); @@ -648,13 +571,10 @@ namespace NCalc.Domain } Result = evaluation; - break; - #endregion - - default: + } else { throw new ArgumentException("Function not found", - function.Identifier.Name); + functionIdentifierName); } } @@ -662,7 +582,7 @@ namespace NCalc.Domain { if (IgnoreCase) { - if (function.ToLower() == called.ToLower()) + if (string.Equals (function, called, StringComparison.OrdinalIgnoreCase)) { return; } @@ -670,7 +590,7 @@ namespace NCalc.Domain throw new ArgumentException("Function not found", called); } - if (function != called) + if (!string.Equals (function, called, StringComparison.Ordinal)) { throw new ArgumentException(String.Format("Function not found {0}. Try {1} instead.", called, function)); } @@ -681,7 +601,7 @@ namespace NCalc.Domain private void OnEvaluateFunction(string name, FunctionArgs args) { if (EvaluateFunction != null) - EvaluateFunction(name, args); + EvaluateFunction(name, args, IgnoreCase); } public override void Visit(Identifier parameter) diff --git a/src/NCalc/Domain/Function.cs b/src/NCalc/Domain/Function.cs index 647a16b..965585c 100644 --- a/src/NCalc/Domain/Function.cs +++ b/src/NCalc/Domain/Function.cs @@ -12,6 +12,8 @@ namespace NCalc.Domain public LogicalExpression[] Expressions { get; set; } + public FunctionArgs ArgumentsCache { get; set; } + public override void Accept(LogicalExpressionVisitor visitor) { visitor.Visit(this); diff --git a/src/NCalc/EvaluateFunctionHandler.cs b/src/NCalc/EvaluateFunctionHandler.cs index 0827734..425ff94 100644 --- a/src/NCalc/EvaluateFunctionHandler.cs +++ b/src/NCalc/EvaluateFunctionHandler.cs @@ -1,4 +1,4 @@ namespace NCalc { - public delegate void EvaluateFunctionHandler(string name, FunctionArgs args); + public delegate void EvaluateFunctionHandler(string name, FunctionArgs args, bool ignoreCase); } diff --git a/src/NCalc/EvaluationOption.cs b/src/NCalc/EvaluationOption.cs index bb0290e..4e468d9 100644 --- a/src/NCalc/EvaluationOption.cs +++ b/src/NCalc/EvaluationOption.cs @@ -52,6 +52,11 @@ namespace NCalc /// /// Defines a "null" parameter and allows comparison of values to null. /// - AllowNullParameter = 1 << 10 + AllowNullParameter = 1 << 10, + + /// + /// Reuse instances of visitors, arg arrays etc over multiple Evaluate invocations + /// + ReuseInstances = 1 << 11, } } diff --git a/src/NCalc/Expression.cs b/src/NCalc/Expression.cs index 813021d..36743db 100644 --- a/src/NCalc/Expression.cs +++ b/src/NCalc/Expression.cs @@ -182,6 +182,8 @@ namespace NCalc public LogicalExpression ParsedExpression { get; private set; } + private EvaluationVisitor Visitor; + protected Dictionary ParameterEnumerators; protected Dictionary ParametersBackup; @@ -249,9 +251,25 @@ namespace NCalc } - var visitor = new EvaluationVisitor(Options); - visitor.EvaluateFunction += EvaluateFunction; - visitor.EvaluateParameter += EvaluateParameter; + EvaluationVisitor visitor; + + if ((Options & EvaluateOptions.ReuseInstances) == EvaluateOptions.ReuseInstances) + { + if (Visitor == null) + { + Visitor = new EvaluationVisitor(Options); + Visitor.EvaluateFunction += EvaluateFunction; + Visitor.EvaluateParameter += EvaluateParameter; + } + visitor = Visitor; + } + else + { + visitor = new EvaluationVisitor(Options); + visitor.EvaluateFunction += EvaluateFunction; + visitor.EvaluateParameter += EvaluateParameter; + } + visitor.Parameters = Parameters; // Add a "null" parameter which returns null if configured to do so diff --git a/src/NCalc/FunctionArgs.cs b/src/NCalc/FunctionArgs.cs index 2c5cd73..d373f7f 100644 --- a/src/NCalc/FunctionArgs.cs +++ b/src/NCalc/FunctionArgs.cs @@ -4,6 +4,7 @@ namespace NCalc { public class FunctionArgs : EventArgs { + private static readonly Expression[] emptyArray = new Expression[0]; private object _result; public object Result @@ -18,7 +19,7 @@ namespace NCalc public bool HasResult { get; set; } - private Expression[] _parameters = new Expression[0]; + private Expression[] _parameters = emptyArray; public Expression[] Parameters { -- 2.28.0.windows.1