Skip to content

Commit 31f4190

Browse files
committed
updated README.md
1 parent 24df4d0 commit 31f4190

1 file changed

Lines changed: 177 additions & 71 deletions

File tree

README.md

Lines changed: 177 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,204 @@
1-
# mexce
1+
# mexce
22

3-
Mini Expression Compiler/Evaluator
3+
A single-header, dependency-free JIT compiler for mathematical expressions.
44

55
## Overview
66

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.
98

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.
1110

12-
It currently supports Windows and Linux.
11+
The library is contained in a single header file (`mexce.h`) with no external dependencies.
1312

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.
1517

16-
Here is an example:
18+
## Installation
1719

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.
2921

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
3523

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"`.
5025

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"
5529

30+
int main() {
31+
float x = 0.0f;
32+
double y = 0.1;
33+
mexce::evaluator eval;
5634

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");
7337

74-
Full results [here](https://github.com/imakris/mexce/blob/master/test/bench_expr_all_results.txt)
38+
eval.set_expression("sin(x) + y");
7539

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;
7744

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+
}
8250

51+
return 0;
52+
}
8353
```
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
84191
cmake -S . -B build
85192
cmake --build build
193+
194+
# Run quick validation tests
86195
ctest --test-dir build
196+
197+
# Run the full performance benchmark
87198
cmake --build build --target run_benchmarks
88199
```
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.
95201

96202
## License
97203

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

Comments
 (0)