Adletec.Sonic
1.2.0
See the version list below for details.
dotnet add package Adletec.Sonic --version 1.2.0
NuGet\Install-Package Adletec.Sonic -Version 1.2.0
<PackageReference Include="Adletec.Sonic" Version="1.2.0" />
paket add Adletec.Sonic --version 1.2.0
#r "nuget: Adletec.Sonic, 1.2.0"
// Install Adletec.Sonic as a Cake Addin #addin nuget:?package=Adletec.Sonic&version=1.2.0 // Install Adletec.Sonic as a Cake Tool #tool nuget:?package=Adletec.Sonic&version=1.2.0
sonic | rapid expression evaluation for .NET
sonic is a rapid evaluation engine for mathematical expressions. It can parse and evaluate strings containing mathematical expressions.
sonic is also the expression evaluator we use in our commercial products. It is a core component of our real-time simulation tools for virtual vehicle and ADAS prototyping and is continuously stress tested in a demanding environment. Its development and maintenance is funded by our product sales.
The guiding principles for sonic are (in that order):
- Performance: sonic is aiming to be the fastest expression evaluator for .NET. It is optimized for both, multi pass evaluation of the same expression and single pass evaluation of many different expressions.
- Usability: sonic is designed to be easy to use. It comes with a sane default configuration, an understandable documentation and a simple API. The most common use-cases should be fast out-of-the-box.
- Maintainability: sonic is designed to be easy to maintain. It is written in a clean and readable code style and comes with a comprehensive test and benchmarking suite. The NuGet package introduces no transient dependencies and is fully self-contained.
sonic originally started as a fork of Jace.NET by Pieter De Rycke, which is no longer actively maintained. It is not a drop-in replacement for Jace.NET, but you should be able to switch to sonic with minimal effort.
sonic is considerably faster than Jace.NET (see benchmarks below). It contains numerous bugfixes and a lot of maintenance work over the latest Jace.NET release (1.0.0). Many of them were originally suggested and developed by the community for Jace.NET, but never merged due to the dormant state of the project. See the changelog for details and a complete list.
Build Status
branch | status |
---|---|
main (development) | |
release |
Quick Start
sonic can parse and evaluate strings containing mathematical expressions. These expressions may rely on variables, which can be defined at runtime.
Consider this simple example:
var variables = new Dictionary<string, double>();
variables.Add("var1", 2.5);
variables.Add("var2", 3.4);
var engine = Evaluator.CreateWithDefaults();
double result = engine.Evaluate("var1*var2", variables); // 8.5
The Evaluator comes with out-of-the-box support for many arithmetic (+
, -
, *
, /
, ...
), trigonometric (sin
, cos
, atan
, ...
) statistic (avg
, max
, min
, median
, ...
), and simple boolean logic (if
, ifless
, ifequal
, ...) functions.
You can add your own domain-specific functions. This example adds a conversion function from length in feet (ft
) to meter (m
):
var engine = Evaluator.Create()
.AddFunction("ft2m", (Func<double, double>)((a) => a * 0.3048))
.Build();
double result = engine.Evaluate("ft2m(30)"); // 9.144
You can find more examples below.
sonic can execute formulas in two modes: dynamic compilation mode and interpreted mode. If dynamic compilation mode is used, sonic will create a dynamic method at runtime and will generate the necessary MSIL opcodes for native execution of the formula. If a formula is re-executed with other variables, sonic will take the dynamically generated method from its cache. Dynamic compilation mode is a lot faster when evaluating an expression, but has a higher overhead when building the formula.
As a rule of thumb, you should use dynamic compilation mode if you are evaluating the same expressions multiple times with different variables, and interpreted mode if you are evaluating many different expressions only once.
Additionally, for specific use-cases (e.g. Unity with IL2CPP) dynamic code generation can be limited. In those cases, you can use the interpreted mode as a fallback.
Installation
sonic is available via nuget:
dotnet add package Adletec.Sonic --version 1.2.0
Usage
Evaluating an Expression
Directly Evaluate an Expression
The easiest way to evaluate an expression is to use the Evaluate()
-method of the Evaluator
:
Dictionary<string, double> variables = new Dictionary<string, double>();
variables.Add("var1", 2.5);
variables.Add("var2", 3.4);
var engine = Evaluator.CreateWithDefaults();
double result = engine.Evaluate("var1*var2", variables);
Create a Delegate for an Expression
sonic can also create a delegate (Func) from your expression which will take the variable dictionary as argument:
var engine = Evaluator.CreateWithDefaults();
Func<Dictionary<string, double>, double> evaluate = engine.CreateDelegate("var1+2/(3*otherVariable)");
Dictionary<string, double> variables = new Dictionary<string, double>();
variables.Add("var1", 2);
variables.Add("otherVariable", 4.2);
double result = evaluate(variables);
If you intend to evaluate the same expression repeatedly with different variables, you should use this method. It will avoid the overhead of retrieving the delegate from the cache, based on the expression string. On the other hand, there is no performance benefit in using this method if you are only evaluating the expression once.
Using Mathematical Functions
You can also use mathematical functions in your expressions:
Dictionary<string, double> variables = new Dictionary<string, double>();
variables.Add("var1", 2.5);
variables.Add("var2", 3.4);
var engine = Evaluator.CreateWithDefaults();
double result = engine.Evaluate("logn(var1,var2)+4", variables);
Built-in Functions
sonic supports most common functions out-of-the-box:
Function | Signature | Parameters |
---|---|---|
Sine | sin(a) |
a : angle in radians |
Cosine | cos(a) |
a : angle in radians |
Secant | sec(a) |
a : angle in radians |
Cosecant | csc(a) |
a : angle in radians |
Tangent | tan(a) |
a : angle in radians |
Cotangent | cot(a) |
a : angle in radians |
Arcsine | asin(a) |
a : angle in radians |
Arccosine | acos(a) |
a : angle in radians |
Arctangent | atan(a) |
a : angle in radians |
Arccotangent | acot(a) |
a : angle in radians |
Natural logarithm | loge(a) |
a : number whose logarithm is to be found |
Common logarithm | log10(a) |
a : number whose logarithm is to be found |
Logarithm | logn(a, b) |
a : number whose logarithm is to be found<br/>b : base of the logarithm |
Square root | sqrt(a) |
a : number whose square root is to be found |
Absolute | abs(a) |
a : number whose absolute value is to be found |
If | if(a,b,c) |
a : boolean expression, e.g. x > 2 <br/>b : result if true(!= 0 )<br/>c : result if false (== 0 ) |
If less | ifless(a,b,c,d) |
a : first value<br/>b : second value<br/>c : result if a < b <br/>d : result if a >= b |
If more | ifmore(a,b,c,d) |
a : first value<br/>b : second value<br/>c : result if a > b <br/>d : result if a <= b |
If equal | ifequal(a,b,c,d) |
a : first value<br/>b : second value<br/>c : result if a == b <br/>d : result if a != b |
Ceiling | ceiling(a) |
a : number to be rounded towards +∞ |
Floor | floor(a) |
a : number to be rounded towards -∞ |
Truncate | truncate(a) |
a : number to be truncated (to integral part) |
Round | round(a) |
a : number to be rounded (to even) |
Maximum | max(a,b,...) |
a,b,... : series of numbers to find the maximum of |
Minimum | min(a,b,...) |
a,b,... : series of numbers to find the minimum of |
Average | avg(a,b,...) |
a,b,... : series of numbers to find the average of |
Median | median(a,b,...) |
a,b,... : series of numbers to find the median of |
Sum | median(a,b,...) |
a,b,... : series of numbers to build the sum of |
Random | random() |
no parameters, returns random number in [0..1] |
The function names are reserved keywords and cannot be overwritten. If you need to override a function, you can globaly disable the built-in functions using the configuration (see below).
Custom Functions
You can define your own functions using the .AddFunction()
-method while instanciating the evaluator.
var engine = Evaluator.Create()
.AddFunction("ft2m", (Func<double, double>)((a) => a * 0.3048))
.Build();
double result = engine.Evaluate("ft2m(30)"); // 9.144
The .AddFunction()
-method provides overloads for functions with up to 16 parameters. If you want to process an arbitrary amount of parameters, you can use dynamic functions:
double MyCustomSumFormula(params double[] a)
{
return a.Sum();
}
var engine = Evaluator.Create()
.AddFunction("customSum", MyCustomSumFormula)
.Build();
double result = engine.Evaluate("customSum(1,2,3,4,5,6)"); // 21.0
Custom function names are overwritable, so you can re-register the same name with a different implementation.
Using Constants
sonic provides support for pre-compile constants. These constants are taken into account during the optimization phase of the compilation process. I.e., if your expression contains an operation like 2 * pi
, this operation is already evaluated when you build a delegate and the result is cached.
Built-in Constants
Constant | Name |
---|---|
π | pi |
ⅇ | e |
The constant names are reserved keywords and cannot be overwritten. If you define a variable with the same name as a constant, the constant will take precedence.
If you need to override a constant, you can globaly disable the built-in constants using the configuration (see below).
Custom Constants
You can define your own constants using the .AddConstant()
-method while instanciating the evaluator.
var engine = Evaluator.Create()
.AddConstant("g", 9.80665)
.Build();
double result = engine.Evaluate("g*2"); // 19.6133
Custom constants will also be taken into account during the optimization phase of the compilation process.
Configuration
The Evaluator
-builder also allows you to configure the evaluator. The following options are available:
Option | Values (bold = default) | Comment |
---|---|---|
UseCulture(CultureInfo cultureInfo) |
CultureInfo.CurrentCulture <br/>CultureInfo.* |
Determines the number format (e.g. decimal separator, thousands separator, etc.); defaults to your system default; |
UseExecutionMode(ExecutionMode executionMode) |
ExecutionMode.Compiled <br/>ExecutionMode.Interpreted |
Compiled will dynamically compile the evaluation to MSIL which grants the best evaluation performance, but comes with the overhead of compilation. If you are using a platform where dynamic compilation is restricted, or don't want to re-evaluate the same expressions, you should use Interpreted . |
EnableCache() / DisableCache() |
enabled |
Can be used to disable the formula cache if set to false . The formula cache keeps a copy of the optimized AST for every given formula string, so it can be re-used. This makes subsequent evaluations of the same formula significantly faster. If you don't intend to re-evaluate the same expressions, you can disable the cache. This will reduce memory consumption and improve initial evaluation performance. |
EnableOptimizer() / DisableOptimizer() |
enabled |
Can be used to disable the optimizer if set to false . The optimizer will pre-evaluate parts of the equation which do not depend on variables, including multiplications with 0 or 0 exponents. You can disable the optimizer if you know for a fact that the given expressions won't contain foldable constants or if you don't intend to re-evalute the same expressions. |
EnableCaseSensitivity() / DisableCaseSensitivity() |
enabled |
Determines wether the provided variable names will be evaluated case-sensitive (enabled) or case-insensitive (disabled). If you don't absolutely need case-insensitivity, you should keep this option set to case-sensitive since this has a notable performance impact. |
EnableDefaultFunctions() / DisableDefaultFunctions() |
enabled |
Can be used to disable the built-in functions. |
EnableDefaultConstants() / DisableDefaultConstants() |
enabled |
Can be used to disable the built-in constants. |
EnableGuardedMode() / DisableGuardedMode() |
disabled |
Enables guarded mode. This means that the engine will throw exceptions for non-fatal errors, i.e. if it receives ambiguous input for which a sane default exists, but which is possibly not what the user intended. You can use this if you want to pin down hard to find bugs in your expressions. Since it comes with a severe performance impact, it is recommended to keep guarded mode disabled in production. Alas, if you prioritize validation over performance, you might decide otherwise. |
UseCacheMaximumSize(int cacheMaximumSize) |
500 <br/> |
The number of expressions to keep in the cache. |
UseCacheReductionSize(int cacheReductionSize) |
50 |
The number of expressions to drop from the cache once it reaches its maximum size (FIFO). |
All options will be applied to the evaluator and all delegates created from it. The configuration is immutable. I.e., if you want to change the configuration of an evaluator, you have to create a new one.
Performance
Benchmark
sonic is primed to deliver great performance out-of-the-box. It comes with a comprehensive benchmarking suite which is easy to run and understand.
You can use the benchmark to compare the performance of different configurations when evaluating specific expressions in a specific way or environment. The benchmark is based on BenchmarkDotNet.
To run the benchmark, you can use the following command:
dotnet run -c Release dotnet run -c Release --project Adletec.Sonic.Benchmark/Adletec.Sonic.Benchmark.csproj
Take a look at Program.cs to see how to extend or adjust the benchmark.
Comparison with other Libraries
To get a better understanding of the performance of sonic, the benchmark also includes a set of comparisons with other popular expression evaluators for .NET.
Disclaimer: Keep in mind that all those libraries have unique features and performance is only one aspect when choosing the right library for your project. The following comparison is in no way a statement towards the general superiority/inferiority of any of the listed libraries.
Benchmark Setup
We're using a simple benchmark which will take the same three equations and evaluate them using the same values with all libraries:
- Expression A - Simple expression
var1 + var2 * var3 / 2
- Expression B - Balanced expression including functions and constants:
sin(var1) + cos(var2) + pi^2
- Expression C - Foldable expression:
(var1 + var2 * var3 / 2) * 0 + 0 / (var1 + var2 * var3 / 2) + (var1 + var2 * var3 / 2)^0
To make sure the expressions will be re-evaluated, we're incrementing each variable on every iteration.
The benchmark runs all iterations on the same machine (MacBook Pro 2021, M1 Max).
Benchmark Results
Default Settings
The following table shows the time in seconds it takes the benchmark to complete 100.000 evaluations of each expression using the default settings of each library. As a reference, it also contains the time it takes to evaluate the same expressions using hardcoded C#.
Library | Expression A | Expression B | Expression C |
---|---|---|---|
Hardcoded C# | 1.036 ms | 1.029 ms | 1.036 ms |
sonic | 6.376 ms | 9.145 ms | 2.853 ms |
Jace.NET | 22.637 ms | 24.918 ms | 29.710 ms |
NCalc | 33.579 ms | 51.400 ms | 127.165 ms |
Keep in mind that this is a very specific benchmark and not entirely fair. The frameworks are using different default settings which might not be optimal for the given benchmark. You can get a better understanding of the performance of each library by running the benchmark yourself and adjusting the expressions to your needs.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
.NET Core | netcoreapp1.0 was computed. netcoreapp1.1 was computed. netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard1.6 is compatible. netstandard2.0 was computed. netstandard2.1 was computed. |
.NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen30 was computed. tizen40 was computed. tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 1.6
- NETStandard.Library (>= 1.6.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
1.5.0 | 10,281 | 2/9/2024 |
1.4.1 | 2,213 | 12/26/2023 |
1.4.0 | 476 | 11/28/2023 |
1.3.2 | 431 | 11/16/2023 |
1.3.1 | 417 | 11/12/2023 |
1.3.0 | 432 | 11/6/2023 |
1.2.0 | 507 | 9/20/2023 |
1.1.0 | 498 | 9/20/2023 |
1.1.0-beta-6 | 497 | 8/23/2023 |
1.1.0-beta-5 | 469 | 8/22/2023 |
1.1.0-beta-4 | 464 | 8/22/2023 |
1.1.0-beta-3 | 478 | 8/22/2023 |