|
1 | | -# mexce |
| 1 | +# mexce |
2 | 2 |
|
3 | | -Mini Expression Compiler/Evaluator |
| 3 | +A single-header, dependency-free JIT compiler for mathematical expressions. |
4 | 4 |
|
5 | 5 | ## Overview |
6 | 6 |
|
7 | | -mexce is a small runtime compiler of mathematical expressions, written in C++. It generates machine code that primarily uses the x87 FPU. |
8 | | -It is a single header with no dependencies. |
| 7 | +`mexce` is a runtime compiler for scalar mathematical expressions written in C++. It parses standard C-like expressions and compiles them directly into x86/x86-64 machine code that utilizes the x87 FPU. |
9 | 8 |
|
10 | | -I wrote this back in 2003 as part of a larger application and then its existence was almost forgotten. The code was now updated with added support for x64 and Data Execution Prevention. |
| 9 | +Once an expression is compiled, subsequent evaluations are direct function calls, which avoids parsing and interpretation overhead. This makes `mexce` well-suited for applications that repeatedly evaluate the same formula with different inputs, such as numerical simulations, data processing kernels, or graphics. |
11 | 10 |
|
12 | | -It currently supports Windows and Linux. |
| 11 | +The library is contained in a single header file (`mexce.h`) with no external dependencies. |
13 | 12 |
|
14 | | -## Usage |
| 13 | +### Requirements |
| 14 | +* **Platforms:** Windows, Linux |
| 15 | +* **Architectures:** x86, x86-64 (other architectures are not supported) |
| 16 | +* **Compiler:** Requires a C++11 compliant compiler. |
15 | 17 |
|
16 | | -Here is an example: |
| 18 | +## Installation |
17 | 19 |
|
18 | | -```cpp |
19 | | -float x = 0.0f; |
20 | | -double y = 0.1; |
21 | | -int z = 200; |
22 | | - |
23 | | -mexce::evaluator eval; |
24 | | - |
25 | | -// associate runtime variables with their aliases in the expression |
26 | | -eval.bind(x, "x", y, "y", z, "z"); |
27 | | - |
28 | | -eval.set_expression("0.3+(-sin(2.33+x-logb((.3*pi+(88/y)/e),3.2+z)))/988.472e-02"); |
| 20 | +Copy `mexce.h` into your project's include path and `#include "mexce.h"`. No other steps are needed. |
29 | 21 |
|
30 | | -cout << endl << "Evaluation results:" << endl; |
31 | | -for (int i = 0; i < 10; i++, x-=0.1f, y+=0.212, z+=2) { |
32 | | - cout << " " << eval.evaluate() << endl; |
33 | | -} |
34 | | -``` |
| 22 | +## Quick Start |
35 | 23 |
|
36 | | -Output: |
37 | | -``` |
38 | | -Evaluation results: |
39 | | - 0.200122 |
40 | | - 0.210523 |
41 | | - 0.224581 |
42 | | - 0.240747 |
43 | | - 0.258237 |
44 | | - 0.276433 |
45 | | - 0.294792 |
46 | | - 0.312816 |
47 | | - 0.330053 |
48 | | - 0.346095 |
49 | | -``` |
| 24 | +The following example shows how to bind variables and evaluate an expression in a loop. A `mexce::evaluator` instance initializes to the constant expression `"0"`. |
50 | 25 |
|
51 | | -## Performance |
52 | | - |
53 | | -Apparently, mexce is quite fast. |
54 | | -Here is how it compares in the [math-parser-benchmark-project](https://github.com/ArashPartow/math-parser-benchmark-project) on an AMD Zen2: |
| 26 | +```cpp |
| 27 | +#include <iostream> |
| 28 | +#include "mexce.h" |
55 | 29 |
|
| 30 | +int main() { |
| 31 | + float x = 0.0f; |
| 32 | + double y = 0.1; |
| 33 | + mexce::evaluator eval; |
56 | 34 |
|
57 | | -| # |Parser | Type | Points | Score |Failures |
58 | | - --------|---------------------|------------|------------|---------|-------- |
59 | | - ***00***|***mexce*** |***double***| ***2630***|***217***|***0*** |
60 | | - 01 | ExprTk | double | 2577| 100 | 0 |
61 | | - 02 | METL | double | 1857| 51 | 0 |
62 | | - 03 | FParser 4.5 | double | 1832| 53 | 2 |
63 | | - 04 | muparser 2.2.4 | double | 1655| 45 | 0 |
64 | | - 05 | muparserSSE | float | 1640| 89 | 58 |
65 | | - 06 | ExprTkFloat | float | 1635| 61 | 62 |
66 | | - 07 | atmsp 1.0.4 | double | 1616| 45 | 4 |
67 | | - 08 | muparser 2.2.4 (omp)| double | 1442| 43 | 0 |
68 | | - 09 | TinyExpr | double | 1183| 37 | 3 |
69 | | - 10 | MathExpr | double | 930| 27 | 12 |
70 | | - 11 | MTParser | double | 836| 29 | 9 |
71 | | - 12 | Lepton | double | 438| 9 | 4 |
72 | | - 13 | muparserx | double | 239| 5 | 0 |
| 35 | + // Associate runtime variables with aliases in the expression. |
| 36 | + eval.bind(x, "x", y, "y"); |
73 | 37 |
|
74 | | -Full results [here](https://github.com/imakris/mexce/blob/master/test/bench_expr_all_results.txt) |
| 38 | + eval.set_expression("sin(x) + y"); |
75 | 39 |
|
76 | | -## Benchmarks |
| 40 | + // The evaluator can also be used for single-shot evaluations |
| 41 | + // without changing the main expression. |
| 42 | + double result = eval.evaluate("cos(pi / 4)"); |
| 43 | + std::cout << "Single-shot evaluation: " << result << std::endl; |
77 | 44 |
|
78 | | -This repository ships with a small harness (`test/benchmark.cpp`) and the |
79 | | -`test/bench_expr*.txt` suites from the |
80 | | -[math-parser-benchmark-project](https://github.com/ArashPartow/math-parser-benchmark-project). |
81 | | -You can build and run the benchmarks using CMake: |
| 45 | + // Loop with the main expression |
| 46 | + std::cout << "\nLoop evaluation results:" << std::endl; |
| 47 | + for (int i = 0; i < 5; ++i, x += 0.1f) { |
| 48 | + std::cout << " " << eval.evaluate() << std::endl; |
| 49 | + } |
82 | 50 |
|
| 51 | + return 0; |
| 52 | +} |
83 | 53 | ``` |
| 54 | + |
| 55 | +## API Reference |
| 56 | + |
| 57 | +#### `bind()` |
| 58 | +Associates a C++ variable with a symbolic name. |
| 59 | +* **Signature:** `void bind(T& var, const std::string& name, ...);` |
| 60 | +* **Supported Types:** `double`, `float`, `int16_t`, `int32_t`, `int64_t`. |
| 61 | +* **Behavior:** |
| 62 | + * Bound variables must outlive the `mexce::evaluator` instance. |
| 63 | + * Throws `std::logic_error` if `name` collides with a built-in function or constant. |
| 64 | + |
| 65 | +#### `unbind() / unbind_all()` |
| 66 | +Removes one or all variable bindings. |
| 67 | +* **Signature:** `void unbind(const std::string& name, ...);`, `void unbind_all();` |
| 68 | +* **Behavior:** |
| 69 | + * If a variable used by the currently compiled expression is unbound, the expression is safely reset to the constant `"0"`. |
| 70 | + * Throws `std::logic_error` if `name` is unknown or empty. |
| 71 | + |
| 72 | +#### `set_expression()` |
| 73 | +Compiles an expression, making it the default for `evaluate()`. |
| 74 | +* **Signature:** `void set_expression(std::string expr);` |
| 75 | +* **Behavior:** |
| 76 | + * Throws `mexce_parsing_exception` on syntax errors, providing the position of the error. |
| 77 | + * Throws `std::logic_error` if the expression string is empty. |
| 78 | + |
| 79 | +#### `evaluate()` |
| 80 | +Executes the expression most recently compiled by `set_expression()`. |
| 81 | +* **Signature:** `double evaluate();` |
| 82 | + |
| 83 | +#### `evaluate(const std::string&)` |
| 84 | +Compiles and executes an expression for a single use without replacing the default expression. |
| 85 | +* **Signature:** `double evaluate(const std::string& expression);` |
| 86 | + |
| 87 | +## Expression Syntax |
| 88 | + |
| 89 | +`mexce` supports standard mathematical notation. |
| 90 | + |
| 91 | +* **Literals:** Numbers in decimal (`123.45`) or scientific (`1.2345e+02`) notation. |
| 92 | +* **Operators:** Infix operators with the following precedence: |
| 93 | + | Precedence | Operator | Function | Description | |
| 94 | + | :--- | :--- | :--- | :--- | |
| 95 | + | 1 (highest) | `^` | `pow` | Power / Exponentiation | |
| 96 | + | 2 | `*`, `/` | `mul`, `div` | Multiplication, Division | |
| 97 | + | 3 | `+`, `-` | `add`, `sub` | Addition, Subtraction | |
| 98 | + | 4 (lowest) | `<` | `less_than`| Less-than comparison | |
| 99 | +* **Unary Operators:** Unary `+` and `-` are supported. |
| 100 | +* **Comparison:** The `<` operator returns a `double` (`1.0` if true, `0.0` if false). |
| 101 | + |
| 102 | +## Built-in Identifiers |
| 103 | + |
| 104 | +### Constants |
| 105 | +* `pi`: The mathematical constant π. |
| 106 | +* `e`: Euler's number *e*. |
| 107 | + |
| 108 | +### Functions |
| 109 | +| Function | Description | |
| 110 | +| :--- | :--- | |
| 111 | +| `add(a,b)`, `sub(a,b)`, `mul(a,b)`, `div(a,b)` | Basic arithmetic. | |
| 112 | +| `neg(x)` | Negation (unary minus). | |
| 113 | +| `abs(x)` | Absolute value. | |
| 114 | +| `mod(a,b)` | Modulo operator. | |
| 115 | +| `min(a,b)`, `max(a,b)` | Minimum and maximum. | |
| 116 | +| `sin(x)`, `cos(x)`, `tan(x)` | Trigonometric functions. | |
| 117 | +| `pow(base, exp)` | General exponentiation. | |
| 118 | +| `exp(x)` | Base-e exponent (`e^x`). | |
| 119 | +| `sqrt(x)` | Square root. | |
| 120 | +| `ln(x)` / `log(x)` | Natural logarithm. | |
| 121 | +| `log2(x)`, `log10(x)` | Base-2 and Base-10 logarithms. | |
| 122 | +| `logb(base, value)` | Logarithm with a custom base. | |
| 123 | +| `ylog2(y, x)` | Computes `y * log2(x)`. | |
| 124 | +| `ceil(x)`, `floor(x)`, `round(x)`, `int(x)` | Rounding functions. | |
| 125 | +| `less_than(a, b)` | Returns `1.0` if `a < b`, else `0.0`. | |
| 126 | +| `sign(x)` | Returns `-1.0` for negative `x`, `1.0` otherwise. | |
| 127 | +| `signp(x)` | Returns `1.0` for positive `x`, `0.0` otherwise. | |
| 128 | +| `bnd(x, period)` | Wraps `x` to the interval `[0, period)`. | |
| 129 | +| `bias(x, a)`, `gain(x, a)` | Common tone-mapping curves (for inputs in `[0,1]`). | |
| 130 | +| `expn(x)` | Returns the exponent part of `x`. | |
| 131 | +| `sfc(x)` | Returns the significand (fractional part) of `x`. | |
| 132 | + |
| 133 | +### Configuration |
| 134 | +* **`MEXCE_ACCURACY`:** Define this macro before including `mexce.h` to enable higher-precision polynomial refinements for `sin()` and `cos()`, trading a small runtime cost for improved accuracy. |
| 135 | + |
| 136 | +## Performance Analysis |
| 137 | + |
| 138 | +`mexce` is designed to produce code with performance comparable to a statically optimizing compiler. Its efficiency was measured using a benchmark suite of 44,229 expressions. |
| 139 | + |
| 140 | +### Benchmark Methodology |
| 141 | +* **System:** AMD Ryzen 7 7840U CPU |
| 142 | +* **Compiler:** GNU GCC 13.1.0 with flags `-O3 -DNDEBUG -Wall -Wextra -Wpedantic`. |
| 143 | +* **Reference Standard:** A high-precision "golden reference" for each expression was generated using Python's **SymPy** library with arbitrary-precision rationals. |
| 144 | +* **Accuracy Metric (ULP):** **Units in the Last Place (ULP)** measures the distance between two floating-point numbers by counting how many representable values exist between them. A ULP of 0 means the numbers are identical. The ULP is computed via a monotonic mapping of floating-point values to integers and finding their absolute difference. |
| 145 | + |
| 146 | +### Speed |
| 147 | + |
| 148 | +The benchmark measures the average time per evaluation. For this scalar workload, `mexce`'s JIT-compiled code performed favorably against statically compiled C++ functions. |
| 149 | + |
| 150 | +| Metric | Mexce | Native Compiler | |
| 151 | +| :--- | :--- | :--- | |
| 152 | +| **Functions Benchmarked** | 44,229 | 44,229 | |
| 153 | +| **Average Runtime per Function** | **4.0 ns** | 5.0 ns | |
| 154 | +| **Total Execution Time** | 19.50 sec | 21.34 sec | |
| 155 | + |
| 156 | +The performance characteristics are attributed to the code generation strategy. For sequential scalar floating-point math, the x87 FPU's stack-based architecture can be more compact and efficient than the register-to-register operations of SSE/AVX instruction sets. |
| 157 | + |
| 158 | +### Accuracy |
| 159 | + |
| 160 | +`mexce`'s accuracy is comparable to that of the native compiler. |
| 161 | + |
| 162 | +#### Accuracy Distribution (ULP) |
| 163 | + |
| 164 | +| ULP Range | Mexce vs Reference | Compiler vs Reference | Mexce vs Compiler | |
| 165 | +|----------------|---------------------|-----------------------|-------------------| |
| 166 | +| 0 (exact) | 20,164 | 16,636 | 24,183 | |
| 167 | +| 1–16 | 23,494 | 26,870 | 19,537 | |
| 168 | +| 17–32 | 198 | 279 | 181 | |
| 169 | +| 33–64 | 136 | 152 | 116 | |
| 170 | +| 65–128 | 60 | 97 | 59 | |
| 171 | +| 129–256 | 33 | 50 | 31 | |
| 172 | +| 257–512 | 66 | 31 | 38 | |
| 173 | +| 513–1024 | 13 | 25 | 17 | |
| 174 | +| 1025–2048 | 13 | 19 | 11 | |
| 175 | +| 2049–4096 | 21 | 8 | 12 | |
| 176 | +| 4097–8192 | 6 | 13 | 12 | |
| 177 | +| 8193–16,384 | 7 | 8 | 3 | |
| 178 | +| 16,385–32,768 | 1 | 2 | 2 | |
| 179 | +| 32,769–65,536 | 2 | 1 | 1 | |
| 180 | +| >65,536 | 15 | 31 | 19 | |
| 181 | + |
| 182 | +#### Analysis of Large Deviations |
| 183 | +The few cases with very large ULP deviations occur where the mathematically correct result is infinity. The symbolic reference engine correctly returns `inf`. However, finite-precision floating-point hardware correctly handles this by overflowing to a very large finite number. This is the expected behavior emulated by both `mexce` and the native compiler. |
| 184 | + |
| 185 | +## Building the Benchmarks |
| 186 | + |
| 187 | +The benchmark harness is included in the repository and can be run using CMake: |
| 188 | + |
| 189 | +```bash |
| 190 | +# Configure and build the project |
84 | 191 | cmake -S . -B build |
85 | 192 | cmake --build build |
| 193 | + |
| 194 | +# Run quick validation tests |
86 | 195 | ctest --test-dir build |
| 196 | + |
| 197 | +# Run the full performance benchmark |
87 | 198 | cmake --build build --target run_benchmarks |
88 | 199 | ``` |
89 | | - |
90 | | -The `run_benchmarks` target writes per-suite summaries to |
91 | | -`bench_expr_results.txt` and `bench_expr_all_results.txt` in the build |
92 | | -directory. The `ctest` invocation executes both suites with a reduced number |
93 | | -of iterations to keep the runtime manageable while still validating that mexce |
94 | | -successfully parses and evaluates every expression. |
| 200 | +* The benchmark harness requires **OpenMP** for its timer; this is not a dependency of the `mexce` library itself. |
95 | 201 |
|
96 | 202 | ## License |
97 | 203 |
|
98 | | -The source code of the library is licensed under the Simplified BSD License. |
| 204 | +The source code is licensed under the Simplified BSD License. |
0 commit comments